Repository navigation
feat(capture-recovery): interrupted-capture recovery machine + truthful one-handed UI projection - #15
Conversation
…ul one-handed UI projection Additive @journal/capture-recovery package (proposal toward contract v0.3, bounded extension of art_I2TCG08V v0.2): - Pure recovery reducer: drafting/pending/saved/failed/discarded; saved only via SubmitAccepted (no optimistic truth); stable submissionId; monotonic attempt; stale-result suppression; duplicate-submit suppression; discard receipts; total reducer with bounded anomalies instead of throws. - Truthful one-handed banner projection (bannerFor): bottom-anchored, 48pt targets, <=2 actions, copy derived from persisted state only. - CaptureStorage contract (load/listUnresolved/listReceipts) with fail-closed decodeStoredState. - 7 replayable JSON fixtures: call interruption, backgrounding+process death, network loss while pending, failed->retry, duplicate submission, stale result, discard receipt. 20 bun tests, 107 assertions. - Imports CaptureId/ExtractionAttempt/ExtractionStatus from @journal/domain; no changes to domain, entry, event, extraction, ui, security, evaluation.
Independent review — verdict: pass-with-notesTested head SHA: What I ran at the exact HEAD
Scope (criterion 8) — verified additive-only20 files, 1367 insertions, 0 deletions: Truthfulness invariants — verified in source, not just test names
Consumer lens (candidate B's two hard-requirement failures): interruption recovery is closed by fixtures 01–03; the truthful failed-extraction state is closed by Notes (non-blocking)
Verdict for the serialized sweep: review result pass-with-notes on |
|
Merge-owner suite evidence — tested head Run by the serialized merge owner (parent-home recovery lane) in a clean detached worktree at the tested head, 2026-09-17:
|
{
"manifestVersion": 1,
"pr": 15,
"prUrl": "https://github.com/OCPdev25/obv-hackaton/pull/15",
"testedHeadSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"classification": "backend-only",
"classificationJustification": "Pure TypeScript package under packages/capture-recovery plus lockfile only (no apps/, backend/, or other package paths changed); the one-handed UI ships as a projection contract, not rendered RN",
"review": {
"result": "pass-with-notes",
"reviewer": "independent review agent — parent-home recovery lane (th_V9xo0wZ7)",
"reviewedHeadSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"date": "2026-09-17",
"url": "https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720572634"
},
"checks": [
{
"name": "Typecheck, test, build",
"status": "green",
"headSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"url": "https://github.com/OCPdev25/obv-hackaton/actions/runs/35258877099/job/105329282536"
}
],
"evidence": [
{
"kind": "test-run",
"name": "capture-recovery focused suite: 20 pass / 0 fail / 107 expect() calls",
"headSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"result": "pass",
"url": "https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720572634"
},
{
"kind": "suite-run",
"name": "turbo typecheck/test/build full repo: 12/12 tasks, 0 cached",
"headSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"result": "pass",
"url": "https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720572634"
}
],
"suites": {
"security": {
"ran": true,
"result": "pass",
"headSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"url": "https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720601545"
},
"evaluation": {
"ran": true,
"result": "pass",
"negativeControl": "fail-as-expected",
"headSha": "ffa545dc70a62f4492dba748f3a18a5d403f8446",
"url": "https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720601545"
}
},
"notes": "Security and evaluation suites run by the serialized merge owner in a clean detached worktree at the tested head; run log in https://github.com/OCPdev25/obv-hackaton/pull/15#issuecomment-5720601545. Manifest recorded by merge owner because the PR predates the verification gate (PR #26). Reviewer consumer notes (projection 48pt/bottom-anchored rendering, durable-adapter integration test at consumer) routed to the composite consumer PR."
} |
…domain (#35) Post-squash adaptation for PR #15's merge commit 9ea7e73: the capture-recovery package was written against a pre-v0.3-fold domain where ExtractionAttempt was a numeric attempt counter and CaptureId was an unbranded string. The canonical domain renamed the counter to AttemptNumber (adaptation A5) and now brands CaptureId (Schema.NonEmptyString.pipe(Schema.brand)). - state.ts / events.ts: attempt fields typed AttemptNumber (the counter) - reducer.ts: createCapture input takes the branded CaptureId (type-only import; the pure reducer gains no runtime dependency) - replay.ts: decode the fixture captureId through the canonical CaptureId schema at the boundary (fail-closed, no casts) - test: decode branded CaptureIds at the four direct createCapture sites and in the storage listUnresolved/listReceipts expectations - README: correct the imported-type names No behavioral change to the recovery machine's truthfulness invariants; the six fixture scenarios, their expectations, and the discard-receipt semantics are unchanged. Co-authored-by: Obvious <obvious@obvious.ai>
Acceptance criteria
drafting / pending / saved / failed / discarded;savedis reachable only viaSubmitAccepted(no optimistic truth) — pinned by tests.submissionId(idempotent server replay) and bumps the extraction attempt — fixture 04.CaptureId/ExtractionAttempt/ExtractionStatusfrom@journal/domain; zero changes to domain, entry, event, extraction, ui, security, or evaluation packages.pnpm turbo run typecheck test build).What this is
Additive
@journal/capture-recoverypackage: a bounded direct-worker extension of the canonical contract (art_I2TCG08Vv0.2), positioned as a proposal implemented as code toward contract v0.3.src/state.ts— recovery state schema (reuses canonicalCaptureId,ExtractionAttempt,ExtractionStatus)src/events.ts— 11 tagged recovery events (interruption, resume, submit/retry/accept/reject, extraction result, network, discard)src/reducer.ts— pure, total reducer; unexpected combinations become bounded anomalies instead of throwssrc/banner.ts— truthful one-handed UI projection: bottom-anchored, 48pt targets, ≤2 actions, copy derived from persisted state onlysrc/storage.ts—CaptureStoragecontract (load/listUnresolved/listReceipts) with fail-closeddecodeStoredStatesrc/replay.ts+fixtures/*.json— 7 replayable scenarios with cold-startreloadstepstest/recovery.test.ts— 20 tests / 107 assertions pinning every invariant aboveREADME.md— the contract document, including ownership/handoff (slot 22 offline-capture lane, voice/photo capture slice, review-UX candidates) and honest known limitationsPhases describe delivery of a capture, never audience:
Entry.visibility(draft|published) stays the only per-entry publication state and household grants stay the audience dimension (contract v0.2, two independent dimensions).Evidence
pnpm exec turbo run build typecheck test --filter=@journal/capture-recovery→ 4/4 tasks green, 20 tests pass (HEADffa545d)pnpm typecheck && pnpm test && pnpm build→ green (turbo cache, HEADffa545d)Evidence receipt (completed at merge)