Skip to content

Closes #170#183

Merged
realproject7 merged 1 commit into
mainfrom
task/170-dashboard-pairing
Jul 14, 2026
Merged

Closes #170#183
realproject7 merged 1 commit into
mainfrom
task/170-dashboard-pairing

Conversation

@realproject7

Copy link
Copy Markdown
Owner

Closes #170

Pair each dashboard History row with its own session's index entry by identity, instead of by a positional reversal that swapped the duration/languages/cost subtitle between two sessions saved in the same clock-minute.

EPIC Alignment

Part of EPIC #134 (v1.3 audit follow-up), Batch 40 (re-pointed from #173 to #170 per operator #939 to restore the standing-order sequence #172#171#170#173). Dashboard/archive cluster: src/dashboard.ts (consumer) + packages/archive/src/dashboard.ts (the aggregate/index contract). Honors the Batch 36 amendment (SessionIndexEntry has no name; aggregateSessions takes only ParsedSession[], so identity is threaded by constructing name-paired entries before the positional step). Does not touch resample.rs/render.ts/parse.ts/pipeline.rs (reserved for #178/#179) or settings.

Self-Verification

The bug. renderHistory() paired each newest-first session (m.sessions) with the oldest-first chronological index (m.stats.index) via index[index.length - 1 - i], assuming the second array is the exact reverse of the first. Array.prototype.sort is stable, so when two sessions tie on (meta.headerDate, meta.startClock) — two back-to-back captures in the same clock-minute, or the <name> (2).md filename-collision the archive tests anticipate — the tie sub-group keeps the same relative order in both arrays rather than being mirrored. The blind full-array reversal then swapped the tied sessions' entries: a correctly-titled, correctly-linked row displayed the OTHER session's duration/date-time/languages/cost subtitle (via rowMeta()).

The fix (pair by identity, per the amendment).

  • Extracted a pure toSessionIndexEntry(session) in @livecap/archive (packages/archive/src/dashboard.ts:83-96), derived solely from the session — aggregateSessions now calls it (identical output, so stats.index and its tests are unchanged).
  • IndexedSession gains an entry field (src/dashboard.ts:60-64); buildDashboardModel builds it from each session alongside its file name (:96-101), so every history row carries the entry derived from its OWN session.
  • renderHistory now uses indexed.entry (src/dashboard.ts:452) — pairing by identity, never by position. The positional reversal and its assumption comment are removed.

This is the amendment's accepted approach — "construct name-paired entries before aggregation, then key the history mapping by name" — realized as each IndexedSession owning its entry (identity ≡ file name).

Note on stats.index. aggregateSessions still produces the chronological index (a public @livecap/archive type, covered by its own tests); the app's history no longer consumes it, but it remains a valid part of the archive contract, so it stays. No duplication — both paths build entries through the one shared toSessionIndexEntry.

Security invariants. No new dependencies. No caption/audio content logged — toSessionIndexEntry carries only meta numbers/langs/title (never transcript text; same fields the index already exposed). Simplest fix that solves the ticket; no drive-by refactors.

