citation-evidence/src/app/App.tsx

347 lines
9.9 KiB
TypeScript
Raw Normal View History

Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
/**
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
* App citation-evidence demo shell (CE-WP-0005).
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
*
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
* Composition:
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
*
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
* SessionProvider (cross-session)
* AppShell owns routing + the top bar
* if no active session CreateFirstSession (empty state)
* else
* EngineProvider key={sessionId} sessionId={sessionId}
* BinderProvider bus={engine.bus}
* ReviewLayout | FormsApp (per `mode`)
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
*
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
* The hash is the single source of truth for `{sessionId, mode}`. The
* SessionService's active id is kept in sync with the hash via a
* useEffect inside `AppShell`. Deep links to unknown sessions redirect
* to the empty state with a toast.
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
*/
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
import { BinderProvider } from "@binder/index";
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
import {
EngineProvider,
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
SessionProvider,
useActiveSession,
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
useEngine,
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
usePdfByteStore,
useSessionByteStoreRegistry,
useSessionService,
useSessionsHydrated,
useSessionVersion,
useSessionVersionBumper,
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
} from "@work/index";
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
import { FormsApp } from "./forms/FormsApp";
import { ReviewLayout } from "./ReviewLayout";
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
import {
CreateFirstSession,
EMPTY_ROUTE,
exportSessionZip,
importSessionZip,
parseRoute,
navigateTo,
SessionMenu,
sessionZipFilename,
Toast,
triggerSessionDownload,
UploadDropzone,
useToast,
type AppMode,
type AppRoute,
} from "./sessions";
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
function readRoute(): AppRoute {
if (typeof window === "undefined") return EMPTY_ROUTE;
return parseRoute(window.location.hash);
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
}
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
function useHashRoute(): AppRoute {
const [route, setRoute] = useState<AppRoute>(() => readRoute());
useEffect(() => {
const handler = () => setRoute(readRoute());
window.addEventListener("hashchange", handler);
return () => window.removeEventListener("hashchange", handler);
}, []);
return route;
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
}
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
function AppShell() {
const route = useHashRoute();
const service = useSessionService();
const hydrated = useSessionsHydrated();
const toast = useToast();
// Guards the "unknown session id → toast + redirect" path against an
// infinite loop: `useToast.show` creates a fresh `toast` object every
// render, which would otherwise re-fire the effect.
const lastHandledSessionIdRef = useRef<string | null>(null);
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
// Sync hash → SessionService.setActive. Unknown session ids fall back
// to the empty state with a toast.
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
useEffect(() => {
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
if (!hydrated) return;
const key = route.sessionId ?? "";
if (lastHandledSessionIdRef.current === key) return;
lastHandledSessionIdRef.current = key;
if (route.sessionId === null) {
service.setActive(null);
return;
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
}
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
const exists = service.get(route.sessionId);
if (exists) {
service.setActive(route.sessionId);
} else {
toast.show("Session not found — opened the empty state instead", "error");
navigateTo(EMPTY_ROUTE);
}
}, [route.sessionId, service, hydrated, toast]);
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
if (!hydrated) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
fontFamily: "system-ui, sans-serif",
color: "#888",
}}
>
Loading
</div>
);
}
if (route.sessionId === null) {
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<EmptyTopBar />
<div style={{ flex: 1, minHeight: 0 }}>
<CreateFirstSession />
</div>
<Toast toast={toast.toast} onDismiss={toast.dismiss} />
</div>
);
}
return <ActiveAppFrame route={route} toast={toast} />;
}
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
function ActiveAppFrame({
route,
toast,
}: {
route: AppRoute;
toast: ReturnType<typeof useToast>;
}) {
// EngineProvider remounts whenever the session id OR the per-session
// version counter changes. Import-into-active-session bumps the version
// so the new state from storage is picked up.
const sessionId = route.sessionId!;
const version = useSessionVersion(sessionId);
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh", color: "#222" }}>
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
<EngineProvider key={`${sessionId}:${version}`} sessionId={sessionId}>
<ActiveTopBar route={route} showToast={toast.show} />
<div style={{ flex: 1, minHeight: 0 }}>
<SessionScopedTree mode={route.mode} />
</div>
</EngineProvider>
<Toast toast={toast.toast} onDismiss={toast.dismiss} />
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
</div>
);
}
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
function SessionScopedTree({ mode }: { mode: AppMode }) {
const engine = useEngine();
return (
<BinderProvider bus={engine.bus}>
{mode === "forms" ? <FormsApp /> : <ReviewLayout upload={<UploadDropzone />} />}
</BinderProvider>
);
}
function EmptyTopBar() {
const sessionService = useSessionService();
const registry = useSessionByteStoreRegistry();
const bumpVersion = useSessionVersionBumper();
const toast = useToast(); // local toast — empty state has its own
const handleImport = useCallback(async (file: File) => {
try {
const result = await importSessionZip(file, {
sessionService,
getOrCreateByteStore: registry.getOrCreateByteStore,
bumpSessionVersion: bumpVersion,
});
navigateTo({ sessionId: result.sessionId, mode: "review" });
toast.show(
result.outcome === "created"
? "Imported as a new session"
: "Merged into existing session",
"success",
);
} catch (err) {
toast.show(
err instanceof Error ? `Import failed: ${err.message}` : "Import failed",
"error",
);
}
}, [sessionService, registry, bumpVersion, toast]);
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
return (
<header
style={{
display: "flex",
gap: 8,
padding: "6px 12px",
borderBottom: "1px solid #ddd",
background: "#fafafa",
fontFamily: "system-ui, sans-serif",
alignItems: "center",
}}
>
<strong style={{ fontSize: 13, marginRight: 8 }}>citation-evidence</strong>
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
<SessionMenu onImportZip={() => pickAndImport(handleImport)} />
<Toast toast={toast.toast} onDismiss={toast.dismiss} />
</header>
);
}
function pickAndImport(onPicked: (file: File) => void): void {
if (typeof document === "undefined") return;
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip,application/zip";
input.onchange = () => {
const file = input.files?.[0];
if (file) onPicked(file);
};
input.click();
}
function ActiveTopBar({
route,
showToast,
}: {
route: AppRoute;
showToast: (msg: string, tone?: "success" | "error" | "info") => void;
}) {
const engine = useEngine();
const byteStore = usePdfByteStore();
const session = useActiveSession();
const sessionService = useSessionService();
const registry = useSessionByteStoreRegistry();
const bumpVersion = useSessionVersionBumper();
const handleModeChange = useCallback(
(next: AppMode) => {
if (!route.sessionId) return;
navigateTo({ sessionId: route.sessionId, mode: next });
},
[route.sessionId],
);
const handleExport = useCallback(async () => {
if (!session) return;
try {
const blob = await exportSessionZip(engine, byteStore, session);
triggerSessionDownload(blob, sessionZipFilename(session));
showToast("Session exported", "success");
} catch (err) {
showToast(
err instanceof Error ? `Export failed: ${err.message}` : "Export failed",
"error",
);
}
}, [engine, byteStore, session, showToast]);
const handleImport = useCallback(
async (file: File) => {
try {
const result = await importSessionZip(file, {
sessionService,
getOrCreateByteStore: registry.getOrCreateByteStore,
bumpSessionVersion: bumpVersion,
});
navigateTo({ sessionId: result.sessionId, mode: "review" });
const totals = result.stats;
const summary =
result.outcome === "created"
? `Imported new session — ${totals.documentsAdded} document${totals.documentsAdded === 1 ? "" : "s"}, ${totals.annotationsAdded} annotation${totals.annotationsAdded === 1 ? "" : "s"}`
: `Merged into existing — ${totals.documentsAdded} new doc${totals.documentsAdded === 1 ? "" : "s"}, ${totals.documentsDeduped} deduped`;
showToast(summary, "success");
} catch (err) {
showToast(
err instanceof Error ? `Import failed: ${err.message}` : "Import failed",
"error",
);
}
},
[sessionService, registry, bumpVersion, showToast],
);
const tabs = useMemo(
() => [
{ id: "review" as const, label: "Review" },
{ id: "forms" as const, label: "Forms" },
],
[],
);
return (
<header
style={{
display: "flex",
gap: 8,
padding: "6px 12px",
borderBottom: "1px solid #ddd",
background: "#fafafa",
fontFamily: "system-ui, sans-serif",
alignItems: "center",
}}
>
<strong style={{ fontSize: 13, marginRight: 8 }}>citation-evidence</strong>
<SessionMenu
onExportZip={() => void handleExport()}
onImportZip={() => pickAndImport((file) => void handleImport(file))}
/>
<div style={{ display: "flex", gap: 4, marginLeft: 12 }}>
{tabs.map((t) => (
<button
key={t.id}
onClick={() => handleModeChange(t.id)}
aria-pressed={route.mode === t.id}
style={tabStyle(route.mode === t.id)}
>
{t.label}
</button>
))}
</div>
Implement CE-WP-0003 T01-T08: form binding + visual guide overlay T01 EvidenceLink/EvidenceSet types - src/shared/evidence-link.ts: status (§2.4), relation (§2.5), target - src/shared/evidence-set.ts: ordered group + activeEvidenceItemId - enum-conformance test parses SharedContracts.md and asserts the runtime lists match exactly T02 Binding service + in-memory link repo + active-state machine - src/binder/repos/in-memory-links.ts: Map-backed EvidenceLinkRepository - src/binder/services/bindings.ts: link/unlink/list/update/setActive emitting §4 EvidenceLinkCreated / EvidenceLinkUpdated / EvidenceItemActivated - src/binder/state/active.ts: (target, evidence, annotation) reducer + ActiveStateProvider + useActiveState hook - extended engine/events/types.ts with EvidenceLinkCreated, EvidenceLinkUpdated, FormFieldActivated payloads T03 Rect registry (SharedContracts §7 — contract FROZEN) - src/binder/visual-guide/rect-registry.ts: register/getRect/subscribe + invalidate + getVersion for useSyncExternalStore - events.ts: scroll/resize/focus pumps via window + ResizeObserver + IntersectionObserver, rAF-throttled - react-hooks.ts: RectRegistryProvider, useRegisterRect(kind,id,ref), useRectRegistryVersion T04 Form schema + renderer - src/app/forms/demo-schema.ts: text/textarea/date minimal schema - src/binder/FormRenderer.tsx: renders schema, each field registers as rect kind="field"; active field gets aria-current="true" - placed in binder/ (not work/) because work cannot import binder per DependencyMap.md §2 and the renderer needs the rect-registry hook; workplan T04 was amended in-place to document this T05 Side-by-side Forms layout + click-to-link - src/app/forms/FormsApp.tsx + src/app/App.tsx top-bar router with hash route #/forms/demo - BinderProvider mounted at app root so links survive tab switching - stage-evidence-then-click-field linking interaction with banner + per-field link-count chip T06 Active-evidence cycling - src/app/forms/ActiveEvidenceChips.tsx: chips per active target, Tab cycles natively, first chip auto-activates on field focus, each chip registers as rect kind="evidence-card" - ScrollBridge in FormsApp wires activeAnnotationId to viewer scroll - EvidenceSidebar + EvidenceStrip highlight the active item via the new useLastActivatedEvidence hook in work/EngineContext T07 SVG visual-guide overlay - src/binder/visual-guide/Overlay.tsx: single fixed-positioned SVG, draws field→card and card→highlight bezier curves for the active triple, rAF-throttled via the registry - src/anchor exposes getHighlightClientRects(annotationId); the spike viewer wraps highlights in [data-highlight-id] so the helper can locate them - src/app/forms/HighlightRectBridge.tsx: registers the active annotation's rect via that helper T08 End-to-end test (PRD scenario steps 5-9) - tests/integration/forms-overlay-e2e.dom.test.tsx: full path from Review-mode capture through Forms-mode link to active triple + aria-current assertions + 2 SVG paths in the overlay - additional integration coverage: forms-link-flow + forms-active-cycling Gates: typecheck ✓ · lint ✓ · build ✓ · 152/152 tests across 21 files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:53:17 +02:00
</header>
);
}
function tabStyle(active: boolean) {
return {
padding: "4px 12px",
fontSize: 12,
border: "1px solid #ccc",
borderBottom: active ? "2px solid #0050b3" : "1px solid #ccc",
background: active ? "#e8f0ff" : "white",
cursor: "pointer" as const,
};
}
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
export function App() {
return (
Implement CE-WP-0005 T01-T08: demo app — sessions, uploads, ZIP archive Turn the MVP into a self-contained demo. Users now: 1. Land on an empty-state and create a named session. 2. Drag-drop or pick arbitrary PDFs into that session. 3. Annotate, build evidence, link to form fields — all session-scoped. 4. Export the whole session as a single .zip archive (manifest + per-document PDFs). 5. Import a .zip back — into a new session, or merged into an existing one (documents deduped by SHA-256 fingerprint; annotations/evidence/links added additively). Architecture: - New shared types: SessionId, Session, SessionArchiveManifest + parseSessionArchiveManifest with schema-version validation. - SessionService (engine/services/sessions.ts) handles lifecycle (create/rename/delete/setActive) + emits 4 new events through its own bus; SharedContracts.md §4 lists the additions. - SessionProvider (work/SessionContext.tsx) owns the cross-session state: service, per-session PdfByteStore registry, per-session version counter that drives EngineProvider remounts after imports. - EngineProvider becomes session-aware (sessionId prop drives per- session localStorage keys). Bumping engineRevision after restoreFromStorage forces consumers to re-render so restored repos show up immediately. - PdfByteStore (source/pdf/byte-store.ts) holds Uint8Array bytes per document and mints blob URLs; ingestPdfFromFile is the upload entry-point that wraps the existing ingestPdf pipeline. - ADR-0008 locks the ZIP layout (manifest.json + documents/<id>.pdf), the manifest schema (schemaVersion 1), and the merge-on-collision policy. JSZip is the only new dependency. - App.tsx restructured: SessionProvider at the root, EngineProvider keyed by ${sessionId}:${version}, hash routing #/s/<id>[/forms/demo], SessionMenu top-bar, CreateFirstSession empty state. - New DocumentRemoved event for per-document delete cleanup in CollectionList; engine.documents.remove() is the new service method. Tests: - Unit: 16 SessionService lifecycle + persistence tests; per-session snapshot round-trip; PdfByteStore + ingestPdfFromFile; SessionArchive parser; exportSessionZip + importSessionZip with create + merge + corrupt-archive paths. - DOM: UploadDropzone, session-scoped CollectionList delete, SessionMenu create/switch/rename, routing parser. - E2E: tests/integration/session-export-reimport.dom.test.tsx walks the full create → annotate → export → reimport flow and asserts the additive merge (deduped doc + doubled evidence rows). - Legacy E2Es updated to use a seed-session helper instead of the removed fixture-button flow. Known limitation (documented in ADR-0008): re-importing your own freshly-exported ZIP creates duplicate annotations. Forward pointer left for an importBundleId follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:57:28 +02:00
<SessionProvider>
<AppShell />
</SessionProvider>
Implement CE-WP-0002 T03-T09: ingest, anchor resolution, engine, UI, persistence, e2e Completes the PDF review slice end-to-end. After this commit a user can open a fixture, select text, save an evidence item with commentary, see it in the sidebar, reload the page, click the item, and the viewer scrolls to the passage. - T03 src/source/pdf/{fingerprint,extract,ingest}.ts + 39 fixture tests - SHA-256 fingerprint over a fresh ArrayBuffer (TS BufferSource-safe) - PDF.js text extract; per-page normalize then join with "\n\n" - PageMap + OffsetMap (gap-free coverage); pageLength = end - start - Updated manifest's Betriebskosten quote to one PDF.js extracts cleanly - T04 src/anchor/selectors/{create,resolve}.ts + 25 unit + 7 fixture tests - createSelectors emits the maximal redundant set (TextQuote + TextPosition + PdfRect + PdfPageText when available) - resolveSelectors implements the SharedContracts §7 ladder; confidence 1.0 (pos+quote) → 0.7 (rect-only) → 0 (unresolved) - Cross-module integration test moved to tests/integration/ to honor the anchor↛source boundary lint rule - T05 engine: sync event bus over the closed §4 vocabulary, Map-backed repos, services, createEngine() composition root, 12 tests - T06 work + app: three-pane shell (CollectionList | ViewerShell | EvidenceSidebar) wired through EngineProvider; EngineContext lives in src/work/ to respect the work↛app boundary; SpikeApp deleted - T07 AnnotationToolbar: pendingSelection in context; Save runs createSelectors → engine.annotations.create → engine.evidence.create - T08 click-to-reopen + localStorage persistence - scrollToAnnotation state in context with a version counter so a second click on the same item re-fires the viewer scroll - captureSnapshot/restoreSnapshot/attachPersister/restoreFromStorage; restore bypasses services to avoid event-loops - active-document id persisted alongside the snapshot so reload lands on the same fixture; ADR-0005 written - 9 persistence tests - T09 tests/integration/app-prd-scenario.dom.test.tsx - end-to-end happy-dom test of PRD scenario steps 1-8 through the real React tree; viewer + ingest mocked per ADR-0004's headless-Chromium limitation. Fixed memo-deps bug in EvidenceSidebar/ViewerShell where useEngineEventTick values were not included in the useMemo deps, leaving stale memoization across event-driven re-renders - vitest.config.ts: happy-dom for *.dom.test.{ts,tsx} files - noEmit added to tsconfig so tsc -b doesn't litter src/ with .js outputs Gates: typecheck ✓ lint ✓ test 109/109 across 11 files ✓ build ✓ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:58:11 +02:00
);
}