Skip to content

CLUE-610: rename the generic document type from "group" to "axes" - #2952

Open
scytacki wants to merge 13 commits into
CLUE-610-deferred-open-and-create-rulesfrom
CLUE-610-generic-type-rename
Open

CLUE-610: rename the generic document type from "group" to "axes"#2952
scytacki wants to merge 13 commits into
CLUE-610-deferred-open-and-create-rulesfrom
CLUE-610-generic-type-rename

Conversation

@scytacki

@scytacki scytacki commented Aug 8, 2026

Copy link
Copy Markdown
Member

Renames the generic document type from the stored value "group" to "axes", across the app, the Firestore rules, and the one-time sweep script.

Stacked on #2951 (CLUE-610-deferred-open-and-create-rules) — review that first. The diff shown against this base is only this work.

Why

type: "group" no longer means "this is a group document". Regular group documents and class-wide collaborative documents both store it, and they are told apart only by guards over their stored axis fields. What the value actually marks is a document whose behavior is read from its axes rather than from its type — but it is still named for the first such document. That costs us twice:

  • It blocks the next kind. Any new axis-native kind must put something in type. A fresh DocumentType enum value reintroduces the per-type branching this refactor exists to remove; storing "group" says something false about a document that is not a group's.
  • It hides what each reader means. Some of the sites comparing against GroupDocument mean "a group's document", some mean "a concurrent document", some mean "an axis-native document". The shared literal makes them look identical.

Why now, and the deadline

type is a stored value, so changing it needs an admin sweep, and a sweep needs a full client drain in front of it. CLUE-604 already schedules exactly that sweep — adding type is one more field in a write that is already happening to these documents.

This means the app change has to ship in 7.5.0. Landing it in 7.6.0 instead would make the sweep wait for 7.6.0 to drain, but 7.6.0's own content is the cleanup the sweep unblocks — so 7.6.0 would have to be split, or the sweep run twice. v7.4.0 is still the newest tag as of 2026-08-08, so the window is open; if 7.5.0 has been cut by the time this is picked up, this needs re-planning rather than merging.

The shape of it: a transitional accept-both window

Nothing rewrites stored data here. Until CLUE-604's sweep runs and old clients drain, both values exist, so every reader accepts both and GroupDocument stays exported.

Readers widen before writers flip, so no commit in the middle can write a value the app cannot read. The commits are task-sized and land in that order:

Commit What
1768c35c2 AxesDocument = "axes" and the isAxesType predicate
2fc531292 nine readers accept both values
58dbd1a6d writers create documents with "axes"; RTDB declarations widened
9ed6aad23, cb8017666 both Firestore rules clauses accept both values
6d03bba57, 0065dbfdf sweep script: one merged write per document

isAxesType is deliberately a type guard, not a booleandb.ts:727 narrows type to a literal to build a discriminated-union member, and a plain boolean return breaks that. It also means the post-sweep cleanup is one edit to one function rather than one per call site.

Two things worth a careful look

The sweep script now makes one merged write per document rather than committing two disjoint write lists. This matters more than it looks: type is the script's own work-queue key — the driving query is where("type", "==", "group"), so once a document's type flips it stops matching and can never be returned by a re-run. Committing type separately from the axis fields would make write order load-bearing across the 400-document chunk boundary, and a document whose type landed while its axis-field write failed would be permanently half-migrated with no recovery path.

The rules widening is monotone — an added || term at each of the two clauses, no operand removed — so every document that reached a decision before reaches the identical decision now, and exactly one additional type literal is granted. isValidDocumentCreateRequest needed no change: it constrains only that type is present, never its value, because the create-time rules key on owner instead.

Deploy sequencing

This fits the release plan already agreed for the CLUE-550 line of work — 7.5.0 CLUE-610, 7.6.0 CLUE-604, 7.7.0 CLUE-612:

  1. Rules deploy, ahead of the 7.5.0 releasenpm run deploy:firestore:rules. Rules first, as usual, and here required rather than merely conventional. Step 7 goes the other way round, for the reason below.
  2. 7.5.0 — this PR. The app accepts both values and writes "axes" on creation. Deadline-bound, see above. Deploy outside class hours — see the caveat below.
  3. Drain. The same drain CLUE-604's sweep already requires; no additional wait.
  4. Run the sweep — not a release. CLUE-604's step 2: dry-run then APPLY=1, against each environment in turn. An operational step gated on step 3 having actually happened, deliberately separate from any version going out. Shipping the script is not running it — this PR ships it; the run is CLUE-604's.
  5. 7.6.0 — CLUE-604's code removals, including this PR's app-side cleanup. CLUE-604 already removes two of the accept-both readers; the rest belong in the same pass — every other accept-both reader, and "group" in isAxesType, isSortableType, and DocumentTypeEnumValues. They share CLUE-604's gate exactly (safe once the sweep has run everywhere, unsafe before), so there is no reason to leave them as unscheduled cleanup. Follows step 4 rather than accompanying it.
  6. Drain again.
  7. 7.7.0 — CLUE-612, plus this PR's rules-side cleanup. CLUE-612 is a rules-only change, so here the rules deploy is the release — there is no app code to sequence ahead of. It can carry this PR's last leftover: dropping the "group" branch from the canonical-race delete. (concurrentChangeOk is deleted wholesale by CLUE-612, so its own branch needs no separate handling.)

Steps 1 and 4 are the two that are not a release going out.

Two things survive that cleanup rather than being dropped with it. The GroupDocument constant stays — it is still the group kind's registered name, and the kind axis is not renamed. And "group" stays in the RTDB type declarations, because RTDB is never swept and keeps a permanent mix of both values.

Why the rules move first here and last in 7.7.0

Both are rules changes and they sit on opposite sides of their releases. The rule is direction:

  • A widening rules change goes first. Step 1 accepts one more type value and takes nothing away, so deploying it against the currently-live app changes no decision. It must precede the app that starts writing the new value, or that app's writes hit rules that don't know the value yet.
  • A narrowing rules change goes last, after a drain. Step 7 stops accepting a write that older bundles still make. Rules apply to every client at once, so it can't ship until the app that made that write is not merely deployed over but gone from people's browsers.

For step 7 the write in question is the on-open concurrent backfill. Two independent things retire it: CLUE-604's step 3 deletes the code in 7.6.0, and the sweep (step 4) removes the unmigrated documents that would trigger it in the first place. Neither is enough alone, because a browser still running the 7.5.0 bundle contains the backfill — hence step 6's drain, and hence 7.6.0 and 7.7.0 being separate releases at all.

And why step 1's rules must precede the app rather than merely accompany it. The widening is monotone — an added || term at each clause, no operand removed — so deploying it against the currently-live app changes no decision, which is what makes rules-first safe. The reverse order is not: once the app writes "axes", two class members can race to create the same canonical document, and the loser's cleanup delete is governed by a rules clause that under un-widened rules matches only "group". That delete runs through deleteOrphanDocument, which is best-effort and swallows its own error — so the failure is silent, and the symptom is Firestore metadata quietly accumulating for documents nobody can reach.

Deploy 7.5.0 outside class hours. From the instant the writers ship, a student still on the old bundle who opens a brand-new shared document — one a classmate on the new bundle just created — fails to open it, and no drain is possible because the window opens when the release does. Only a group's or class's first document created in that window is exposed; everything already stored is untouched. Accepted rather than designed around, since deferring the writers forces the second sweep this work exists to prevent.

Deliberately not here

  • No reader is rebased onto a real axis. The design records what each site actually asks, so a later pass can rebase each onto the axis that answers it. That is ten judgment calls and this release has a deadline.
  • The RTDB type is not swept. Existing records keep "group" forever — only createdAt is read back from that tree, and the tree is slated for removal. The declarations widen so both values are representable; the data is not touched.
  • Nothing is deleted. The on-open concurrent backfill and getDocumentTitle are widened here and removed by CLUE-604, so the two stories do not both claim those sites.
  • The sweep is not run.

Testing

Full unit suite (3613) and the emulator-backed rules suite (430) both pass; check:types and lint:build clean. Each widened reader has a case for both values — the transitional window is the only time both occur, and exactly when a regression would ship. The rules tests cover both values at both clauses.

A literal-string sweep across src, shared, functions-v2, scripts, and firestore.rules found no reader left comparing against a bare "group"; the remaining hits are type declarations that legitimately admit both values, the owner axis (DocumentOwnerType), the kind registry, and unrelated uses of the word — a CSS class, drawing-object types, ARIA roles, a Sort Work section key, a sticky-note audience.

The sweep script's tests pin the query predicate (type == "group", not the value it writes) and the 400-document chunk boundary, including that the final partial batch is actually committed.

Jira

CLUE-610 and CLUE-604 have been updated: CLUE-610 gained the rename plus its release requirements, and CLUE-604's step 2 was restated around the merged write. CLUE-612 was re-read and needs nothing — it is written entirely in terms of the shared marker, never the type value.

