Skip to content

CLUE-550 Stage 2: Class-wide slots + get-or-create via pointer - #2928

Open
scytacki wants to merge 32 commits into
CLUE-550-concurrent-kind-axesfrom
CLUE-550-class-wide-slots
Open

CLUE-550 Stage 2: Class-wide slots + get-or-create via pointer#2928
scytacki wants to merge 32 commits into
CLUE-550-concurrent-kind-axesfrom
CLUE-550-class-wide-slots

Conversation

@scytacki

@scytacki scytacki commented Jul 24, 2026

Copy link
Copy Markdown
Member

CLUE-550 Stage 2 — Class-wide slots + get-or-create via pointer

Stage 2 of the class-wide collaborative documents work. It makes the app auto-create a class-wide collaborative document — the Driving Question Board (DQB) is the default slot — exactly once per class per unit, server-race-free, reusing the CLUE-524 canonical-pointer engine. There is no UI yet (Sort Work sectioning, titling, presentation, and editing are Stage 3); this PR creates the documents but shows nothing.

Builds on the concurrent/kind axes and kind registry from Stage 1 (base branch CLUE-550-concurrent-kind-axes). The target architecture and per-axis status live in docs/document-axes/README.md (this PR flips the roadmap's creation-factory row to in progress and extends the canonical row to class+unit).

How a class-wide document is modeled

It is the same generic collaborative document as a group doc, differentiated only by orthogonal fields — no new document type:

Concern Value
Behavior (concurrent history, non-owner editable) concurrent: true (Stage-1 axis)
Identity kind (e.g. drivingQuestionBoard), registered with metadataFields: { concurrent: true }
Scope context_id (classHash) + unit; no offeringId/groupId
Pointer slot canonical: <kind> (label = kind)
Legacy type tag type: "group" (transitional, Stages 1–3)
RTDB owner class-scoped synthetic uid class_<classHash> (mirrors group docs' group_<offeringId>_<groupId>)

Because it carries concurrent: true and type: "group", Stage-1's behavior wiring (concurrent history manager, non-owner write-sync, class-wide read) already applies to it with no new branches.

What's in the PR

  1. classWideDocuments unit config — an authored array of { kind, title } slots on UnitConfiguration, read as stores.appConfig.classWideDocuments. This is the correct home for the authored DQB title.
  2. Field-based canonical pointer builder — replaces the group-specific getGroupCanonicalPointerPath with one getCanonicalPointerPath(scope, label) that derives the path from the scope fields present: canonical/v1/classes/<classHash> / (offerings/<offeringId> | units/<unit>) / [groups/<groupId>] / slots/<label>. The units/ segment is used only when there is no offering, so the group pointer path is byte-identical (no migration). A future problem/planning pointer (offering + owner) slots in as a users/<owner> segment — documented, not built.
  3. firestore.rules class+unit pointercanonicalPointerPath rewritten to build the same path field-by-field (path() + string concat), gated by hasCanonicalScope (a non-empty offering or unit field — a field written as null counts as absent scope); plus a new immutable pointer write-guard match block canonical/v1/classes/{classId}/units/{unit}/slots/{label}.
  4. Kind-registry-driven creation — a class-wide document is created with createDocument({ type: "group", kind }); its registered ownerType: "class" / scopeType: "classUnit" drive the rest. getDocumentOwner returns the synthetic class owner class_<classHash>, getDocumentScopeFields stamps context_id + unit (no offeringId/groupId), and getDocumentKindMetadataFields(kind) stamps kind + concurrent — so the document is stamped correctly with no class-wide-specific branch, and the kind (e.g. drivingQuestionBoard) is not mis-stamped as "group".
  5. getOrCreateClassWideDocument({ kind, title }) — builds the class+unit pointer path with the slot's kind as the label and delegates to the existing race-safe getOrCreateCanonicalDocument.
  6. Auto-creation on unit opencreateDeclaredClassWideDocuments() registers each declared slot's kind and fires one getOrCreateClassWideDocument per slot inside DB.connect()'s unitLoadedPromise.then block, fire-and-forget (the pointer transaction converges all class members to one doc per slot). A malformed or duplicate kind throws in registerDocumentKind and is caught and skipped, so one bad author entry never blocks the others.
  7. Verification + roadmap — emulator tests for two-client pointer convergence and the review-issue-Curriculum/Investigation/Problem/Section models #6 ordering, plus the document-axes roadmap status flip.

Notes for reviewers

  • No data migration. Class-wide documents are unreleased.
  • Security surface is small: no RTDB rules change (the class subtree is already class-member-writable); no Firestore create/update change beyond the pointer path (a class member can already create a doc with their own context_id; the create carries no canonical, which is granted only by the pointer-claim update).
  • Review issue Curriculum/Investigation/Problem/Section models #6 (drift-guard weakening) is a verified no-op — the history.length === 0 weakening lived only in the superseded original DQB PR and is in neither this branch nor master. The metadata-before-history ordering is structurally guaranteed because getOrCreateCanonicalDocument writes the Firestore metadata before the document is opened; the emulator test verifies this rather than changing drift-guard code.
  • Dormant for any unit that declares no classWideDocuments. The demo/units/qa unit now declares one drivingQuestionBoard slot so the create/pointer flow can be exercised end-to-end (see Testing); production curriculum units declare none.
  • Two things flagged for the Stage-3 plan (not issues here): the eager open of each slot doc on unit load (cost + ensuring a type:"group" doc with no offering/group doesn't leak into existing type-based enumerations), and confirming the concurrent history-write path is authorized for the class_<classHash> synthetic owner when editing is wired.

Testing

  • Unit (Jest): config resolution; the field-based pointer builder (group byte-identical + class+unit); class+unit creation stamping (scope, kind, concurrent; no offeringId/groupId/canonical); getOrCreateClassWideDocument fast/create paths; per-slot auto-creation.
  • Rules (emulator, canonical-pointers-rules): class-member create/read of the class+unit pointer; immutability; cross-class denial; canonical-claim-confirmed-by-pointer; two-client convergence; Curriculum/Investigation/Problem/Section models #6 readability; plus a class-scoped (unit: null) doc cannot claim canonical. 25/25 green.
  • Whole-repo check:types and lint:build clean.
  • Manual end-to-end: the demo/units/qa unit declares a drivingQuestionBoard class-wide slot (src/public/demo/units/qa/content.json). Loading that unit auto-creates one class-wide document per class+unit and claims its canonical pointer, so the Firestore metadata document (owner class_<classHash>, unit, kind: "drivingQuestionBoard", concurrent: true) and the classes/<classHash>/units/<unit>/slots/drivingQuestionBoard pointer can be inspected in the emulator/console.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.83562% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.18%. Comparing base (f681050) to head (51b4117).

Files with missing lines Patch % Lines
src/lib/db.ts 90.00% 8 Missing ⚠️
src/models/document/document-kinds.ts 97.95% 1 Missing ⚠️
Additional details and impacted files
@@                        Coverage Diff                        @@
##           CLUE-550-concurrent-kind-axes    #2928      +/-   ##
=================================================================
+ Coverage                          86.14%   86.18%   +0.04%     
=================================================================
  Files                                937      937              
  Lines                              53374    53453      +79     
  Branches                           14100    14122      +22     
=================================================================
+ Hits                               45981    46071      +90     
+ Misses                              7377     7366      -11     
  Partials                              16       16              
Flag Coverage Δ
cypress-regression 73.25% <82.26%> (+0.24%) ⬆️
cypress-smoke 42.37% <79.43%> (+0.37%) ⬆️
jest 55.66% <67.80%> (+0.04%) ⬆️

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 Jul 24, 2026

Copy link
Copy Markdown

collaborative-learning    Run #19564

Run Properties:  status check passed Passed #19564  •  git commit 51b4117a42: docs: note the demo/units/qa manual-verification path in the stage-2 spec [CLUE-...
Project collaborative-learning
Branch Review CLUE-550-class-wide-slots
Run status status check passed Passed #19564
Run duration 09m 46s
Commit git commit 51b4117a42: docs: note the demo/units/qa manual-verification path in the stage-2 spec [CLUE-...
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 5
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 220
View all changes introduced in this branch ↗︎

The document-axes roadmap tracks the axis architecture; the #6 first-session
drift finding belongs in the CLUE-550 design specs, which carry it. Also drop
the redundant row-flip sentence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLUE-550]

Move canonical-document pointers out of the interleaved classes/<id>/... tree
into a top-level, versioned `canonical/v1/classes/.../slots/<label>` collection.
Because pointers no longer share paths with class/offering/group data, the rules
state the uniform concerns once on a recursive-wildcard match (class-member read;
immutable) and keep only create per-scope (each scope authorizes its own slot
claim; Firestore ORs allow-rules, so a single wildcard create couldn't be
tightened per-scope). The `v1` segment versions the layout and restores even
path depth. No migration: pointers exist only for unreleased group docs, and an
orphaned old-path pointer falls back to the existing no-pointer path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scytacki and others added 12 commits July 24, 2026 08:42
Group the three canonical-pointer match blocks under a single
match /canonical/v1/classes/{classId} so classId is captured once and
shared. Inline the single-use canonicalPointerReadable helper into the
wildcard read rule and canonicalPointerCreatable. Rule semantics are
unchanged; the emulator canonical-pointers-rules suite passes 24/24.

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

Add a target-architecture section deciding how document metadata types
evolve once `type` is decomposed into axes: the kind-discriminated
DBDocumentMetadata union dissolves into a generic base plus structural
requirement types narrowed by axis-field type guards (e.g.
hasOfferingScope), never by `type` — the type-system form of the core
"read axes, not kind" rule. Notes that DBGroupDocMetadata and
DBClassWideDocMetadata already share `type: "group"` with different
shapes, so `type` already fails to discriminate shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoist `kind` out of the classWide bundle into an explicit parameter on
createDocument, getOrCreateCanonicalDocument, and
createFirestoreMetadataDocument (defaulting to `type`, since every
non-class-wide kind equals its type). createFirestoreMetadataDocument
now always stamps getDocumentKindMetadataFields(kind) instead of
reconstructing it from classWide?.kind ?? metadata.type.

Two simplifications fall out: IClassWideCreateInfo drops `kind` (keeping
only unit/syntheticUid), and createFirestoreMetadataDocument drops its
classWide object param for a bare `unit?`, keying the class+unit scope
branch on unit != null. Group documents now pass kind === type
explicitly, which is where type and kind will diverge once `type`
decomposes to a generic value.

check:types clean; db.test.ts 36/36; eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold pointerPath/type/kind into IGetOrCreateCanonicalDocumentOpts so the
method takes one options object instead of mixing positional args with an
opts bag. Move class-wide kind registration from getOrCreateClassWideDocument
into createDeclaredClassWideDocuments, so registration lives in the
declaring function rather than the get-or-create path.

check:types clean; db.test.ts 36/36; eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the owner (authoring identity, stored as `uid`) derivation into the
kind registry: kinds declare an ownerScope (user | group | class) and
getDocumentOwner(kind, ctx) in document-kinds maps it to the concrete uid
from runtime context, replacing the type switch in createDocument. This is
the first slice of the `owner` axis living in one place; the document-axes
roadmap marks the axis in progress.

Make the class-wide owner the class (class_<classHash>), shared across all
of a class's units, instead of class_<unitCode>. The unit belongs to the
document's canonical slot, not its ownership -- mirroring how a group
document's owner is group_<off>_<grp>. Reads rebuild the RTDB path from the
stored metadata uid and documents are located by canonical pointer, so the
owner value is not load-bearing and no migration is needed.

Decouple the pointer createdBy from the owner: it now records the real
user.id of whoever won the creation race (or backfilled a legacy pointer),
added provenance. Orphan cleanup reads the owner back from the created
metadata's uid. With createdBy and the owner both sourced elsewhere, the
groupUserId opt and the classWide syntheticUid field drop out, and
createDocument passes `owner` straight to the storage-path helpers
(getUserPath falls back to user.id), removing the syntheticUserId variable.

check:types clean; lint clean; document-kinds + db suites 45/45;
emulator canonical-pointers-rules 24/24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move a group-typed document's scope association fields into the kind layer:
getDocumentScopeFields(kind, ctx) in document-kinds returns { groupId } for
group scope and { unit } for class scope (keyed on the owner scope, which is
unambiguous for these two fields). This is the first slice of the `scope`
axis; other kinds' scope fields stay on the createDocument type switch.

With scope (and owner) now kind-driven, createDocument no longer needs the
classWide descriptor: for a type:"group" document it distinguishes the group
doc from a class-wide doc by the kind's registered scope. IClassWideCreateInfo
is deleted and getOrCreateCanonicalDocument carries a plain title.

getDocumentScopeFields is called inside createFirestoreMetadataDocument, which
reads the runtime values from the stores, so groupId/unit are no longer passed
as parameters. The context-validity check the inline group reject provided is
preserved as an explicit validateDocumentKindCreation(kind) in db.ts (it has
the stores access the registry lacks), which throws before anything is written.

Stamped fields are unchanged on every path (group / class-wide / personal /
problem); the refactor only relocates where they are derived. The document-axes
roadmap marks the scope axis in progress.

check:types clean; lint clean; document-kinds + db suites 48/48.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Stop returning the RTDB metadata object from createDocument. It is only
  used internally (written to RTDB, read back for the Firestore doc); no
  caller consumed the returned copy. The two publish methods that forwarded
  it now resolve { document } only (their sole consumer ignored the value),
  and the now-unused DBPublicationDocumentMetadata import is dropped.
- Rename the internal metadata local to rtdbMetadata for clarity.
- Deduplicate the common metadata fields via `const common = {...} as const`
  (as const preserves the version "1.0" literal so the discriminated-union
  members still match); `type` stays a fresh literal per branch.
- Add a default branch that throws for unhandled document types (e.g. the
  deprecated "section"). The switch is not exhaustive, and because
  rtdbMetadata is read inside a .then() closure TypeScript's used-before-
  assigned analysis can't see the gap, so guard it explicitly rather than
  silently set(undefined).
- Note the write-only, problem-family-only top-level classHash on the
  problem metadata and in metadata-fields.md.

check:types clean; lint clean; db suite 36/36.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ds explicitly [CLUE-550]

Group and class-wide documents now store only base RTDB metadata (version/self/
createdAt/type); their scope, owner, title, and kind are stamped into the Firestore
metadata by createFirestoreMetadataDocument. DBClassWideDocMetadata is removed and
DBGroupDocMetadata reduced to the bare shared type:"group" shape.

createFirestoreMetadataDocument now takes title directly and resolves every scope
field explicitly (context_id, unit/investigation/problem, offering/group) via
getDocumentScopeFields, rather than letting offeringId ride the metadata spread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ing to group docs [CLUE-550]

Separate the document-kind registry into independent ownerType and scopeType axes
(renamed from ownerScope) and register every built-in kind, so getDocumentScopeFields
resolves scope for all document types from the registry + stores context rather than
from round-tripped RTDB metadata. createFirestoreMetadataDocument now resolves every
scope field (context_id, unit/investigation/problem, offering/group) in one call.

kind/concurrent are stamped into Firestore metadata only for type:"group" docs
(group + class-wide); other kinds are registered for scope/owner resolution but their
kind is deliberately not persisted yet, to avoid migrating publication kinds later.
The on-open concurrent backfill is likewise explicitly gated to GroupDocument.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t the RTDB metadata [CLUE-550]

createFirestoreMetadataDocument no longer receives the round-tripped RTDB metadata
object. It takes a single options object { documentKey, type, kind, owner, createdAt,
title? } and reads context_id/offeringId/groupId from the user store. Every field it
writes to Firestore is now explicit rather than copied from the RTDB metadata via a
cleanedMetadata spread, making the write self-evident and paving the way for removing
the RTDB metadata entirely.

The RTDB round-trip remains only to resolve the server createdAt timestamp, which is
the sole value still sourced from it. uid is passed as owner (equivalent to the prior
type-based branch, since userContext.uid === user.id === the owner for user-scoped kinds).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ment [CLUE-550]

Class-wide documents now resolve their display title live from the kind registry
instead of stamping an authored title into each document's Firestore metadata —
mirroring how problem documents derive their title from the unit config. An author
changing a slot's title applies to every document of that kind, with no stored title
and no migration.

- document-kinds.ts: kinds may register a static `title`; getDocumentTitle(doc) resolves
  a title by kind (class-wide static title, else the group-doc label for other
  type:"group" docs, else undefined so the caller falls back). The group-doc label keys
  on `type` (always present), not `kind` (null on legacy/un-backfilled metadata records);
  the class-wide lookup keys on `kind`, which these new docs always carry.
- document-utils.ts: getDocumentDisplayTitle consults getDocumentTitle first; its
  type===GroupDocument branch is removed.
- db.ts: class-wide slots register their title on the kind; the authored title no longer
  threads through the canonical creation path (createDocument/createFirestoreMetadataDocument
  keep title for personal/learning-log docs, which do persist and display a stored title).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure [CLUE-550]

Add the Stage-2 design spec describing what the class-wide-slots PR delivers,
including the post-implementation refactors (kind-driven owner/scope derivation,
createFirestoreMetadataDocument options object, collapsed RTDB group metadata,
titles resolved by kind).

Update the document-axes roadmap and target-architecture to match: owner/scope
axes described via ownerType/scopeType, and scope's read side reframed as
field/axis guards with a unified scope getter left as an open question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scytacki and others added 2 commits July 26, 2026 22:59
…-550]

Rename the classWideDocuments config-entry variables and params (slot/slots ->
classWideDoc/classWideDocs) so .kind/.title read naturally and do not overload
the precise `kind` term; the canonical pointer-slot terminology is preserved.
Also tighten several surrounding comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert multi-line JSDoc to block style (delimiters on their own lines),
shorten/reword several comments, and drop a stale label note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scytacki and others added 2 commits July 26, 2026 23:46
…-slots

Brings in the Stage-1 comment/tidy refinements. Resolved the document-kinds.ts
conflicts by taking the parent's concise base-comment wording while keeping the
Stage-2 additions (owner/scope/title functions and the registerDocumentKind(kind,
info) signature). The db.ts backfill auto-merge combined the parent's concurrent
derivation with our type === GroupDocument write-back guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Stage 2 support for class-wide collaborative “slot” documents (e.g., DQB) by extending the kind/axis registry, generalizing canonical pointer paths to be scope-field-driven, and auto-creating declared class-wide documents on unit load. This fits into the document-axes refactor by moving more creation-time behavior (owner/scope derivation) behind the kind registry while reusing the existing canonical-pointer get-or-create transaction for race-free convergence.

Changes:

  • Introduces classWideDocuments in unit configuration and exposes it via ConfigurationManager/AppConfigModel.
  • Generalizes canonical pointer paths into a versioned canonical/v1/.../slots/<label> layout and updates Firestore rules + emulator tests accordingly.
  • Adds class-wide doc creation (class_<classHash> owner, class+unit scope) and fire-and-forget auto-creation on unit open, plus Jest coverage for metadata stamping and pointer convergence logic.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/models/stores/unit-configuration.ts Adds classWideDocuments authored unit config surface.
src/models/stores/configuration-manager.ts Exposes classWideDocuments via config manager getter.
src/models/stores/app-config-model.ts Surfaces classWideDocuments on stores.appConfig.
src/models/stores/app-config-model.test.ts Tests config resolution for classWideDocuments.
src/models/document/document-utils.ts Uses kind registry to resolve display titles first (incl. class-wide slot titles).
src/models/document/document-utils.test.ts Adds tests for group vs class-wide title resolution behavior.
src/models/document/document-kinds.ts Expands kind registry to include owner/scope derivation + optional static titles.
src/models/document/document-kinds.test.ts Adds coverage for owner selection, scope field selection, and title resolution.
src/lib/scoped-document-pointers.ts Replaces group-only pointer builder with scope-driven canonical/v1/.../slots/... builder.
src/lib/scoped-document-pointers.test.ts Tests pointer paths for group scope vs class+unit scope.
src/lib/db.ts Implements kind-driven owner/scope stamping, class-wide get-or-create, and unit-load auto-creation.
src/lib/db.test.ts Adds unit tests for Firestore metadata stamping and class-wide pointer get-or-create flows.
src/lib/db-types.ts Collapses group/class-wide RTDB metadata shape under transitional type:"group".
firestore.rules Moves pointers under canonical/v1, adds class+unit create scope, updates canonical path helper.
firebase-test/src/canonical-pointers-rules.test.ts Adds emulator tests for class+unit pointer rules + canonical-claim integrity.
docs/superpowers/specs/2026-07-23-clue-550-stage-2-class-wide-slots-design.md New design spec documenting Stage 2 scope, pointer layout, and creation strategy.
docs/document-metadata/metadata-fields.md Updates metadata-field documentation around RTDB classHash handling.
docs/document-axes/target-architecture.md Refines axis/scope guidance toward field guards over kind/type branching.
docs/document-axes/README.md Updates roadmap/status notes for Stage 2 progress.
Comments suppressed due to low confidence (1)

src/lib/db.ts:759

  • getOrCreateGroupDocument() builds a pointer path using user.offeringId but doesn’t validate it. If offeringId is empty/undefined, getCanonicalPointerPath() will produce a path that doesn’t match the allowed pointer create scopes (and the group owner uid can be malformed), causing hard-to-diagnose failures. Fail fast when offeringId is missing.
    const groupId = user.currentGroupId;
    if (!groupId) {
      return Promise.reject("Cannot create group document because user is not in a group.");
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/scoped-document-pointers.ts
Comment thread firestore.rules
Comment thread src/lib/db.ts
Comment thread src/lib/db.ts
…e kinds [CLUE-550]

registerDocumentKind is now the single home for kind checking: a kind must be a
camelCase identifier (safe as a Firestore path segment / registry key) and must
be registered exactly once; both violations throw. isValidDocumentKind moved from
document-types.ts to document-kinds.ts alongside it.

createDeclaredClassWideDocuments catches those throws and skips the offending
class-wide entry rather than crashing startup. Built-in kinds register via
registerBuiltInDocumentKinds(); resetDocumentKindRegistryForTests() lets tests
work with the now-throwing registry.

Rename the example class-wide kinds driving-question-board -> drivingQuestionBoard
and word-wall -> wordWall, and the hyphenated test kinds to camelCase.

Document the kind format and the classWideDocuments option in unit-configuration.md,
and add a CLAUDE.md reminder to keep that doc in sync with authorable config changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scytacki and others added 2 commits July 27, 2026 07:11
…UE-550]

The icon field was authored but inert — nothing read it. Remove it from the
config type and the authoring doc; icon presentation (and its config shape) is a
Stage-3 decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflect the changes made during review: registry-derived owner/scope
(ownerType/scopeType), the createFirestoreMetadataDocument options object,
collapsed RTDB metadata (DBClassWideDocMetadata removed), titles resolved by
kind, kind validation + uniqueness in registerDocumentKind, the classWideDoc
rename, and dropping the unused icon field (classWideDocuments is { kind, title }).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p owners [CLUE-550]

Firestore rules `hasCanonicalScope` used a bare `!= ""` test, which reads a
field written as null (class-scoped docs store `unit: null`) as present, since
`null != ""` is true in rules. Add a `hasScopeField` helper requiring a
non-empty string and use it for both the scope check and the pointer-path
branch selection, so a null-valued field counts as absent scope.

Group owner ids are `group_<offeringId>_<groupId>`, so require both offeringId
and currentGroupId when creating group-owned documents (was currentGroupId
only), in validateDocumentKindCreation and the getOrCreateGroupDocument guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s/qa unit [CLUE-550]

Lets the class-wide create/pointer flow be exercised end-to-end against a
loadable demo unit: opening qa auto-creates one class-wide document per class
per unit and claims its canonical pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…spec [CLUE-550]

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants