Skip to content

CLUE-610: Deferred open and create-time ownership rules - #2951

Open
scytacki wants to merge 14 commits into
CLUE-610-class-wide-documentsfrom
CLUE-610-deferred-open-and-create-rules
Open

CLUE-610: Deferred open and create-time ownership rules#2951
scytacki wants to merge 14 commits into
CLUE-610-class-wide-documentsfrom
CLUE-610-deferred-open-and-create-rules

Conversation

@scytacki

@scytacki scytacki commented Aug 7, 2026

Copy link
Copy Markdown
Member

Stacked on #2949 — review that first; this PR targets its branch.

CLUE-610's remaining half: the things that should not be left open once a real curriculum unit turns the class-wide document on.

Opening a unit no longer waits on the class-wide document

getOrCreateCanonicalDocument splits into a resolver — which converges the whole class onto one document per slot — and a thin opener over it. A declared class-wide document is now resolved at unit load but not opened, so the fast path costs one pointer read instead of a second metadata read, an RTDB fetch, a DocumentModel, and a history subscription for every student on every load. That was measured at ~700ms.

Group documents are unchanged: getOrCreateGroupDocument still opens, and because the resolver hands back the metadata it already holds on the legacy and create paths, opening one costs no extra read.

The document is opened on demand instead — from Sort Work, or by guaranteeInitialDocuments after a reload with it as the primary document. That second path was gated on groupDocumentsEnabled, which a unit can legitimately leave off while declaring classWideDocuments; the gate now covers both, so such a unit no longer loses its primary document on reload.

A document can no longer be created under someone else's name

isValidDocumentCreateRequest constrained neither uid nor concurrent, so a class member could create a document stamped with a classmate's id — it would then appear under that classmate's name in Sort Work — or one already marked class-shared, which hands the whole class read and write on its history.

The obvious fix does not apply, because documents several people edit together are deliberately owned by a synthetic id rather than a person. So the rule accepts three shapes, each corroborated by the caller's token or by fields the document itself carries: the caller; class_<class_hash> of the caller's own class; or group_<offeringId>_<groupId> agreeing with the document's own offeringId and groupId. concurrent: true at creation requires one of the two synthetic owners.

Two residuals are deliberate, and are recorded in docs/document-axes/README.md and in the rules comments rather than left implicit:

  • Nothing proves the caller is a member of the group they name. Group membership lives in the Realtime Database and the token carries no group claim, so a student can still create a document owned by another group in their own offering.
  • Any class member may mint documents owned by their own class. The class identity is corroborated; the authority to mint under it is not. That is inherent — minting is how a class converges on one document.

concurrent also remains settable on an existing type: "group" document, a transitional allowance the backfills need. CLUE-612 closes it once CLUE-604's migration has drained.

Deploying this

The rules must go out a release ahead of the app, per the usual pattern. Group and class-wide document creation is expected to fail in the window between the two — the canonical pointer paths in #2949 move — and neither is in use in a released unit. Nothing else may break, which is what the new "document shapes the deployed client creates" tests hold: one create per type the current client writes, all still permitted. Deliberately no keys().hasOnly(...) allowlist on the create — that would have to enumerate every shape the deployed client writes and is the change most likely to break unrelated document types.

Verification

npm test (3593 passed), check:types, lint:build, and the firebase-test emulator suite (426 passed) all green.

Confirmed in Chrome against live Firestore on demo/units/qa: the document is created but absent from stores.documents on both the create and fast paths; Sort Work shows it under all four filters and opens it on demand with an Edit button; a reload with it as the primary document restores it with groupDocumentsEnabled off (checked with the Problem tab active, so Sort Work could not be what re-opened it); group documents open unchanged.

Worth knowing for anyone repeating that: unit=qa loads the unit from the remote curriculum repo, which has no class-wide slot, so the feature silently isn't there. The local unit has to be named outright — unit=http://localhost:<port>/demo/units/qa/content.json.

One nuance the manual run turned up: expanding the "Whole Class" section in Sort Work opens the document, because rendering the thumbnail fetches it. Still on demand, still off the unit-load path, but a lower bar than an explicit open.

🤖 Generated with Claude Code

scytacki and others added 13 commits August 7, 2026 15:41
…-consistent group [CLUE-610]

Adjusts two existing student-create tests to name the acting student as `uid`
instead of relying on specDocumentDoc()'s default teacher owner — they were
passing only because the create rule ignored `uid`, which is the hole this
change closes.
…er [CLUE-610]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…E-610]

groupDocumentsEnabled and classWideDocuments are independent unit config,
so a unit declaring class-wide documents without enabling group documents
lost the eager re-open on reload and landed on an empty workspace.
The comment above concurrentChangeOk stated the same CLUE-612-can't-ship-
until-CLUE-604-drains fact twice in nearby passages; keep it once.
…one [CLUE-610]

documentOwnerIsCallersClass lets any class member mint any number of
class_<class_hash>-owned, concurrent:true documents; the class identity is
corroborated by the token but the authority to mint under it is not. Record
this in both registers that already carry the group-membership residual.
…LUE-610]

concurrentCreateOk tested `!= true`, which lets a truthy non-boolean like
concurrent: "true" skip the owner check; DocumentModel.concurrent is
types.maybe(types.boolean), so such a document throws when opened. `== false`
is strictly tighter at no cost, since the deployed client only ever writes
the field absent or boolean true.
…pe list [CLUE-610]

kDeployedShapes lists every document shape the deployed client creates except
group and class-wide documents, the only two shapes the new create rule can
reject; their breakage in the window between the rules deploy and the app
deploy is accepted, not an oversight.
…LUE-610]

The "create path" test asserted the opened result but not that
findFirestoreMetadata was skipped, so reverting the create path to return a
bare key instead of reusing its own write would still pass.
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.89%. Comparing base (61a310d) to head (a0a021a).

❗ There is a different number of reports uploaded between BASE (61a310d) and HEAD (a0a021a). Click for more details.

HEAD has 12 uploads less than BASE
Flag BASE (61a310d) HEAD (a0a021a)
cypress-regression 12 0
Additional details and impacted files
@@                        Coverage Diff                         @@
##           CLUE-610-class-wide-documents    #2951       +/-   ##
==================================================================
- Coverage                          85.74%   69.89%   -15.86%     
==================================================================
  Files                                978      973        -5     
  Lines                              55853    55835       -18     
  Branches                           14735    14736        +1     
==================================================================
- Hits                               47891    39025     -8866     
- Misses                              7944    16776     +8832     
- Partials                              18       34       +16     
Flag Coverage Δ
cypress-regression ?
cypress-smoke 41.42% <41.66%> (-0.25%) ⬇️
jest 56.71% <91.66%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cypress

cypress Bot commented Aug 7, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19756

Run Properties:  status check passed Passed #19756  •  git commit a0a021a806: docs: deferred open and create-time ownership rules design spec [CLUE-610]
Project collaborative-learning
Branch Review CLUE-610-deferred-open-and-create-rules
Run status status check passed Passed #19756
Run duration 03m 38s
Commit git commit a0a021a806: docs: deferred open and create-time ownership rules design spec [CLUE-610]
Committer Scott Cytacki
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 4
View all changes introduced in this branch ↗︎

…-610]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant