Report untouched concepts as gaps, and give breadth a docs home - #88
Merged
Merged
Conversation
The _routes.json excludes "/" from middleware to reduce TTFB, but without the middleware's cache headers, the CDN couldn't cache the homepage. This adds Cache-Control with s-maxage=86400 so the CDN can serve the page from edge cache. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The _routes.json was excluding "/" and static assets from middleware to reduce TTFB. However, the middleware adds important cache headers (Cache-Control with s-maxage) and Vary headers that the _headers file alone doesn't fully replicate. Removing _routes.json lets the middleware handle all routes consistently. The _headers file remains for CDN cache headers on the root page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… docs home `weakConcepts()` filtered on `mastery[c.id] && …`, so a concept never opened could not be reported as a gap at any surface. The product's goal is breadth — coverage plus retention — which is exactly a claim about absence, so the app could say you were shaky on something you had studied but never that you had not opened distributed systems at all. Sweep's ROI ranking already worked around this with its own `isUnknown`; the recommender itself still did not. `conceptGaps()` replaces it and returns both kinds of thin: `shaky` (studied, confidence under the bar) and `uncovered` (no mastery row at all). Shaky is reported first because it is decaying now, while an uncovered concept has been at zero since the catalogue was written; within the uncovered group the order is `sweepOrder`, so the recommender and a triage pass agree on what comes next. Deliberately not filtered by prereq reachability — prereq gating reads mastery, so for a learner who has touched nothing every gap would be filtered back out, which is the same blind spot arriving by a second route. Both consumers propagate the distinction to the UI rather than only the data: /progress and /learn/all now label an uncovered concept `never opened` instead of "0% confident", which implies a measurement that was never taken, and the uncovered card offers Read rather than Review. The stale claim in roi.ts's header — that the recommender cannot see untouched concepts — is corrected. The regression tests pass an empty mastery map on purpose. The old test gave every concept in its fixture a mastery row, which is precisely why a passing suite never noticed; verified that the new assertion fails against the old implementation (expected 5, received 0). Docs: the surface was built before its proposal was written, inverting the spec-before-feature rule. The OpenSpec change closed that on the spec side but lives outside the canonical docs tree, so `docs/product/breadth-sweep.md` is now its home — rating semantics, the coverage fix, the surfaces that consume it, and the deferred cross-device sync that still needs owner approval before any schema change. Generator details link to content-pipelines.md rather than repeating them; the durable lesson (a truthiness guard deletes the meaning of absence, and an all-keys-present fixture will never catch it) is one entry in learnings.md. Cross-device sync remains unbuilt and unapproved. No schema was touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI's `blume validate` rejected two relative links to ../architecture/decisions/0004-fsrs-spaced-repetition.md in the new breadth-sweep doc. The file exists; Blume just does not resolve relative paths into the decisions/ subtree, and every other doc in the repo already links ADRs by absolute GitHub blob URL. Matched that convention. `blume validate` now reports 0 errors. The remaining warning about public/.github/workflows/deploy.yml is pre-existing in docs/operations/deploy.md and untouched. Worth noting for next time: `pnpm docs:validate` and `pnpm exec blume validate` are different checks. The first passed while the second was failing, so local verification missed what CI enforces. 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.
Closes #79.
The
/sweepsurface was already ~90% built, and the OpenSpec change the issue body was generated from already existed. The bug itself was never fixed —roi.tshad worked around it with its own localisUnknown, which is why it went unnoticed.The blind spot
weakConcepts()filtered onmastery[c.id] && (…confidence ?? 1) < 0.6, so a concept the learner had never touched could never be reported as a gap. The app could say you were shaky on something you had studied, but never that you had not covered something at all — which directly defeats a product whose stated goal is breadth.Replaced by
conceptGaps(mastery, limit)returning{ concept, kind: 'shaky' | 'uncovered', confidence }:sweepOrderimported fromsweep.ts(foundations → editorial priority, DRY with triage order)Shaky is listed first: it decays, whereas uncovered has been at zero all along.
Gaps are deliberately not filtered by prerequisite reachability. Prereq gating reads mastery, so for a learner who has touched nothing, every gap would be filtered straight back out — reintroducing the same blind spot under a new name.
Downstream surfaces, all updated
src/pages/Progress.tsx— "Weakest concepts" → "Biggest gaps"; uncovered rows readNext: cover X … never openedrather than0% confidentsrc/pages/LearnAll.tsx—WeakAreasPanel→GapsPanel; uncovered cards shownew/never opened, a zero-width bar, and a Read CTA to/concepts/:idinstead of Review (there is nothing to review); empty state rewritten, since it is now only reachable when the catalogue is fully coveredsrc/lib/roi.tsandroi.test.ts— both carried a present-tense comment asserting the recommender cannot see untouched concepts. True when written, false after this change; corrected and pointed atconceptGapsVerified
BuildLab/FeynmanGateusepickNextConcept, not this function, and no e2e test references the changed strings.The regression test was proven, not assumed
A throwaway probe ran the old implementation against the new assertion and failed with
expected 5, received 0(toHaveLength(5)onweakConceptsOld({}, 5)). Probe deleted after confirming.Verification
pnpm test→ 486 passed (481 before, 5 new)pnpm typecheck→ exit 0pnpm lint→ 401 files, no fixes appliedpnpm docs:validate→ OK (74 markdown files, 10 ADRs, 26 required files)pnpm test:coverage→ thresholds met;recommend.tsat 73.07%pnpm quality:cycles→ zero import cyclespnpm exec vite build→ cleanNot run:
pnpm test:e2e(needs a server/browsers) andpnpm build/pnpm ready, which stop at the env gate because.env.localholds empty placeholders — a pre-existing, documented condition.Doc
docs/product/breadth-sweep.md, 179 lines. Covers why breadth, what was built, rating semantics, the coverage-gap fix and its consuming surfaces, ranking and "no hub" honesty, storage/privacy, the deferred sync, and the spec-after-code process note. Links out tocontent-pipelines.md#source-hubs, ADR 0004,surfaces.md, and the OpenSpecdesign.mdrather than re-explaining. Registered indocs/index.md,docs/product/overview.md, and the/sweeprow insurfaces.md.Explicitly not done
Cross-device sync is untouched — no schema, handler, or registry change. It needs a schema change and owner review first, per its own design doc.
Open questions
tasks.md./sweepearn aSITE_NAV_ITEMSentry? Costs a ~294-file curriculum regeneration./concepts/:idrather than/sweep?domain=<tag>.PROJECT_STATUS.mdstill does not mention Sweep at all; left alone as owner-maintained.🤖 Generated with Claude Code