Tests / evidence.

  • test/dashboard.test.ts — new regression pairs each session with its OWN index entry when two tie on date+clock (#170): two sessions tied on date+clock with distinct duration (45 vs 10)/languages (EN→KO vs JA→FR)/cost, asserts each row's entry matches its own session, plus an entry↔session invariant over all rows.
  • Negative control (scratch, not committed) confirmed the old positional pairing swapped them: [{title:"Alpha",oldDur:10,ownDur:45},{title:"Beta",oldDur:45,ownDur:10}] — proving the regression is not vacuous.
  • Existing dashboard tests green: 18 app (test/dashboard.test.ts) + 8 archive (packages/archive/test/dashboard.test.ts, incl. the aggregateSessions index-row assertions — unchanged).
  • Full app suite 154 tests pass; archive package suite green; pnpm typecheck (app + packages), pnpm lint, pnpm build all clean.

Design Fidelity

No visual change — the fix corrects WHICH session's data a row shows, not how it renders. The historyRow markup, classes, and rowMeta() subtitle format are untouched; only the (now correct) entry feeds them.

Element Design (unchanged) Source pointer Markup/token/layout change?
History row title span + meta subtitle + optional snippet; click → detail src/dashboard.ts historyRow (:548-576); .dash-row/.dash-row-title/.dash-row-meta in styles.css None — same nodes/classes
Row subtitle (meta) rowMeta(entry) — date · time · duration · langs · cost src/dashboard.ts rowMeta (:228) None — same formatter; now fed the row's OWN entry
Row title / click target bound to session/name (was already correct) historyRow openDetail(name, session) None

The only user-visible effect: two sessions saved in the same clock-minute now each show their own duration/languages/cost, instead of each other's.

Deviations

  • Threaded identity by giving each IndexedSession its own entry (built in buildDashboardModel) rather than adding a name field to SessionIndexEntry + changing the aggregateSessions signature — this keeps the archive contract and its ~9 test call sites untouched, and moves the pairing into the pure, node-testable buildDashboardModel (renderHistory is DOM-coupled). It matches the ticket's fix option (a) ("build the SessionIndexEntry alongside each IndexedSession").
  • Left DashboardStats.index in place (still produced + tested as archive API) though the app history no longer reads it — removing it would be an out-of-scope contract change.

🤖 Generated with Claude Code

… position

renderHistory() paired each newest-first session (m.sessions) with the
oldest-first chronological index (m.stats.index) via index[index.length-1-i],
assuming the second array is the exact reverse of the first. Array.sort is
stable, so when two sessions tie on (headerDate, startClock) — two captures in
the same clock-minute, or the "<name> (2).md" collision — the tie sub-group keeps
the SAME relative order in BOTH arrays instead of being mirrored, so the blind
reversal swapped tied sessions' entries: a row showed the OTHER session's
duration/languages/cost subtitle under a correctly-titled, correctly-linked row.

Per the Batch 36 amendment (SessionIndexEntry has no name field; aggregateSessions
takes only ParsedSession[]): construct name-paired entries before the positional
step. Extract a pure `toSessionIndexEntry(session)` in @livecap/archive (reused by
aggregateSessions, no behavior change) and have buildDashboardModel derive each
IndexedSession's `entry` from its OWN session, alongside its file name. renderHistory
now reads `indexed.entry` — pairing by identity, never position. Positional
reversal + its comment removed.

Regression: a fixture of two sessions tied on date+clock with distinct
duration/langs/cost asserts each row carries its own entry (a negative control
confirmed the old reversal swapped them 45↔10). Existing dashboard tests green
(18 app + 8 archive); full app suite 154; typecheck/lint/build clean. No new
deps; no caption content logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@project7-interns project7-interns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE

Epic Alignment: PASS

The #170 fix preserves session/index identity before aggregation and eliminates the stable-sort tie mispair without changing the archive index contract.

Checked (evidence)

  • Identity pairing: src/dashboard.ts:57-64,91-107,445-454 derives and carries each session’s own entry; the positional reversal is removed.
  • Shared contract: packages/archive/src/dashboard.ts:83-104,142 centralizes entry derivation in toSessionIndexEntry, used by both aggregate output and dashboard model.
  • Regression: test/dashboard.test.ts:188-228 covers equal date/clock sessions with distinct duration, languages, and cost.
  • Design Fidelity: live PR table spot-checked; historyRow markup/classes and rowMeta() formatting are unchanged, with no CSS/token/layout/animation diff.
  • Riskiest part of this diff: tie ordering in stable sorts; row metadata is now object-bound rather than position-bound.
  • Kill-list: scanned new ranges — clean.
  • CI: gh pr checks 183 → app-macos, color-guard, no-stub-gate, packages-linux all pass.

Findings

  • None.

Decision

The amended identity approach and its tie regression fully satisfy #170 without scope creep.

@realproject7

Copy link
Copy Markdown
Owner Author

RE2 — APPROVE at PR #183 head (pending app-macos/packages-linux green)

The #170 positional-pairing bug is correctly fixed via identity pairing, the amendment is honored, and the toSessionIndexEntry extraction is byte-identical to the old inline push. Reviewed the whole diff (4 files) and independently ran the app + archive suites + both typechecks + lint on Linux with a real pnpm install (correct workspace linkage).

Checked (evidence)

  • Bug correctly diagnosed & fixed. Old buildDashboard's history pairing read index[index.length-1-i] against newest-first m.sessions, assuming the chronological index is the exact mirror. On a (headerDate, startClock) tie, Array.sort is stable so BOTH the ascending index and the descending sessions preserve the tie-group's input order (not mirrored) — the reversal lands index[len-1-i] on the other tied session, swapping duration/langs/cost subtitles (title/click target came from m.sessions so stayed correct). Fix: each IndexedSession now carries its OWN entry = toSessionIndexEntry(session) built in buildDashboardModel (src/dashboard.ts:96-101), and renderHistory uses m.sessions directly (:449, positional reversal + comment deleted) — pairing by identity, never position.
  • Amendment honored. Per the Batch-36 amendment (SessionIndexEntry has no name; aggregateSessions takes only ParsedSession[]), the fix builds the entry alongside each session before the positional step (fix option a) and keys the row by the session it was derived from — it does NOT try to thread a name into SessionIndexEntry. Verified SessionIndexEntry's shape is unchanged.
  • Extraction is byte-identical (so stats.index is unchanged). Compared toSessionIndexEntry (packages/archive/src/dashboard.ts:83-96) field-by-field against the old inline index.push at 5b02a74: every field matches, and the old talkRatioMic/smoothScore locals were metrics ? finite(metrics.talkRatioMic|smoothScore) : null — exactly what the helper computes. aggregateSessions now just calls the helper in the same loop/order, so stats.index output is identical. Confirmed empirically: the archive package's 8 dashboard tests pass unchanged.
  • No other positional consumer. Grep of PR-head src/dashboard.ts for .index/stats.index → the history row was the only positional pairing; it's gone. Trends etc. use stats.index content, not position-against-sessions.
  • Test is non-vacuous. New tie regression: two sessions on the same 2026-06-10 09:00 (the "(2)" filename-collision case) with distinct duration(45/10)/langs(EN→KO/JA→FR)/cost → asserts each row's entry carries its OWN numbers (would swap under the old reversal — dev's negative control confirmed) + an invariant loop that every entry is derived from its own session's meta.
  • Independently reproduced (node 24 / vitest 3.2.6, real pnpm install): app test/dashboard.test.ts 18/18 (incl. the tie regression); full app suite 22 files / 154 tests; @livecap/archive package 12 files / 95 tests (incl. the 8 unchanged dashboard tests); app-tsconfig AND host-tsconfig tsc --noEmit both clean; eslint on the 4 changed files clean.
  • Boundary/invariants/design: 4 files only (src/dashboard.ts, packages/archive/src/dashboard.ts + barrel index.ts, test/dashboard.test.ts); package.json/lock untouched (no new deps); no reserved files; no caption content logged (entry carries only meta numbers/langs/title). No visual change — historyRow markup/classes and rowMeta() untouched; only the (now correct) entry feeds them.

CI condition

color-guard/no-stub-gate pass; app-macos/packages-linux pending — packages-linux runs the archive-package tests, which I reproduced green locally, but the merge gate should still wait for both green. (Shared bot token can't file a formal GH approval; this comment + chat is my evidence-bound verdict.)

@realproject7
realproject7 merged commit 17bdeb3 into main Jul 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[audit][bug] Dashboard history pairs sessions with index entries by reversed position — ties in the two stable sorts misalign badges and stats

2 participants