[#113] Archive: persist coaching rewrites into session files#122
Conversation
Coaching rewrites from the post-meeting review were thrown away — the archive
kept only raw utterances. Persist them so past sessions retain coaching value.
- types.ts: optional CaptionEntry.coaching { better, changes[], explanation },
only ever set for "me" entries (+ exported CoachingData).
- render.ts: renderCoaching emits a trailing "## Coaching" section keyed by
(timestamp · k), where k is the 1-based occurrence among "me" entries sharing
that timestamp. Returns "" when no entry is coached, so coaching-free docs are
byte-identical (append-only transcript invariant + goldens intact).
- parse.ts: inverse — parses the section (derived from render's exact
separators, not guessed; multi-line better/explanation supported) and
re-attaches to me-entries by occurrence. Files without the section parse
exactly as before.
- writer.ts: amendCoaching(updates) — post-finalize only (all other methods
throw once finalized), atomic temp+rename like the rest of the writer. Attaches
to a COPY of each matched entry so caller inputs are never mutated; because the
section is appended, every other section stays byte-identical.
No new runtime deps; headless (no Tauri imports); never logs caption content;
confined to packages/archive.
Tests: render→parse deep-equal (EN + KO); finalize-then-amend byte-identical;
occurrence disambiguation; backward-compat fixture; CJK-safe multibyte round-trip;
amend-before-finalize throws; unmatched-update no-op. Full archive suite (75) green.
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: REQUEST CHANGES
Epic Alignment: PASS
The archive-only design matches #113/#107: optional coaching data, rendered/parsing ## Coaching, and post-finalize amend are in the right package and preserve #114 boundaries. One source hygiene issue blocks approval.
Checked (evidence)
- Structural gate:
gh pr view 122 --json body,headRefOid,mergeStateStatusate422337df81d2ce90c88f75338532bdc1b4f1397includes filled## EPIC Alignmentand## Self-Verification; PR is CLEAN. - Issue/epic context:
gh issue view 113plusgh api repos/realproject7/livecap/issues/107problem #5 require archive persistence for coaching rewrites, with #114 UI wiring later. - Scope:
gh pr diff 122 --name-onlyshows onlypackages/archivesource/tests. - Render contract:
packages/archive/src/render.ts:77/:78define named separators;:103renders trailing## Coachingonly for coachedmeentries;:130appends it after transcript. - Parse contract:
packages/archive/src/parse.ts:167parses coaching blocks;:324attaches parsed coaching to transcript entries;:365only attaches tospeaker: "me". - Writer contract:
packages/archive/src/writer.ts:145adds post-finalizeamendCoaching;:148/:149gate opened/finalized state;:165writes via existing atomic temp+rename path. - Riskiest part: the persisted markdown keying and amend path; conceptually sound, but the implementation currently introduces a binary byte into a TypeScript source file.
- Kill-list: scanned changed diff for TODO/FIXME/stubs/logging/new deps/Tauri; finding below.
- CI:
gh pr checks 122→ app-macos pass, packages-linux pass, no-stub-gate pass.
Findings
- [blocking] TypeScript source contains a literal NUL byte, causing
parse.tsto be treated as binary.- File:
packages/archive/src/parse.ts:157 - Why it fails:
coachingKey()uses an actual null byte between${timestamp}and${occurrence}. Live byte check found one0x00byte in the file, andgh pr diff 122 --patchshowspackages/archive/src/parse.tsas aGIT binary patchinstead of a text diff. Source files must remain text-reviewable; otherwise future reviews, blame, grep/diff tooling, and patch review can miss behavior changes in this parser. - Do instead: replace the literal NUL with a text-safe key encoding, e.g.
JSON.stringify([timestamp, occurrence]), or an escaped string sequence that leaves the source file ASCII/text ("\u0000"as characters in source, not an embedded byte). Add a small regression check if useful that the key remains collision-safe without embedding control bytes in the source.
- File:
Decision
REQUEST CHANGES. The feature shape is aligned, but the binary byte in parse.ts must be removed before this can be approved.
|
RE2 review — PR #122 (Closes #113, archive coaching persistence) @ e422337 — APPROVE Reviewed the live diff against issue #113 + its 2026-07-05 amendment, deriving the parser from render (not the description). Checked (evidence)
Findings
Non-blocking notes (optional, not gating)
DecisionAPPROVE at e422337. Amend is append-only/byte-identical and post-finalize+atomic, parser is a real derived inverse (EN/KO/CJK/multi-line/occurrence all round-trip), input immutability fixed, backward-compatible, boundaries respected, no new deps / no caption logging. No merge (notify-and-wait per dispatch). |
RE1 review: coachingKey embedded a literal NUL between timestamp and occurrence, which turned parse.ts into a binary (un-reviewable) file. Encode the key as JSON.stringify([timestamp, occurrence]) in both parse.ts and writer.ts — collision-safe and plain text. Add a source-hygiene regression test asserting the coaching modules contain no control bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
RE2 re-review — PR #122 (Closes #113) @ be8c514 — APPROVE @re1's blocking finding is fully resolved. Reviewed the delta e422337..be8c514. Checked (evidence)
Findings
DecisionAPPROVE at be8c514. |
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Epic Alignment: PASS
PR #122 satisfies #113 under #107 by adding archive-only persisted coaching data, a rendered/parsed ## Coaching section, and post-finalize amendment while preserving #114 UI boundaries.
Checked (evidence)
- Re-review delta: reviewed fix commit
be8c514after prior RE1 finding ate422337; PR head isbe8c5147ab620daf4da1232d3dafa7a7af1d1450. - Prior finding resolved: current
packages/archive/src/parse.ts:157now usesJSON.stringify([timestamp, occurrence]); live byte check on currentparse.tsfound0NUL bytes. - Writer symmetry:
packages/archive/src/writer.ts:151defines the same JSON-encoded key and:155/:164use it for update lookup and entry matching. - Regression coverage:
packages/archive/test/coaching.test.ts:227adds the source-hygiene guard;:235rejects control bytes except tab/newline/carriage return in parse/writer/render sources. - Text-reviewability:
gh api repos/realproject7/livecap/pulls/122/filesnow returns a normal text patch forpackages/archive/src/parse.ts(additions: 110,deletions: 4) instead of omitting it as binary. - Original scope still checked: archive-only file set; render appends
## Coaching, parse reattaches only tomeentries, and writer gatesamendCoachingto post-finalize atomic rewrite. - Riskiest part: persisted markdown keying; acceptable now because keying is collision-safe without embedding binary/control bytes in source.
- Kill-list: scanned changed delta; clean.
- CI:
gh pr checks 122→ app-macos pass, packages-linux pass, no-stub-gate pass.
Findings
- None.
Decision
APPROVE. The blocking source hygiene issue is fixed and covered by a regression guard; the archive coaching persistence remains scoped and aligned. No merge action taken.
Closes #113
EPIC Alignment
CaptionEntry.coaching?: CoachingData(types.ts), the rendered## Coachingsection (render.ts renderCoaching), its parse inverse (parse.ts), andSessionArchiveWriter.amendCoaching(updates)(writer.ts). The(timestamp · occurrence)key is the contract UI: persist coaching on generate + Dashboard renders saved rewrites #114 uses to target ameentry. No existing signature changed; the new field is optional and additive.Depends on:. Consumes the just-merged Engine: batch coaching rewrites into grouped completions (~30s → ~6s for 10 utterances) #112 coach-result shape structurally ({ better, changes[], explanation }) without importingpackages/engine(this package stays engine-free). Deliberately does not touchpackages/engine(Engine: batch coaching rewrites into grouped completions (~30s → ~6s for 10 utterances) #112) orsrc/(UI: persist coaching on generate + Dashboard renders saved rewrites #114) — UI: persist coaching on generate + Dashboard renders saved rewrites #114 builds onamendCoaching+ the parsedcoachingfield cleanly.Self-Verification
pnpm lint→ exit 0 (clean).pnpm typecheck+pnpm -r --filter './packages/*' typecheck→ clean (archivetsc --noEmitpasses understrict+noUncheckedIndexedAccess).pnpm --filter './packages/archive' test→ 75 passed (11 new incoaching.test.ts, existing 64 preserved — goldens/parse/dashboard all green, proving byte-identity + backward compat).pnpm test:app→ 91 passed; engine typecheck clean (untouched).package.json/lockfile change), no Tauri imports (grepclean — headless), no caption content logged (git diffadds zeroconsole./log().scripts/no-stub-gate.sh→ passed.SessionArchiveWriterthrough the in-memoryFakeFs(open → append → finalize →amendCoaching→parseSession), so round-trips run against the exact bytes the writer emits.What
types.ts— optionalCaptionEntry.coaching+ exportedCoachingData(only ever populated forspeaker: "me"entries).render.ts—renderCoaching(entries)emits a trailing## Coachingsection, each coachedmeentry as### (timestamp · k) — <source echo>+**Better:** …(+ optional**Changes:**/**Explanation:**, omitted when empty).kis the 1-based occurrence amongmeentries sharing that timestamp. Returns""when nothing is coached → coaching-free documents are byte-identical (append-only invariant + goldens intact). Section is placed after the transcript soamendCoachingonly ever appends.parse.ts— the exact inverse, keyed offrender.ts's named separators (not guessed): parses the section into a(timestamp · occurrence) → CoachingDatamap (multi-linebetter/explanationsupported) and re-attaches tomeentries by recomputing the same occurrence. Files without the section parse exactly as before.writer.ts—amendCoaching(updates): Promise<void>, callable only after finalize (every other method throws once finalized), atomic via the existing temp-file + rename discipline. Attaches to a copy of each matched entry so the caller'sCaptionEntryobjects are never mutated; unmatched updates are ignored (never throws).Deviations
## Coachingsection, not inline in the transcript — this makesamendCoachinga pure append (guaranteeing byte-identity of all prior sections) and keeps the transcript's append-only invariant intact. The schema still lives onCaptionEntry.coachingas specified; the section is keyed back to entries by(timestamp · occurrence).amendCoachingreturnsPromise<void>(per the amendment's suggested signature) though the injected fs is synchronous — it resolves immediately; callersawaitit.packages/archive;packages/engine(Engine: batch coaching rewrites into grouped completions (~30s → ~6s for 10 utterances) #112) andsrc/(UI: persist coaching on generate + Dashboard renders saved rewrites #114) untouched.