CLUE-610: Deferred open and create-time ownership rules - #2951
Open
scytacki wants to merge 14 commits into
Open
Conversation
…rate metadata comment [CLUE-610]
…-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.
…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 Report✅ All modified and coverable lines are covered by tests.
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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-610-deferred-open-and-create-rules
|
| Run status |
|
| Run duration | 03m 38s |
| Commit |
|
| Committer | Scott Cytacki |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
…-610] Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
getOrCreateCanonicalDocumentsplits 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, aDocumentModel, and a history subscription for every student on every load. That was measured at ~700ms.Group documents are unchanged:
getOrCreateGroupDocumentstill 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
guaranteeInitialDocumentsafter a reload with it as the primary document. That second path was gated ongroupDocumentsEnabled, which a unit can legitimately leave off while declaringclassWideDocuments; 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
isValidDocumentCreateRequestconstrained neitheruidnorconcurrent, 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; orgroup_<offeringId>_<groupId>agreeing with the document's ownofferingIdandgroupId.concurrent: trueat creation requires one of the two synthetic owners.Two residuals are deliberate, and are recorded in
docs/document-axes/README.mdand in the rules comments rather than left implicit:concurrentalso remains settable on an existingtype: "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 thefirebase-testemulator suite (426 passed) all green.Confirmed in Chrome against live Firestore on
demo/units/qa: the document is created but absent fromstores.documentson 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 withgroupDocumentsEnabledoff (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=qaloads 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