From 96227251d83fbc054bca33b95f145d41027409bd Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Tue, 16 Jun 2026 20:29:38 -0700
Subject: [PATCH 1/8] docs(spec): The Mastery Loop (Pillar 3, Spec 1) design
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
learn → prove (self-graded flip-card check from the deep-dive's existing
Feynman questions) → see it grow (library mastery coverage). Shared pure
mastery.js model + IDB; zero new AI calls, no background.js changes.
Research-backed by the "AI-Augmented Textbook"/Learn Your Way paper.
Includes a rendered HTML version for review.
---
.../specs/2026-06-16-mastery-loop-design.html | 143 ++++++++++++++++++
.../specs/2026-06-16-mastery-loop-design.md | 110 ++++++++++++++
2 files changed, 253 insertions(+)
create mode 100644 docs/superpowers/specs/2026-06-16-mastery-loop-design.html
create mode 100644 docs/superpowers/specs/2026-06-16-mastery-loop-design.md
diff --git a/docs/superpowers/specs/2026-06-16-mastery-loop-design.html b/docs/superpowers/specs/2026-06-16-mastery-loop-design.html
new file mode 100644
index 0000000..0dfd894
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-16-mastery-loop-design.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+ The Mastery Loop — Pillar 3, Spec 1
+
+
+
+
+
+
+
The Mastery Loop
+
+ Pillar 3 · Spec 1 · 2026-06-16 · Status: Approved (design) — pending plan
+ Surface: the deep-dive panel of the output tab + the library.
+
+
+ learn (run a deep dive) → prove it (self-graded flip-card check) → see it grow (mastery across your library)
+
+
+
+
+ Research backbone. Informed by "Towards an AI-Augmented Textbook" / Google "Learn Your Way" (arxiv 2509.13348v4): personalize + multi-represent + embedded assessment, RCT-validated for better immediate and 3-day retention. RepoLens already has latent assessment — the deep-dive Feynman stage emits self-test questions. This spec turns that into an interactive mastery loop, adapted to a zero-backend, BYO-key extension: self-graded, no new AI calls.
+
+
+
Goals
+
+
A shared, persisted per-repo mastery signal the rest of Pillar 3 builds on.
+
An interactive, self-graded understand-check from the deep-dive's existing questions — no new AI calls.
+
A library mastery view showing coverage grow (the collection/progression payoff).
+
Tasteful: Mono Ink, no badge-soup, no confetti; fully local.
+
+
+
Non-goals
+
+
No AI-graded MCQ / distractor generation (self-graded flip cards only in v1).
+
No background.js / message changes; no new AI calls; no backend.
+
No corkboard knowledge-graph integration (Spec 2). No spaced-repetition UI yet (model leaves room via lastCheckedAt).
+
+
+
Components
+
+
+ ① The signal — mastery.js (new pure module) + IDB
+
Levels: new scanned, no check · explored attempted, not passed · understood passed.
+
Pure exports (no DOM/network): MASTERY_LEVELS + levelLabel(); deriveCheckResult(questions, ratings) → { level, score, glows, grows } (score = gotIt/total; understood when score ≥ ⅔ (the UNDERSTOOD_THRESHOLD constant, ≈0.667 — e.g. 2 of 3 passes; compare against the 2/3 constant, not a rounded 0.67), tunable; glows = the questions you got, grows = the ones to revisit — pure reflection, no AI); aggregateMastery(records) → coverage counts.
+
+
+
+ ② Earn — the Understand-Check (Deep Dive panel)
+
The deep-dive already prints self-test questions. Make them interactive flip cards:
+
┌─ Check your understanding ──────────────┐
+│ Q: What runs a Hono request? │
+│ [ Reveal answer ] │
+│ …answer… │
+│ [ Got it ] [ Shaky ] [ Missed ] │
+└──────────────────────────────────────────┘
+ → tally → "Solid on routing · revisit middleware"
+ → marks this repo understood, saved to your library
+
After all cards: deriveCheckResult sets the level + a Glows & Grows summary from which questions you rated low, then persists to mastery[repoId]. Re-takeable; latest result wins. UI is DOM glue; all logic lives in mastery.js.
+
+
+
+ ③ See — the Mastery Map (library)
+
+
Each card gets a subtle level indicator (small ring/dot — not a loud badge): new faint, explored half, understood full/accent.
+
One honest aggregate line: "Understood 12 of 40 · 7 explored."
+
A level filter (All / Understood / Explored / New), reusing the existing library filter pattern.
+
+
Rendered in library.js from the mastery records; stats from aggregateMastery.
+
+
+
Architecture (files)
+
+
Createmastery.js — pure model + helpers (scoring, leveling, aggregation).
+
Createtests/mastery.test.js.
+
Modifystore.js — getMastery/setMastery (IDB, existing store patterns) + a store test.
+
Modifyoutput-tab.js (+ HTML styles) — flip-card check in the deep-dive panel.
DOM glue:node --check, npm run check:html, manual smoke.
+
Existing suite green; eslint . 0 errors; HTML gate passes.
+
+
+
Constraints
+
Zero-build, zero-dep, vanilla ES modules. No new AI calls, no background.js changes, no backend — mastery is local IDB; BYO-key untouched. Mono Ink; no emoji; card-flip motion behind prefers-reduced-motion; tasteful (no confetti / badge-soup).
+
+
Acceptance criteria
+
+
After a deep dive, self-test questions render as an interactive self-graded check (reveal + Got it / Shaky / Missed).
+
Completing it persists mastery[repoId] + shows Glows & Grows + the earned level.
+
understood requires score ≥ ⅔ (the UNDERSTOOD_THRESHOLD constant, ≈0.667; e.g. 2 of 3); else explored.
+
Library cards show their level; library shows an honest aggregate + a level filter.
+
mastery.js fully unit-tested; persistence tested with fake-indexeddb.
+
All existing tests pass + new; eslint . 0 errors; HTML gate passes; no AI calls / background.js changes added.
+
+
+
+ Resolved decisions: earn = self-graded flip cards (no AI MCQ in v1) · 3 levels, understood = ≥⅔ "got it" (tunable) · 3-way rating (Got it / Shaky / Missed), "Shaky" feeds grows · mastery map v1 = indicators + aggregate + filter; corkboard/graph deferred to Spec 2.
+
+
+
+
+
+
+
diff --git a/docs/superpowers/specs/2026-06-16-mastery-loop-design.md b/docs/superpowers/specs/2026-06-16-mastery-loop-design.md
new file mode 100644
index 0000000..2a0c326
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-16-mastery-loop-design.md
@@ -0,0 +1,110 @@
+# The Mastery Loop (Pillar 3, Spec 1)
+
+- **Date:** 2026-06-16
+- **Status:** Approved (design) — pending implementation plan
+- **Surface:** the deep-dive panel of the output tab + the library
+- **Part of:** **Pillar 3 — The Knowledge Game** (library/corkboard/canvas made smooth, juicy, addictive). Pillar A (output-tab Four-Act narrative) shipped (PR #36).
+ - **Spec 1 — The Mastery Loop** ← *this* (mastery/progression + collection)
+ - **Spec 2 — The Knowledge Graph** (anchor-to-library + corkboard concept graph; "plug pieces, think in new ways")
+ - Juice (crafting/flow motion) + smalls (re-leveling, spaced repetition, mnemonics) woven in
+
+## Research backbone
+
+Informed by *"Towards an AI-Augmented Textbook"* / Google "Learn Your Way" (https://arxiv.org/html/2509.13348v4): **personalize + multi-represent + embedded assessment**, validated by RCT (better immediate *and* 3-day retention). RepoLens already does multi-representation (lenses) and has latent assessment — the deep-dive Feynman stage already emits self-test questions. This spec turns that latent assessment into an interactive **mastery loop**, the paper's "embedded questions + section quiz with Glows & Grows feedback," adapted to a zero-backend, BYO-key extension (self-graded, no new AI calls).
+
+## The loop
+
+**learn** (run a deep dive) → **prove it** (self-graded flip-card check) → **see it grow** (mastery coverage across your library). This is "evaluations compound" made tangible.
+
+## Goals
+
+- A shared, persisted per-repo **mastery signal** the rest of Pillar 3 builds on.
+- An interactive, **self-graded** understand-check built from the deep-dive's existing `{q,a}` questions — no new AI calls.
+- A library **mastery view** that shows knowledge coverage growing (the collection/progression payoff).
+- Tasteful (Mono Ink, no badge-soup, no confetti); fully local.
+
+## Non-goals
+
+- No AI-graded MCQ / distractor generation (self-graded flip cards only in v1).
+- No `background.js` / message-contract changes; no new AI calls; no backend.
+- No corkboard knowledge-graph integration (that's Spec 2).
+- No spaced-repetition scheduling UI yet (the data model leaves room — `lastCheckedAt` — but the resurfacing UX is a later small).
+
+## Components
+
+### ① The signal — `mastery.js` (new pure module) + IDB persistence
+
+Per-repo record, persisted in IDB via `store.js`:
+
+```
+mastery[repoId] = {
+ level: 'new' | 'explored' | 'understood',
+ lastCheckedAt: ISO string | null,
+ lastResult: { gotIt: number, shaky: number, missed: number, total: number } | null,
+}
+```
+
+Levels:
+- **new** — scanned, no check taken.
+- **explored** — deep dive run / check attempted but not passed.
+- **understood** — passed the check.
+
+`mastery.js` is pure (no DOM, no network) and exports:
+- `MASTERY_LEVELS` (ordered: new < explored < understood) + `levelLabel(level)`.
+- `deriveCheckResult(questions, ratings)` → `{ level, score, glows, grows }`.
+ - `ratings` is an array aligned to `questions`, each `'gotIt' | 'shaky' | 'missed'`.
+ - `score` = gotIt / total.
+ - `level` = `'understood'` when `score >= UNDERSTOOD_THRESHOLD` where `UNDERSTOOD_THRESHOLD = 2/3` (≈0.667 — e.g. 2 of 3 questions passes; tunable constant). Compare against the `2/3` constant, not a rounded `0.67` (2/3 = 0.6667 < 0.67, so a rounded literal would wrongly require 3-of-3). Else `'explored'`.
+ - `glows` = the `q` text of the gotIt questions (what you're solid on); `grows` = the `q` text of shaky/missed questions (what to revisit). No AI — pure reflection of which questions were self-rated low.
+- `aggregateMastery(records)` → `{ understood, explored, new, total }` for the library view.
+
+### ② Earn — the Understand-Check (deep-dive panel, "Go Deeper")
+
+The deep-dive output already renders a "self-test questions" section from `deepdive.parseFeynman().questions`. Turn it interactive:
+- Each question is a flip card: shows `q`; **Reveal answer** shows `a`; then three self-rating buttons: **Got it · Shaky · Missed**.
+- After all cards rated, call `deriveCheckResult(questions, ratings)`, persist to `mastery[repoId]` (via `store.js`), and show a **Glows & Grows** summary ("Solid on: …; revisit: …") + the new level.
+- Re-taking is allowed; the latest result replaces the prior one.
+- The flip-card UI is DOM glue in `output-tab.js`/HTML/CSS; all scoring/leveling/summary logic lives in `mastery.js` (testable).
+
+### ③ See — the Mastery Map (library)
+
+- Each library card gains a subtle level indicator (small ring/dot — *not* a loud badge): new (faint), explored (half), understood (full/accent).
+- One honest aggregate line at the top of the library: e.g. "Understood 12 of 40 · 7 explored."
+- A level filter (All / Understood / Explored / New) reusing the existing library filter pattern.
+- Rendered in `library.js` from the mastery records; coverage stats from `aggregateMastery`.
+
+## Architecture (files)
+
+- **Create** `mastery.js` — pure model + helpers (one responsibility: the mastery signal + scoring + aggregation).
+- **Create** `tests/mastery.test.js` — `deriveCheckResult` thresholds/levels/glows-grows, `aggregateMastery`, level helpers.
+- **Modify** `store.js` — add `getMastery(repoId)` / `setMastery(repoId, record)` (IDB; follow the existing scene/library store patterns). Add a store test in `tests/`.
+- **Modify** `output-tab.js` (+ `output-tab.html` styles) — render the flip-card check in the deep-dive panel; wire ratings → `deriveCheckResult` → `store.setMastery` → summary + level.
+- **Modify** `library.js` (+ styles) — card level indicators, the aggregate line, the level filter.
+
+## Testing
+
+- **vitest (pure):** `mastery.js` — `deriveCheckResult` (understood vs explored at the threshold boundary; glows/grows partition by rating), `aggregateMastery` counts, level ordering/labels.
+- **vitest + fake-indexeddb:** `store.js` mastery get/set round-trip (matches existing `store-*.test.js` pattern).
+- **DOM glue:** `node --check` on changed JS; `npm run check:html`; manual smoke (run a deep dive → take the check → see the level on the library card + aggregate update).
+- Existing suite stays green; `eslint .` 0 errors; HTML parse gate passes.
+
+## Constraints
+
+- Zero-build, zero-dep, vanilla ES modules. No new AI calls, no `background.js` changes, no backend — mastery is local IDB only; BYO-key untouched.
+- Mono Ink palette; no emoji on product surfaces; the card-flip/reveal motion gated behind `prefers-reduced-motion` using `--dur-*`/`--ease-*`. Tasteful — no confetti, no badge-soup.
+
+## Acceptance criteria
+
+- [ ] After a deep dive, the self-test questions render as an interactive self-graded check (reveal + Got it / Shaky / Missed).
+- [ ] Completing the check persists `mastery[repoId]` and shows a Glows & Grows summary + the earned level.
+- [ ] `understood` requires `score >= 2/3` (the `UNDERSTOOD_THRESHOLD` constant, ≈0.667; e.g. 2 of 3 questions); otherwise `explored`.
+- [ ] Library cards show their mastery level; the library shows an honest aggregate ("Understood X of Y") and a level filter.
+- [ ] `mastery.js` logic is fully unit-tested; mastery persistence is tested with fake-indexeddb.
+- [ ] All existing tests pass + new tests; `eslint .` 0 errors; HTML gate passes; no AI calls or `background.js` changes added.
+
+## Resolved decisions
+
+- Earn mechanism = **self-graded flip cards** (no AI-graded MCQ in v1).
+- 3 levels (new/explored/understood); understood threshold = ≥⅔ "got it" (tunable constant).
+- 3-way self-rating (Got it / Shaky / Missed); "Shaky" doesn't count toward mastered, feeds "grows."
+- Mastery map v1 = card indicators + aggregate line + level filter; corkboard/graph integration deferred to Spec 2.
From 3eeae6a1bd33154a2d84d0ee0c773c5846ec7a1b Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Tue, 16 Jun 2026 20:34:38 -0700
Subject: [PATCH 2/8] =?UTF-8?q?docs(spec):=20mastery=20loop=20=E2=80=94=20?=
=?UTF-8?q?fold=20in=20review=20(counts,=20CRUD=20store,=20edge=20cases,?=
=?UTF-8?q?=20no-%=20UI)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../specs/2026-06-16-mastery-loop-design.html | 20 +++++-----
.../specs/2026-06-16-mastery-loop-design.md | 37 ++++++++++++-------
2 files changed, 35 insertions(+), 22 deletions(-)
diff --git a/docs/superpowers/specs/2026-06-16-mastery-loop-design.html b/docs/superpowers/specs/2026-06-16-mastery-loop-design.html
index 0dfd894..eb05be6 100644
--- a/docs/superpowers/specs/2026-06-16-mastery-loop-design.html
+++ b/docs/superpowers/specs/2026-06-16-mastery-loop-design.html
@@ -75,7 +75,8 @@
Components
lastResult: { gotIt, shaky, missed, total } | null,
}
Levels: new scanned, no check · explored attempted, not passed · understood passed.
-
Pure exports (no DOM/network): MASTERY_LEVELS + levelLabel(); deriveCheckResult(questions, ratings) → { level, score, glows, grows } (score = gotIt/total; understood when score ≥ ⅔ (the UNDERSTOOD_THRESHOLD constant, ≈0.667 — e.g. 2 of 3 passes; compare against the 2/3 constant, not a rounded 0.67), tunable; glows = the questions you got, grows = the ones to revisit — pure reflection, no AI); aggregateMastery(records) → coverage counts.
+
Constants: UNDERSTOOD_THRESHOLD = 2/3, MASTERY_LEVELS. Pure exports (no DOM/network): levelLabel(); deriveCheckResult(questions, ratings) → { level, score, gotIt, shaky, missed, total, glows, grows } (counts returned so the caller persists lastResult directly; score = gotIt/total; understood when score ≥ ⅔ — compare against the 2/3 constant, not a rounded 0.67, so 2-of-3 passes; glows = questions you got, grows = ones to revisit, pure reflection, no AI); aggregateMastery(records) → coverage counts (coverage left derived in UI).
+
Edge cases:zero questions → level: 'new', caller writes nothing (no accidental promotion); partial completion (closed before all rated) → persist nothing; retakes → latest completed result wins (no history in v1).
@@ -89,15 +90,15 @@
Components
└──────────────────────────────────────────┘
→ tally → "Solid on routing · revisit middleware"
→ marks this repo understood, saved to your library
-
After all cards: deriveCheckResult sets the level + a Glows & Grows summary from which questions you rated low, then persists to mastery[repoId]. Re-takeable; latest result wins. UI is DOM glue; all logic lives in mastery.js.
+
One card at a time (reveal → rate → auto-advance). The completion screen leads with the level earned + Glows & Grows, not a raw percentage (score is stored, not shown as the headline). Persist only on completion — write mastery[repoId] only when every question is rated; closing early writes nothing. Re-takeable; latest result wins. UI is DOM glue; all logic lives in mastery.js.
③ See — the Mastery Map (library)
-
Each card gets a subtle level indicator (small ring/dot — not a loud badge): new faint, explored half, understood full/accent.
-
One honest aggregate line: "Understood 12 of 40 · 7 explored."
-
A level filter (All / Understood / Explored / New), reusing the existing library filter pattern.
+
A subtle level indicator left of the title (not a loud badge): new = ○ faint, explored = ◐ half, understood = ● accent. Text label on hover/focus only.
+
One honest aggregate line: "Understood 12 of 40 · 7 explored." Never a "Mastery 82%" — implies precision the signal lacks.
+
A single-select level filter (All / Understood / Explored / New), reusing the existing library filter pattern (no multi-filter in v1).
Rendered in library.js from the mastery records; stats from aggregateMastery.
@@ -106,7 +107,7 @@
Architecture (files)
Createmastery.js — pure model + helpers (scoring, leveling, aggregation).
Createtests/mastery.test.js.
-
Modifystore.js — getMastery/setMastery (IDB, existing store patterns) + a store test.
+
Modifystore.js — getAllMastery() (read full map) + setMastery(repoId, record) (persist a computed record). CRUD only — no leveling/merge logic in the store (no updateMastery that computes); + a store test.
Modifyoutput-tab.js (+ HTML styles) — flip-card check in the deep-dive panel.
After a deep dive, self-test questions render as an interactive self-graded check (reveal + Got it / Shaky / Missed).
-
Completing it persists mastery[repoId] + shows Glows & Grows + the earned level.
-
understood requires score ≥ ⅔ (the UNDERSTOOD_THRESHOLD constant, ≈0.667; e.g. 2 of 3); else explored.
-
Library cards show their level; library shows an honest aggregate + a level filter.
+
Completing it (all questions rated) persists mastery[repoId] + shows Glows & Grows + the earned level.
+
understood requires score ≥ ⅔ (the UNDERSTOOD_THRESHOLD constant, ≈0.667; 2 of 3 passes); else explored.
+
Partial completion writes nothing; a zero-question deep dive can't earn mastery (stays new, no write).
+
Library cards show their level (○ / ◐ / ●); library shows an honest aggregate (never a %) + a single-select filter.
mastery.js fully unit-tested; persistence tested with fake-indexeddb.
All existing tests pass + new; eslint . 0 errors; HTML gate passes; no AI calls / background.js changes added.
diff --git a/docs/superpowers/specs/2026-06-16-mastery-loop-design.md b/docs/superpowers/specs/2026-06-16-mastery-loop-design.md
index 2a0c326..16426c6 100644
--- a/docs/superpowers/specs/2026-06-16-mastery-loop-design.md
+++ b/docs/superpowers/specs/2026-06-16-mastery-loop-design.md
@@ -51,33 +51,42 @@ Levels:
`mastery.js` is pure (no DOM, no network) and exports:
- `MASTERY_LEVELS` (ordered: new < explored < understood) + `levelLabel(level)`.
-- `deriveCheckResult(questions, ratings)` → `{ level, score, glows, grows }`.
+- Constants: `UNDERSTOOD_THRESHOLD = 2 / 3` and `MASTERY_LEVELS = { NEW: 'new', EXPLORED: 'explored', UNDERSTOOD: 'understood' }`.
+- `deriveCheckResult(questions, ratings)` → `{ level, score, gotIt, shaky, missed, total, glows, grows }` (the counts are returned so the caller can persist `lastResult` directly).
- `ratings` is an array aligned to `questions`, each `'gotIt' | 'shaky' | 'missed'`.
- `score` = gotIt / total.
- - `level` = `'understood'` when `score >= UNDERSTOOD_THRESHOLD` where `UNDERSTOOD_THRESHOLD = 2/3` (≈0.667 — e.g. 2 of 3 questions passes; tunable constant). Compare against the `2/3` constant, not a rounded `0.67` (2/3 = 0.6667 < 0.67, so a rounded literal would wrongly require 3-of-3). Else `'explored'`.
+ - `level` = `'understood'` when `score >= UNDERSTOOD_THRESHOLD`. Compare against the `2/3` constant, **not a rounded `0.67`** (2/3 = 0.6667 < 0.67, so a rounded literal would wrongly require 3-of-3; 2-of-3 must pass). Else `'explored'`.
- `glows` = the `q` text of the gotIt questions (what you're solid on); `grows` = the `q` text of shaky/missed questions (what to revisit). No AI — pure reflection of which questions were self-rated low.
-- `aggregateMastery(records)` → `{ understood, explored, new, total }` for the library view.
+ - **Zero questions** (`questions` empty) → `{ level: 'new', score: 0, total: 0, gotIt: 0, shaky: 0, missed: 0, glows: [], grows: [] }`. The caller writes nothing (see Edge cases) — no accidental promotion.
+- `aggregateMastery(records)` → `{ understood, explored, new, total }` for the library view. (`coverage` is left derived in the UI, not stored.)
+
+### Edge cases (explicit)
+
+- **Zero-question check** → `deriveCheckResult` returns `level: 'new'` and the caller persists nothing. A deep dive that emitted no questions cannot earn mastery.
+- **Partial completion** → if the user closes the panel before every question is rated (`ratings.length < questions.length`), **persist nothing**. Mastery is written only on a fully-rated check.
+- **Retakes** → allowed; the latest completed result replaces the prior `mastery[repoId]` (latest wins). No history kept in v1.
### ② Earn — the Understand-Check (deep-dive panel, "Go Deeper")
The deep-dive output already renders a "self-test questions" section from `deepdive.parseFeynman().questions`. Turn it interactive:
-- Each question is a flip card: shows `q`; **Reveal answer** shows `a`; then three self-rating buttons: **Got it · Shaky · Missed**.
-- After all cards rated, call `deriveCheckResult(questions, ratings)`, persist to `mastery[repoId]` (via `store.js`), and show a **Glows & Grows** summary ("Solid on: …; revisit: …") + the new level.
-- Re-taking is allowed; the latest result replaces the prior one.
+- **One card at a time:** show `q`; **Reveal answer** shows `a`; then three self-rating buttons (**Got it · Shaky · Missed**); rating auto-advances to the next card. Low friction.
+- **Completion screen** (after all cards rated): lead with the **level earned** + **Glows & Grows** ("Solid on: …; revisit: …"), *not* a raw percentage — the score is stored but not the headline (a 66.7% feels like a grade; "Understood — revisit middleware" feels educational).
+- **Persist only on completion:** call `deriveCheckResult(questions, ratings)` and write `mastery[repoId]` (via `store.js`) **only when every question is rated**. Closing early writes nothing.
+- Re-taking is allowed; the latest completed result replaces the prior one.
- The flip-card UI is DOM glue in `output-tab.js`/HTML/CSS; all scoring/leveling/summary logic lives in `mastery.js` (testable).
### ③ See — the Mastery Map (library)
-- Each library card gains a subtle level indicator (small ring/dot — *not* a loud badge): new (faint), explored (half), understood (full/accent).
-- One honest aggregate line at the top of the library: e.g. "Understood 12 of 40 · 7 explored."
-- A level filter (All / Understood / Explored / New) reusing the existing library filter pattern.
+- Each library card gains a subtle level indicator to the left of the title — *not* a loud badge: **new = ○** (faint outline), **explored = ◐** (half), **understood = ●** (accent fill). The text label appears only on hover/focus.
+- One honest aggregate line at the top of the library: e.g. "Understood 12 of 40 · 7 explored." Never a "Mastery 82%" — that implies precision the self-graded signal doesn't have.
+- A **single-select** level filter (All / Understood / Explored / New) reusing the existing library filter pattern (no multi-filter in v1).
- Rendered in `library.js` from the mastery records; coverage stats from `aggregateMastery`.
## Architecture (files)
- **Create** `mastery.js` — pure model + helpers (one responsibility: the mastery signal + scoring + aggregation).
- **Create** `tests/mastery.test.js` — `deriveCheckResult` thresholds/levels/glows-grows, `aggregateMastery`, level helpers.
-- **Modify** `store.js` — add `getMastery(repoId)` / `setMastery(repoId, record)` (IDB; follow the existing scene/library store patterns). Add a store test in `tests/`.
+- **Modify** `store.js` — add `getAllMastery()` (read the full `{ [repoId]: record }` map for the library) and `setMastery(repoId, record)` (persist one already-computed record). **CRUD only** — no leveling/merge logic in the store; the level is computed in `mastery.js` and passed in. Do *not* add an `updateMastery` that computes inside the persistence layer. Follow the existing scene/library store patterns; add a store test in `tests/`.
- **Modify** `output-tab.js` (+ `output-tab.html` styles) — render the flip-card check in the deep-dive panel; wire ratings → `deriveCheckResult` → `store.setMastery` → summary + level.
- **Modify** `library.js` (+ styles) — card level indicators, the aggregate line, the level filter.
@@ -97,9 +106,11 @@ The deep-dive output already renders a "self-test questions" section from `deepd
- [ ] After a deep dive, the self-test questions render as an interactive self-graded check (reveal + Got it / Shaky / Missed).
- [ ] Completing the check persists `mastery[repoId]` and shows a Glows & Grows summary + the earned level.
-- [ ] `understood` requires `score >= 2/3` (the `UNDERSTOOD_THRESHOLD` constant, ≈0.667; e.g. 2 of 3 questions); otherwise `explored`.
-- [ ] Library cards show their mastery level; the library shows an honest aggregate ("Understood X of Y") and a level filter.
-- [ ] `mastery.js` logic is fully unit-tested; mastery persistence is tested with fake-indexeddb.
+- [ ] `understood` requires `score >= 2/3` (the `UNDERSTOOD_THRESHOLD` constant, ≈0.667; e.g. 2 of 3 questions); otherwise `explored`. A 2-of-3 check earns `understood`.
+- [ ] **Partial completion** (panel closed before all questions rated) persists nothing.
+- [ ] A deep dive with **zero questions** cannot earn mastery — it stays `new` and writes nothing.
+- [ ] Library cards show their mastery level (subtle ○ / ◐ / ● indicator, label on hover/focus); the library shows an honest aggregate ("Understood X of Y · N explored", never a "Mastery %") and a single-select level filter.
+- [ ] `mastery.js` logic is fully unit-tested (incl. the 2/3 boundary, glows/grows partition, zero-question, aggregate counts); mastery persistence round-trip tested with fake-indexeddb.
- [ ] All existing tests pass + new tests; `eslint .` 0 errors; HTML gate passes; no AI calls or `background.js` changes added.
## Resolved decisions
From 8bf3d575ed6ad4effda89c28b755a75178f8e93c Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Tue, 16 Jun 2026 20:41:10 -0700
Subject: [PATCH 3/8] docs(plan): The Mastery Loop implementation plan (Pillar
3, Spec 1)
---
.../plans/2026-06-16-mastery-loop.html | 110 ++++
.../plans/2026-06-16-mastery-loop.md | 558 ++++++++++++++++++
2 files changed, 668 insertions(+)
create mode 100644 docs/superpowers/plans/2026-06-16-mastery-loop.html
create mode 100644 docs/superpowers/plans/2026-06-16-mastery-loop.md
diff --git a/docs/superpowers/plans/2026-06-16-mastery-loop.html b/docs/superpowers/plans/2026-06-16-mastery-loop.html
new file mode 100644
index 0000000..40cf375
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-16-mastery-loop.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+ The Mastery Loop — Implementation Plan
+
+
+
+
+
+
+
The Mastery Loop
+
+ Implementation plan · Pillar 3 / Spec 1 · 2026-06-16 · branch feat/mastery-loop
+ Overview for review. Full step-by-step code (TDD steps, commands, commits) lives in the companion .md.
+
+
+
+
+ Goal. A self-graded mastery loop: earn a per-repo signal from the deep-dive's existing self-test questions, persist it locally, and show coverage growing across the library. No background.js changes, no new AI calls, fully local.
+
+
+
Architecture
+
New pure mastery.js (all scoring/leveling/aggregation, fully unit-tested). store.js gains CRUD for a new IDB mastery store. The deep-dive panel's existing "Test Yourself" block becomes an interactive flip-card check that computes a result via mastery.js and persists it. The library reads the mastery map for per-card indicators, an honest aggregate, and a single-select level filter.
getMastery / getAllMastery / setMastery(repoId, record), mirroring the decisions store; round-trip tested with fake-indexeddb. CRUD only — no compute in the store.
Replace the static "Test Yourself" block with one-card-at-a-time reveal → rate (Got it / Shaky / Missed) → auto-advance. On completion: compute via deriveCheckResult, show level + Glows/Grows (no %), persist via setMastery. Partial/zero-question → no write.
Load the mastery map once on library load, merge masteryLevel onto rows, render ○ / ◐ / ● left of the title (label on hover).
+
+
+
Task 5 · Aggregate line + level filter modify
+
library.js, library-filters.js, library.html
+
Honest aggregate ("Understood X of Y · N explored", never a %) via aggregateMastery; a single-select All/Understood/Explored/New filter mirroring the existing filter pattern.
`;
+ try {
+ await setMastery(repoId, record);
+ const saved = host.querySelector('.uc-saved');
+ if (saved) saved.hidden = false;
+ } catch (err) {
+ console.error('[mastery] save failed', err);
+ }
+ };
+
+ host.addEventListener('click', (e) => {
+ const action = e.target.closest('[data-uc]')?.dataset.uc;
+ if (!action) return;
+ if (action === 'reveal') { drawAnswer(); return; }
+ ratings[i] = action; // gotIt | shaky | missed
+ i++;
+ if (i < questions.length) drawCard();
+ else finish(); // persist only here, when every card is rated
+ });
+
+ drawCard();
+}
+```
+
+- [ ] **Step 4: Wire it into the deep-dive render**
+
+Replace the static `questionsBlock` (line 988-989) with a placeholder host, and mount the check after the panel HTML is set. Concretely: keep a container `
Check your understanding
` in place of the old "Test Yourself" block, then after the deep-dive panel's `innerHTML` is assigned, call:
+
+```js
+const ucHost = document.getElementById('dd-understand-check');
+renderUnderstandCheck(ucHost, fey.questions || [], lastData?.repoId);
+```
+
+(Read the surrounding assignment to place this call right after the panel HTML is written, like `renderCanvas`'s mount pattern.)
+
+- [ ] **Step 5: Verify**
+
+Run: `node --check output-tab.js && npm run check:html`
+Manual smoke: run a deep dive → the "Check your understanding" card appears → reveal → rate through all → see the level + Glows/Grows + "Saved to your library"; close mid-way → nothing saved.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add output-tab.js output-tab.html
+git commit -m "feat(mastery): interactive self-graded understand-check in the deep dive"
+```
+
+---
+
+## Phase 4 — See (the library mastery map)
+
+### Task 4: Per-card mastery indicator
+
+**Files:**
+- Modify: `library.js` — load the mastery map where rows are loaded; pass level into `card()` (~line 152); render the indicator.
+- Modify: `library-data.js` — thread a `mastery` level onto the row shape (or merge in `library.js`).
+- Modify: `library.html` — indicator styles.
+
+- [ ] **Step 1: Read the load + card render path**
+
+Run: `grep -n "getAllMastery\|allRows\|libraryRow\|scrollPoints\|function card\|render(" library.js library-data.js` and read `card()` (~152-192) + where `allRows` is populated. Determine the single place the mastery map should be fetched (once, on library load) and merged onto rows by `repoId`.
+
+- [ ] **Step 2: Indicator styles**
+
+In `library.html`, add:
+
+```css
+ .lib-mastery { display:inline-block; width:11px; text-align:center; margin-right:6px; vertical-align:baseline; }
+ .lib-mastery.m-new { color: var(--text-faint); }
+ .lib-mastery.m-explored { color: var(--warn-ink); }
+ .lib-mastery.m-understood { color: var(--accent); }
+```
+
+- [ ] **Step 3: Load mastery + merge onto rows**
+
+Where the library loads its rows (the function that fills `allRows`), `import { getAllMastery } from './store.js';` and `import { levelLabel } from './mastery.js';`, fetch the map once and set `row.masteryLevel = (masteryMap[row.repoId]?.level) || 'new'` on each row. (Do this in `library.js` at load — keep `library-data.js` pure if it doesn't already touch the store.)
+
+- [ ] **Step 4: Render the glyph in `card()`**
+
+In `card(r, i)` (~line 152), before the repo title, add a glyph mapped from `r.masteryLevel` (`new → ○`, `explored → ◐`, `understood → ●`) with the level as the `title` (hover label):
+
+```js
+const M_GLYPH = { new: '○', explored: '◐', understood: '●' };
+const mLevel = r.masteryLevel || 'new';
+const masteryDot = `${M_GLYPH[mLevel]}`;
+```
+
+Insert `${masteryDot}` immediately before the card's title text in the returned template.
+
+- [ ] **Step 5: Verify + commit**
+
+Run: `node --check library.js library-data.js && npm run check:html`
+Manual: a repo you marked understood shows ● on its card.
+```bash
+git add library.js library-data.js library.html
+git commit -m "feat(mastery): per-card mastery indicator in the library"
+```
+
+### Task 5: Aggregate line + single-select level filter
+
+**Files:**
+- Modify: `library.js` — render the aggregate line; add the level-filter control + state.
+- Modify: `library-filters.js` — apply the level filter in `applyFilters`.
+- Modify: `library.html` — filter control markup/styles (near the existing sort/lang filters).
+
+- [ ] **Step 1: Read the filter architecture**
+
+Run: `grep -n "applyFilters\|state\.\|capability\|lang-filter\|lib-sort\|libraryStats" library.js library-filters.js library-data.js` and read `applyFilters` (in `library-filters.js`) + how an existing filter (e.g. `capability` or `lang`) is wired end-to-end: the `state` field, the control that sets it, and where `applyFilters` reads it. Mirror that exact pattern for `state.mastery`.
+
+- [ ] **Step 2: Aggregate line**
+
+Using `aggregateMastery` (`import { aggregateMastery } from './mastery.js';`) over the loaded mastery map, render one line near the library header:
+
+```js
+const agg = aggregateMastery(masteryMap);
+const masteryLine = agg.total
+ ? `Understood ${agg.understood} of ${agg.total}${agg.explored ? ` · ${agg.explored} explored` : ''}`
+ : '';
+```
+
+Insert into the header area as plain text (no percentage). (Read where the existing header/stats render to place it.)
+
+- [ ] **Step 3: Add a single-select level filter**
+
+Add a control (mirroring the existing language/sort `