🤖 Generated with Claude Code

scytacki and others added 12 commits August 7, 2026 16:54
…LUE-610]

Both tests reused whatever `db` the previous test left behind instead of opening
their own studentAuth client, so the caller identity under test was an accident
of declaration order and either test would throw undefined `db` if run in
isolation.
…l ones [CLUE-610]

The merged-write comments claimed a half-swept document (type:"axes" without
kind:"group") matches neither getDocumentTitle branch and renders with no
title. That branch checks isAxesType(type) && groupId, and isAxesType accepts
both "axes" and "group", so a half-swept group document still matches and
still renders correctly — the claimed hazard doesn't exist on this branch.

Replace it with the two hazards that are real: a group document missing
concurrent+kind loses its history manager and canonical-pointer slot label;
a class-wide document missing curriculum scope is invisible to Sort Work's
unit-scoped query. Also brings the function docstring in line with the
three-part (not two-pass) behavior the header already describes.
…LUE-610]

Ten no-behavior-change fixes from the final whole-branch review:

- Replace the sweep script's merged-write rationale in all four places
  (design spec, script header, script body comment, test comment) with the
  one argument that survives verification: type is the script's own
  work-queue key (where("type", "==", "group")), so committing it separately
  from the axis fields would make write order load-bearing and could
  permanently strand a half-migrated document with no way to find it again.
  Two earlier rationales in the same spots were checked against the code and
  found false.
- Name and accept the deploy-window collision in the design spec (§7) and
  the plan's deploy-sequencing section: old clients meeting a brand-new
  "axes"-typed document at the moment 7.5.0's writers ship, before any drain
  is possible. Mitigation: deploy outside class hours.
- Strengthen the sweep script's test mock so it can catch a silent
  under-migration: pin the query predicate (collectionGroup + where args)
  instead of discarding them, and give db.batch() a fresh recorder per call
  so a 401-document case can assert the 400/1 commit split.
- Reword the db.ts kind-lookup comment to state the actual constraint (no
  "axes" registry entry, so the group kind's `concurrent: true` is what's
  being borrowed) instead of a narrower and partly inaccurate one.
- Delete a redundant db.test.ts case whose assertions are a strict subset of
  the amended create-path test just above it.
- Note above both widened firestore.rules clauses that "group" and "axes"
  are both live until CLUE-604's sweep has run everywhere, so neither branch
  is dead code.
- Correct "all ten call sites" to "every call site" in the isAxesType
  comment (an eleventh site was added after that comment was written).
- Align the design spec with shipped code: DBGroupDocMetadata.type is
  "group" | "axes", not narrowed to "axes"; the isAxesType snippet now shows
  its type-guard return annotation.
- Remove the design spec's link to an untracked session-handoff note so the
  committed doc doesn't 404 once that note is gone.
- Note in the sweep script header that write volume now scales with every
  matched document, not just the ones needing a concurrent/scope backfill.

Covering tests, npm run check:types, and npm run lint:build are clean.

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

The 401-document test counted batch allocations, so deleting the tail commit
still passed it. The mock now records whether each batch was committed, and the
test asserts on that — verified by deleting the tail commit and watching it fail.

Also folds the plan-only deploy steps into the design spec, since the plan is
transient: the rules deploy is now its own sequencing step with its command.
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.22222% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 69.90%. Comparing base (a0a021a) to head (02158a3).

Files with missing lines Patch % Lines
src/components/document/document-workspace.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                             Coverage Diff                             @@
##           CLUE-610-deferred-open-and-create-rules    #2952      +/-   ##
===========================================================================
+ Coverage                                    69.89%   69.90%   +0.01%     
===========================================================================
  Files                                          973      973              
  Lines                                        55835    55849      +14     
  Branches                                     14736    14739       +3     
===========================================================================
+ Hits                                         39025    39042      +17     
+ Misses                                       16776    16773       -3     
  Partials                                        34       34              
Flag Coverage Δ
cypress-smoke 41.43% <93.33%> (+<0.01%) ⬆️
jest 56.73% <91.66%> (+0.02%) ⬆️

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 8, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19764

Run Properties:  status check passed Passed #19764  •  git commit 02158a3252: docs: sequence rules by direction, split the sweep from the release, and schedul...
Project collaborative-learning
Branch Review CLUE-610-generic-type-rename
Run status status check passed Passed #19764
Run duration 03m 37s
Commit git commit 02158a3252: docs: sequence rules by direction, split the sweep from the release, and schedul...
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 ↗︎

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