+
+
+ 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 `