Closes #170#183
Conversation
… 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
left a comment
There was a problem hiding this comment.
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-454derives and carries each session’s own entry; the positional reversal is removed. - Shared contract:
packages/archive/src/dashboard.ts:83-104,142centralizes entry derivation intoSessionIndexEntry, used by both aggregate output and dashboard model. - Regression:
test/dashboard.test.ts:188-228covers equal date/clock sessions with distinct duration, languages, and cost. - Design Fidelity: live PR table spot-checked;
historyRowmarkup/classes androwMeta()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.
RE2 — APPROVE at PR #183 head (pending
|
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 noname;aggregateSessionstakes onlyParsedSession[], so identity is threaded by constructing name-paired entries before the positional step). Does not touchresample.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) viaindex[index.length - 1 - i], assuming the second array is the exact reverse of the first.Array.prototype.sortis 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).mdfilename-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 (viarowMeta()).The fix (pair by identity, per the amendment).
toSessionIndexEntry(session)in@livecap/archive(packages/archive/src/dashboard.ts:83-96), derived solely from the session —aggregateSessionsnow calls it (identical output, sostats.indexand its tests are unchanged).IndexedSessiongains anentryfield (src/dashboard.ts:60-64);buildDashboardModelbuilds it from each session alongside its file name (:96-101), so every history row carries the entry derived from its OWN session.renderHistorynow usesindexed.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
IndexedSessionowning its entry (identity ≡ file name).Note on
stats.index.aggregateSessionsstill produces the chronologicalindex(a public@livecap/archivetype, 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 sharedtoSessionIndexEntry.Security invariants. No new dependencies. No caption/audio content logged —
toSessionIndexEntrycarries 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 regressionpairs 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'sentrymatches its own session, plus an entry↔session invariant over all rows.[{title:"Alpha",oldDur:10,ownDur:45},{title:"Beta",oldDur:45,ownDur:10}]— proving the regression is not vacuous.test/dashboard.test.ts) + 8 archive (packages/archive/test/dashboard.test.ts, incl. theaggregateSessionsindex-row assertions — unchanged).pnpm typecheck(app + packages),pnpm lint,pnpm buildall clean.Design Fidelity
No visual change — the fix corrects WHICH session's data a row shows, not how it renders. The
historyRowmarkup, classes, androwMeta()subtitle format are untouched; only the (now correct) entry feeds them.src/dashboard.tshistoryRow(:548-576);.dash-row/.dash-row-title/.dash-row-metainstyles.cssrowMeta(entry)— date · time · duration · langs · costsrc/dashboard.tsrowMeta(:228)session/name(was already correct)historyRowopenDetail(name, session)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
IndexedSessionits ownentry(built inbuildDashboardModel) rather than adding anamefield toSessionIndexEntry+ changing theaggregateSessionssignature — this keeps the archive contract and its ~9 test call sites untouched, and moves the pairing into the pure, node-testablebuildDashboardModel(renderHistory is DOM-coupled). It matches the ticket's fix option (a) ("build the SessionIndexEntry alongside each IndexedSession").DashboardStats.indexin 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