rag-service/openspec/changes/archive/2026-09-21-ocr-ingest-integration/apply-progress.md

588 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Apply Progress: OCR Ingest Integration
## Current State
- **Mode:** Strict TDD
- **Delivery:** Feature-branch-chain; Units 1–13 implemented as bounded slices; production acceptance remains pending
- **Completed tasks:** 1.1–1.4, 2.1–2.5, 3.1–3.4, 4.1–4.5, 5.1–5.4, 6.1–6.4, 7.1–7.3
- **Overall task progress:** 29/30 complete
## Unit 1: Migration
### Implementation Summary
- Added `migrations/002_ocr_review.sql` with durable OCR jobs, auditable page records, atomic review correction targets, and a partial unique index for non-terminal OCR identities.
- Added focused schema contract coverage in `tests/catalog/migration-002.test.ts`.
- Preserved `migrations/001_knowledge_lifecycle.sql` and all repository logic unchanged.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 1.2 | `tests/catalog/migration-002.test.ts` | Schema contract | N/A (new files) | Valid RED: 4/4 failed with `ENOENT` because migration 002 did not exist. An earlier syntax-invalid run was corrected and is not counted as RED. | 4/4 passed with migration 002 present. | 4 independent schema behaviors cover jobs, pages, corrections, and pending identity uniqueness. | None needed; SQL already matches the minimal authoritative contract. |
### Test Summary
- **Total tests written:** 4
- **Total tests passing:** 4
- **Layers used:** Schema contract (4)
- **Approval tests:** None — no existing production file was modified
- **Pure functions created:** 0
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `npx --no-install tsx --test tests/catalog/migration-002.test.ts` — exit 0; 4 tests, 4 passed, 0 failed. |
| Runtime harness command/scenario and exact result | N/A — the task forecast defines Unit 1 as schema-only, and the project has no PostgreSQL test harness or database-test dependency. Applying a live migration would require external database access outside this unit. |
| Rollback boundary | Delete `migrations/002_ocr_review.sql` and `tests/catalog/migration-002.test.ts`, revert task 1.2 to pending, and remove this Unit 1 progress/history entry. No existing migration or repository behavior must be reverted. |
### Additional Validation
- `npm run check` — exit 0.
- `git diff --check` — exit 0 after removing one trailing-space warning introduced in the history header.
### Deviations
None — the migration follows the proposal, specifications, design, and closed OCR persistence contract.
### Remaining Persistence Tasks
- [x] 1.1 Complete repository-level RED coverage for duplicate pending ingestion, lease recovery, and review transitions in Unit 2.
- [x] 1.2 Add migration 002 schema and pending OCR identity unique index.
- [x] 1.3 Add catalog repository candidates and leases in Unit 2.
- [x] 1.4 Refactor persistence code after Unit 2 reaches green.
### Review Boundary
- **Start:** Migration 001 already supplies lifecycle states and parent catalog tables.
- **End:** Migration 002 and its focused schema contract test are green.
- **Out of scope:** Repository methods, dispatch behavior, runtime PostgreSQL migration, Unit 2, commits, pushes, PRs, deployment, and secrets.
- **Rollback:** Remove only the two Unit 1 implementation files and their progress metadata.
- **Authored review budget:** 225 additions/deletions, below the 400-line budget for this autonomous slice.
## Unit 2: Repository
### Implementation Summary
- Added pending OCR identity lookup, atomic queue claiming, expired-lease recovery, and guarded review transitions to `CatalogRepository`.
- Added a focused fake-pool repository harness that covers duplicate reuse, empty queues, remote identity preservation, and invalid transitions.
- Corrected the canonical test script so it executes both root-level and nested test suites.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 1.1 | `tests/catalog/repository-ocr.test.ts` | Unit/fake pool | 4/4 existing tests passed | 0/6 with methods absent; then 5/6 while each completion gate was missing | 6/6 passed after task 1.3 | Found/not-found candidates, claimed/empty queue, expired lease, valid/invalid transition | Covered by 1.4 |
| 1.3 | `tests/catalog/repository-ocr.test.ts` | Unit/fake pool | 4/4 existing tests passed | Tests preceded production code | 6/6 passed | Multiple inputs and state paths exercised | Shared OCR row mapping extracted only after green |
| 1.4 | `tests/catalog/repository-ocr.test.ts` | Approval refactor | 6/6 before refactor | N/A — behavior-preserving approval baseline | 6/6 after refactor | Existing six cases preserved | Reusable aliased-column projection removed SQL duplication |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/catalog/repository-ocr.test.ts` — exit 0; 6 tests passed, 0 failed. `NODE_ENV=test npx --no-install tsx --test tests/catalog/*.test.ts` — exit 0; 10 tests passed, 0 failed. |
| Runtime harness command/scenario and exact result | `NODE_ENV=test npx --no-install tsx --test tests/*.test.ts` — exit 0; 25 root tests passed, 0 failed. Canonical `npm test` — exit 0; 35 total tests passed, 0 failed, proving the 25 root and 10 nested tests execute together. |
| Rollback boundary | Revert Unit 2 changes in `src/modules/catalog/repository.ts`, delete `tests/catalog/repository-ocr.test.ts`, and revert the `package.json` test script; Unit 1 migration remains intact. |
### Validation and Boundary
- Corrective rerun for failed evidence revision `sha256:9433c0cd6628f36f4c96a27d41c6d429f4303d038ffe928f297367c274f21c2f`; fresh evidence revision `sha256:a19119f84381447f54a6223e2a203acaf9d3ae08b6ae8f585e3bd52cd510909b`.
- The failed canonical script expanded only nested tests after nested suites appeared. The minimal correction is `NODE_ENV=test tsx --test tests/*.test.ts tests/**/*.test.ts`.
- Focused repository suite passed 6/6; all catalog suites passed 10/10; explicit root suites passed 25/25; canonical `npm test` passed 35/35; `npm run check` and `git diff --check` exited 0.
- Native Unit 2 accounting was 511 changed lines. The maintainer approved `size:exception` exclusively for Unit 2; the corrective attempt has a separate 600-line remediation budget. All later units retain the normal 400-line budget.
- No design deviations. No Unit 3 work was started.
### Corrective TDD Evidence
| Correction | Safety Net / RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|
| Canonical test discovery | Failed evidence revision proved canonical `npm test` executed only 10 nested tests while the explicit root command executed 25 tests. | Updated only the package test argument list; canonical `npm test` passed 35/35. | Direct root 25/25 and nested catalog 10/10 results sum to and identify the canonical 35/35 run. | None needed — one package-script argument was added. |
## Unit 3: Extraction
### Implementation Summary
- Added a Node 22 fixture spike proving `pdf-parse` page callbacks backed by bundled PDF.js `2.0.550` preserve ordered pages, including a blank page.
- Added `parsePdfPages` with one-based page identity, raw native text, stable SHA-256 hashes, and fail-closed complete-page accounting.
- Preserved textual ingestion as data-only, excluded `CMakeLists.txt`, MDX, and shell lookalikes, and kept task 2.2 pending for Unit 4 detection/composition assertions.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 2.1 | `tests/parsers/pdf-pages.test.ts`; `scripts/spike-pdfjs.ts` | Runtime spike | 20/20 lifecycle service tests passed | Focused suite failed 0/1 because `parsePdfPages` did not exist; the spike and parser implementation were still absent. | Node 22.14.0 spike returned pages 1–3 exactly, including blank page 2. | Three pages prove non-empty, blank, and later-page ordering paths. | Replaced the over-budget direct dependency with the design-approved existing `pdf-parse` pagerender path; spike remained green. |
| 2.3 | `tests/parsers/pdf-pages.test.ts` | Unit/integration fixture | 20/20 lifecycle service tests passed | Focused suite failed 0/1 on the missing export before production code. | Final focused run passed 5/5 after implementation. | Covered page hashes/blank page, native PDF composition, non-PDF rejection, data-only text parsing, and unsupported lookalikes. | Focused suite remained 5/5 after consolidating on the existing parser dependency. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/parsers/pdf-pages.test.ts` — exit 0; 5 passed, 0 failed. |
| Runtime harness command/scenario and exact result | `NODE_ENV=test npx --no-install tsx scripts/spike-pdfjs.ts` — exit 0 on Node 22.14.0; PDF.js 2.0.550 returned three ordered pages with page 2 blank. |
| Rollback boundary | Revert `src/modules/parsers/parser-registry.ts`; delete `scripts/spike-pdfjs.ts`, `tests/parsers/pdf-pages.test.ts`, and `tests/fixtures/ocr/native-three-pages.pdf`; revert tasks 2.1/2.3 and this Unit 3 progress/history entry. Units 1–2 remain intact. |
### Validation and Boundary
- Canonical `npm test` passed 40/40; `npm run check` and `git diff --check` exited 0.
- Native authored slice before progress/history metadata: 225 additions/deletions; no package or lockfile delta remains.
- Start: Unit 2 persistence is complete. End: per-page native extraction and parser threat routing are green.
- Out of scope: detection thresholds, OCR selection, composition, risks, Unit 4 tasks 2.4/2.5, commits, pushes, PRs, deployment, and restricted paths.
- No specification deviation. The design-authorized `pdf-parse` pagerender fallback was selected only after its Node 22 fixture spike passed.
## Unit 4: Detection and Composition
### Implementation Summary
- Added pure per-page native detection under `pdf-detection-v1`, exact OCR page selection, verified-blank classification, and fail-closed OCR quality gates.
- Added deterministic candidate composition using exactly one source per page, ordered OCR lines, blank omission, canonical hashes, and unchanged risk tokens prioritized for review.
- Completed the remaining task 2.2 scenarios by combining the Unit 3 data-only/parser threat coverage with Unit 4 mixed-PDF, detection, blank, hash, and risk RED assertions.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 2.2 | `tests/ocr/detection.test.ts`; `tests/parsers/pdf-pages.test.ts` | Unit/integration fixture | Unit 3 parser suite passed 5/5 before edits. | Focused Unit 4 suite failed before production with `ERR_MODULE_NOT_FOUND` for `src/modules/ocr/detection.js`. | Focused suite passed 5/5 after detection and composition were added. | Real generated mixed PDF selected pages 2 and 3; `.txt`/`.md` stayed data-only and CMake/MDX/shell lookalikes stayed unsupported. | None needed; routing remains separated between parser support and the pure PDF OCR planner. |
| 2.4 | `tests/ocr/detection.test.ts` | Unit | N/A (new production file) | Threshold, mixed-page, blank, and quality assertions preceded production code. | Exact native, blank, and OCR quality boundaries passed in the 5/5 focused suite. | Every native and OCR threshold has a passing boundary plus a failing neighbor; visible ink with empty OCR is blocked. | Pure functions and named policy version were present at green; final focused suite remained 5/5. |
| 2.5 | `tests/ocr/detection.test.ts` | Unit | N/A (new production file) | Ordered composition, blank omission, canonical hash, audit-source, and risk assertions preceded production code. | Deterministic composition and risk prioritization passed in the 5/5 focused suite. | Reordered pages, tied OCR boxes, blank middle page, repeated token, and four known ambiguous tokens exercise distinct paths. | Reused shared canonical JSON and SHA-256 utilities; final focused suite and type check remained green. |
### Test Summary
- **Tests:** 5 written and focused-passing; 45 canonical (unit 4, integration fixture 1)
- **Approval tests:** None — new production files; **pure functions created:** 6
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/ocr/detection.test.ts` — exit 0; 5 tests passed, 0 failed. |
| Runtime harness command/scenario and exact result | `NODE_ENV=test npx --no-install tsx --test --test-name-pattern "parsed mixed PDF" tests/ocr/detection.test.ts` — exit 0; 1 test passed, 0 failed. The harness generated a three-page PDF, parsed it through the Node 22 `pdf-parse` page callback, and selected only blank page 2 and insufficient page 3 for OCR. |
| Rollback boundary | Delete `src/modules/ocr/detection.ts`, `src/modules/ocr/composition.ts`, and `tests/ocr/detection.test.ts`; revert tasks 2.2/2.4/2.5 and this Unit 4 progress/history entry. Units 1–3 and their parser threat coverage remain intact. |
### Validation and Boundary
- Canonical `npm test` passed 45/45; `npm run check`, tracked `git diff --check`, and no-index whitespace checks for all three new files exited 0.
- Authored Unit 4 code and tests total 322 additions and 0 deletions, below the 400-line budget with no size exception.
- Start: Unit 3 page extraction and parser threat routing are complete. End: detection, blank/quality gates, deterministic composition, hashes, and risk tokens are green.
- Out of scope: OCR service, client, dispatcher, ingest routing, review APIs, commits, pushes, PRs, deployment, and restricted paths.
- No specification or design deviation.
## Unit 5: OCR Service
### Implementation and Correction Summary
- Added pinned Unit 5 FastAPI test dependencies and retained the ignored `ocr-service/.venv` environment.
- Added RED-first HTTP tests and a SQLite-backed private API for bearer auth, idempotency, allowlisting, upload/page limits, queue pressure, status, deletion, and health.
- Preserved failed evidence `sha256:4a5c8ed8d8b302f7ff654d4f40fe0f13049bfdd29db4c76576f3847c0a9be15a`: its harness returned `401/400/400`; inspection showed `&` backgrounded the preceding shell AND-list, so request variables existed only in the child shell and authenticated curls sent an empty `request` form, reproduced as `400 INVALID_REQUEST`.
- Corrected only the harness command boundary; the 381-line candidate implementation remained unchanged before progress persistence.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 3.1 | `ocr-service/tests/test_api.py` | HTTP integration | N/A (new service) | Collection failed with `ModuleNotFoundError: No module named 'app'`; failed localhost runtime evidence then supplied correction RED. | 8/8 focused tests and corrected curl `401/202/409` passed. | Auth variants, idempotency/conflict, allowlist, limits, pressure, integrity, and real multipart requests exercised distinct paths. | No production refactor: the defect was shell variable scope in the harness. |
| 3.2 | `ocr-service/tests/test_api.py` | HTTP integration/SQLite | N/A (new service) | Same missing-service RED covered the API; failed harness blocked completion. | 8/8 focused tests and real Uvicorn harness passed. | Ready/not-ready health, queue capacity, status, deletion, and process cleanup exercised distinct paths. | Pure page hashing and centralized terminal errors retained; no correction needed. |
### Work Unit Evidence
- Focused: `ocr-service/.venv/bin/python -m pytest ocr-service/tests -k auth` — exit 0; 8 passed, 0 failed (one dependency deprecation warning).
- Runtime: local Uvicorn plus multipart curl — unauthorized `401` with `UNAUTHORIZED`, accepted `202` with queued identity, conflicting same key `409` with `IDEMPOTENCY_CONFLICT`.
- Gates: `npm test` 45/45, `npm run check`, Python `compileall`, tracked and untracked whitespace checks — all exit 0/equivalent clean.
- Cleanup: Uvicorn PID 268425 terminated and absent; temporary PDF, SQLite DB, and log removed; `.venv` retained.
- Rollback: remove `.gitignore` Unit 5 entries and `ocr-service/`, then revert tasks 3.1/3.2 and this Unit 5 progress/history block; Units 1–4 remain intact.
- Fresh evidence revision: `sha256:0d8b2c57bbab967473eaf99a7ca900168554e64254fa80197b3626b4570b58d0`; corrected final authored change count: 397 additions plus deletions, within 400.
## Unit 6: OCR Rendering and Image
### Implementation Summary
- Added deterministic 200 DPI PDF page rendering with a pre-render 25-megapixel guard, a PaddleOCR adapter, stable line identities, quality metrics, and the exact result schema.
- Added a deterministic fake-driven test seam and preserved ambiguous OCR tokens without correction.
- Added the private CPU image with PaddleOCR `3.4.0`, PaddlePaddle CPU `3.2.2`, baked Latin/mobile models, one Uvicorn worker, a non-root user, private job storage, healthcheck, and documented 3 CPU/5 GiB deployment limits.
- Added a Dockerfile-specific deny-by-default build context so restricted and unrelated repository paths are never sent to the Docker daemon.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 3.3 | `ocr-service/tests/test_render.py` | Unit/integration fixture | Existing OCR API suite passed 8/8 before production edits. | Focused collection failed because `app.engine` did not exist. | Focused render suite passed 5/5; complete OCR suite passed 13/13. | Selected pages 1/3, invalid page and pixel-limit paths, repeatable fake results, and Paddle payload normalization exercise distinct behavior. | Named constants and pure metric/result transforms retained; focused suite remained 5/5. |
| 3.4 | `ocr-service/tests/test_render.py` | Static/container runtime | Existing OCR API suite passed 8/8 before production edits. | Docker contract was absent; deny-by-default context triangulation then failed until `Dockerfile.dockerignore` existed. | Image built with baked models; offline constrained container rendered and validated PNG/result output. | Static pin/runtime assertions plus real model build and `--network none` execution cover configuration and runtime paths. | Added missing OpenCV runtime libraries after the first build exposed `libGL.so.1`; exact corrected image passed. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `ocr-service/.venv/bin/python -m pytest ocr-service/tests -k render -q` — exit 0; 5 passed, 8 deselected, 1 dependency deprecation warning. |
| Runtime harness command/scenario and exact result | `docker build --file ocr-service/Dockerfile --tag rag-ocr-service:unit6 .` — corrected exact image exited 0 and baked both models. A `docker run --rm --network none --cpus 3 --memory 5g ...` harness loaded cached PaddleOCR 3.4.0/PaddlePaddle 3.2.2 models, rendered a 1700x2200 PNG (23,309 bytes), returned schema `1`, one page, and one OCR line; host Pillow reopened it as PNG 1700x2200. |
| Rollback boundary | Remove `ocr-service/{Dockerfile,Dockerfile.dockerignore,app/engine.py,app/models.py,app/render.py,tests/test_render.py}` and revert Unit 6 changes in `app/main.py`, `requirements.txt`, and `README.md`; Units 1–5 remain intact. |
### Validation and Boundary
- RED was followed by GREEN and post-refactor reruns. One expected GREEN iteration corrected a test-side non-whitespace count from 16 to the mathematically correct 15.
- The first image build failed on missing `libGL.so.1`; adding the required slim-image runtime libraries produced a successful build and runtime harness. A later optional connectivity-check refactor rebuild exhausted Docker storage; that unvalidated one-line refactor was reverted, the validated Dockerfile bytes were restored, and the image tag was removed during cleanup.
- `npm test` passed 45/45; `npm run check`, Python `compileall`, tracked/untracked whitespace checks, and the full OCR pytest suite (13/13) passed.
- Start: Unit 5 private queue/API is complete. End: Unit 6 render/engine/schema/container behavior is complete. Unit 7 client work was not started.
- Native Unit 6 implementation/tests/docs are 397 authored additions plus deletions, within the 400-line budget; required SDD progress and workspace history metadata are administrative evidence outside that native slice.
- No specification or design deviation. The Docker image is private by deployment contract; CPU/RAM labels document limits that the platform must enforce.
## Unit 7: OCR Client and Durable Artifacts
### Implementation Summary
- Added a private OCR HTTP client that preserves the exact idempotency key across one initial submission and at most two transient resubmissions, treats `429` as retryable queue pressure without immediate resubmission, and treats deterministic failures as terminal.
- Added 2–15 second polling backoff plus strict acknowledgement, status, engine, job, document, ordered-page, metrics, line, and schema validation before a result can be persisted.
- Added durable `0600` original and canonical manifest writes, UUIDv5 document artifact identities, lexical path containment, verifiable hashes, and age-gated orphan sweeping that preserves retained, recent, non-UUID, and symlink entries.
- Left task 4.1 pending because Unit 7 proves only its retries/integrity clause; Unit 8 still owns flag-off `201`, OCR `202`/status, OCR-down fail-closed, and catalog-down `503` HTTP coverage.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 4.2 | `tests/ocr/client.test.ts` | Unit/runtime fetch stub | Existing OCR detection suite passed 5/5; production file was new. | Focused suite failed with `ERR_MODULE_NOT_FOUND` for `src/modules/ocr/client.js` before production code existed. | Final focused suite passed 6/6; runtime retry scenario passed 1/1. | Network then `503` produced exactly three same-key attempts with 2s/4s backoff; `429` and `422` each stopped after one attempt; malformed acknowledgement/schema/pages failed integrity; polling used 2s/4s. | Adapted multipart bytes to a type-safe `Uint8Array`; focused suite and type check remained green. |
| 4.3 | `tests/ocr/client.test.ts` | Filesystem integration | Existing OCR detection suite passed 5/5; production file was new. | The same missing-module RED preceded artifact production code. | Private originals, canonical manifest, safe resolution, and sweep passed in the final 6/6 suite. | Two out-of-order documents proved canonical ordering/hashes; traversal was rejected; sweep removed only an old orphan while preserving retained/recent directories and a symlink. | A test-side assertion was corrected to inspect the retained dangling symlink with `lstat`; no production behavior changed, and the suite passed 6/6. |
### Test Summary
- **Tests written and passing:** 6 focused tests; unit/fetch-stub and filesystem integration layers.
- **Approval tests:** None — both production modules are new.
- **RED:** exit 1, module-not-found before either production file existed.
- **GREEN/REFACTOR:** exit 0, 6 passed, 0 failed.
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/ocr/client.test.ts` — exit 0; 6 passed, 0 failed. |
| Runtime harness command/scenario and exact result | `NODE_ENV=test npx --no-install tsx --test --test-name-pattern "runtime retry stub" tests/ocr/client.test.ts` — exit 0; 1 passed, 0 failed. Deterministic stub observed network failure, `503`, then `202`: exactly 3 submission attempts, one unchanged idempotency key, 2,000/4,000 ms backoffs, followed by a strictly validated two-page result. |
| Rollback boundary | Remove `src/modules/ocr/client.ts`, `src/modules/ocr/artifacts.ts`, and `tests/ocr/client.test.ts`; revert tasks 4.2/4.3 and this Unit 7 progress/history metadata. Units 1–6 remain intact. |
### Validation and Boundary
- `npm test` passed 51/51; `npm run check`, `npm run build`, tracked whitespace, and no-index whitespace checks for all three new files passed.
- Test-created `rag-ocr-artifacts-*` and `rag-ocr-sweep-*` directories were removed; no server, external OCR call, or persistent process was started; `ocr-service/.venv` was preserved.
- Start: Unit 6 OCR rendering and container runtime are complete. End: Unit 7 client retries/integrity and durable artifacts/orphan sweep are implemented and green.
- Out of scope and untouched: dispatcher, reconciler, ingest branch, app upload/status routes, Unit 8 tests, commits, pushes, PRs, native review, deployment, and restricted paths.
- The implementation/test slice is 506 authored additions (client 174, artifacts 129, tests 203). This exceeds the 400-line default after one honest cohesive assessment; no code-golf or second slicing pass was attempted. The maintainer explicitly approved `size:exception`, including 585 total native-accounting lines with required metadata.
- No specification or design deviation.
### Approved Size-Exception Corrective Context
- The native attempt was reset after failed evidence revision `sha256:dd224c3864adc8380dc7d9fd147cb448f5da8138277f3f346a35d7eeb3aca158`; the failure was budget-only, not a functional defect.
- Corrective work unit `unit-7-size-exception-validation` uses attempt token `sha256:0109cfd8d3b37a5672c65a37700269b89cb80e4e357a39f68a920779a1715c39` and must settle against the failed revision through the parent.
- Production and test files remain unchanged for the corrective rerun; only approval/reset evidence and fresh validation metadata may change.
- Task 4.1 remains pending, while tasks 4.2 and 4.3 remain complete. Unit 8 was not started.
## Unit 8: Dispatcher and Runtime Orchestration
- Completed multi-document progress, uniqueness-race reuse, exact lease recovery, persisted-key dispatch, default runtime wiring, authenticated status, and fail-closed retrieval; no activation or embeddings occur on OCR failure.
### Evidence Chain and Review Accounting
- **Complete review slice:** Relative to committed HEAD `ac046e3`, Unit 8 contains 1,045 functional changed lines plus 41 required metadata lines, for 1,086 total changed lines. The complete slice is not within 400 lines, and no `size:exception` was approved or recorded for it.
- **Interrupted preserved candidate:** Native failed/interrupted revision `sha256:182cc73954a8518d03bd6c3ef5f4141b352aa9b69fca4177447d76deaddad223` contained 815 changed lines.
- **Auto-chain reset:** The maintainer-selected preflight strategy was `auto-chain` with `feature-branch-chain`. Native reset revision `sha256:c0db82cf3db8ae790d34e4c310b998f97bb36864c97c3aa84e4c87ab77d07c2a` preserved the interrupted candidate and scoped the next bounded recovery objective only to additional changes; it did not approve the complete Unit 8 slice as a size exception.
- **Recovery delta:** After that reset, the bounded recovery added 350 functional changed lines plus 41 required metadata lines, for 391 total changed lines within the separate 400-line recovery objective.
- **Passed recovery:** Fresh passed evidence revision `sha256:e424afe7b2f8efd547dcfd0baf64ec1b46ac344f39afd87259560a094d59163f` closes the interrupted → auto-chain reset → passed recovery chain.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 4.1 | `tests/ocr/{dispatcher,client}.test.ts` | HTTP/integration | Recovered baseline 7/7 passed; not labeled RED | New scenarios produced genuine 5/9 RED; catalog `/retrieve` already failed closed | Unit 8 9/9; client 6/6 | 201/202/status, OCR failure, 503 retrieval, retries/integrity | Consolidated persisted-key assertion; 9/9 |
| 4.4 | `tests/ocr/dispatcher.test.ts` | Unit/integration | Recovered baseline 7/7 | Exact recovery failed by claiming unrelated work | 9/9 | Live/new queue separation plus periodic dispatch | Exact-job dispatch helper; 9/9 |
| 4.5 | `tests/ocr/dispatcher.test.ts` | HTTP/filesystem | Recovered baseline 7/7 | Runtime wiring RED was 8/10 | 9/9 | Multipart upload, durable reload, duplicate race, mixed documents | Shared queue drain; 9/9 |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test | `NODE_ENV=test npx --no-install tsx --test tests/ocr/dispatcher.test.ts` — 9 passed, 0 failed. |
| Focused repository OCR test | `NODE_ENV=test npx --no-install tsx --test tests/catalog/repository-ocr.test.ts` — 6 passed, 0 failed. |
| Canonical Node test | `npm test` — 60 passed, 0 failed. |
| Runtime harness | Bounded localhost curl — ingest `202/ocr_queued`; authenticated status `200/ocr_queued`; leaked child from initial shell cleanup was detected, terminated, and port/temp cleanup reverified. |
| Rollback boundary | Revert Unit 8 deltas in `src/{app,config/env}.ts`, catalog/ingest/client modules, `src/modules/ocr/dispatcher.ts`, and its test; preserve Units 1–7. |
## Unit 9: Review and Indexing Core
- Added authenticated review/approval routes with injectable service boundaries, immutable correction preparation, stale/duplicate conflict guards, and new/reusable activation CAS behavior. Rejection transition and playground UI remain Unit 10.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 5.1 | `tests/ocr/review.test.ts` | HTTP/unit | App suites 9/9 and 20/20 | Missing `indexing.js`; exit 1 | 5/5 | Unauthorized/premature, candidate/line/active staleness, duplicate targets, new/reusable races, rejected no-index | Assertions remained 5/5 |
| 5.2 | `tests/ocr/review.test.ts` | HTTP/unit | 29/29 | Tests preceded `review.ts` | 5/5 | Audit view plus two corrections and four conflict paths | Clone prevents failed commits mutating loaded candidates; 5/5 |
| 5.3 | `tests/ocr/review.test.ts` | Unit | N/A (new file) | Tests preceded `indexing.ts` | 5/5 | New/reusable true/false, both CAS races, rejected no-index | Named CAS error mapping; 5/5 |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test | `NODE_ENV=test npx --no-install tsx --test tests/ocr/review.test.ts` — exit 0; 5 passed, 0 failed. |
| Runtime harness | Same runner with `--test-name-pattern "review HTTP rejects"` — exit 0; 1 passed; real localhost Express requests returned authenticated boundary outcomes without external services. |
| Rollback boundary | Remove `src/modules/ocr/{review,indexing}.ts` and `tests/ocr/review.test.ts`; revert only the Unit 9 route additions in `src/app.ts` and Unit 9 metadata. Preserve Units 1–8. |
### Validation and Boundary
- Canonical `NODE_ENV=test npm test` passed 65/65; `npm run check`, `npm run build`, tracked whitespace, and no-index whitespace checks passed.
- Unit 9 adds 341 functional changed lines and 38 metadata changed lines, 379 total, within the 400-line budget. No live PostgreSQL, OpenAI, Qdrant, or OCR process was used; no test process remained.
- Start: Unit 8 dispatch/status is complete. End: review/correction and indexing/CAS ports plus authenticated routes are green. Unit 10 task 5.4 remains unchecked.
## Unit 10: Rejection and Review UI
- Added authenticated rejection with current candidate-hash and state guards, required audit fields, durable store completion before success, and no indexing or activation call.
- Extended the existing static playground with authenticated candidate loading, protected page images, native/raw/candidate comparisons, line confidence/boxes, corrections, risks, approval, and rejection controls.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 5.4 | `tests/ocr/review.test.ts` | HTTP integration/static HTTP | Existing review suite passed 5/5 before edits. | Genuine RED passed only 5/7: rejection returned 404 instead of authenticated 401 and the playground lacked the review contract. A malformed-body edge then returned 409 instead of 400. | Focused suite passed 7/7 after minimal rejection and UI implementation. | Unauthorized, malformed, stale-hash, successful, repeated-state, durable-status, and static audit-field paths exercise distinct outcomes. | Shared UI decision-state helper extracted; focused suite remained 7/7. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/ocr/review.test.ts` — exit 0; 7 passed, 0 failed. |
| Runtime harness command/scenario and exact result | `NODE_ENV=test npx --no-install tsx --test --test-name-pattern "authenticated rejection" tests/ocr/review.test.ts` — exit 0; 1 passed, 0 failed. Real localhost Express requests returned 401/400/409/200, persisted the rejected audit state through the injected store, retrieved rejected status, and made zero indexing calls. |
| Rollback boundary | Revert Unit 10 deltas in `src/app.ts`, `src/modules/ocr/review.ts`, `tests/ocr/review.test.ts`, and `public/playground/{index.html,app.js,styles.css}` plus Unit 10 metadata; preserve Units 1–9. |
### Validation and Boundary
- Canonical `NODE_ENV=test npm test` passed 67/67; `npm run check` and `npm run build` exited 0.
- Previously observed tracked `git diff --check` exited 0; no-index whitespace checks for Unit 10's untracked `src/modules/ocr/review.ts` and `tests/ocr/review.test.ts` exited 0.
- Unit 10 adds 271 functional and 42 metadata changed lines, 313 total, below the 400-line budget. No live database, embedding, vector, OCR, or external service was used; no persistent process was started.
- Start: Unit 9 review/indexing ports and approval routes are green. End: rejection isolation and the static review UI are green. Retention, deploy, OpenAPI, and E2E tasks remain untouched.
- No specification or design deviation.
## Unit 11: Retention and Flag-Off Isolation
### Implementation Summary
- Added a retention boundary that expires 30-day reviews before deletion, retains failed/rejected artifacts for 7 days, and retains superseded OCR artifacts for 30 days.
- Added exact state/artifact CAS claims, active-pointer checks, activation exclusion after a deletion claim, and restart-safe `retention_deleting→retention_deleted` completion.
- Wired retention into the existing exclusive reconciler cycle and made status/review/decision APIs return `404` without store access while OCR is disabled; native synchronous ingestion remains unchanged.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 6.1 | `tests/ocr/retention.test.ts` | Repository/HTTP/filesystem integration | Existing repository, dispatcher, and review suites passed 22/22. | Genuine missing-module RED failed before `retention.ts`; activation-race RED then failed 0/1 with “Missing expected rejection.” | Focused suite passed 6/6. | Exact TTL SQL, active pointer/state guards, flag-off `201/404`, and deletion-claim activation conflict exercise independent paths. | No production refactor was needed; two test-side expectations were corrected during GREEN without weakening behavior. |
| 6.2 | `tests/ocr/retention.test.ts` | Filesystem/reconciler integration | Same 22/22 safety net. | The missing-module RED preceded all production retention code. | Focused suite passed 6/6; relevant regression set passed 28/28. | Review expiry, terminal deletion, active preservation, simulated restart, repeat no-op, and exclusive reconciler execution cover distinct paths. | Named store boundary keeps filesystem behavior isolated; final focused suite remained 6/6. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/ocr/retention.test.ts` — exit 0; 6 passed, 0 failed. |
| Runtime harness command/scenario and exact result | The focused suite used real temporary directories and localhost Express requests: interrupted deletion removed only the claimed directory, resumed to `retention_deleted`, repeated as a no-op, preserved the active directory, returned native `201`, and returned `404` for all disabled OCR candidate APIs without store access. |
| Rollback boundary | Remove `src/modules/ocr/retention.ts` and `tests/ocr/retention.test.ts`; revert only Unit 11 seams in `src/app.ts`, catalog repository/reconciler, and explicit enabled-state setup in dispatcher/review tests. Preserve Units 1–10. |
### Validation and Boundary
- Relevant regression suites passed 28/28 after explicitly enabling OCR in pre-existing enabled-path tests; the initial 19/22 run exposed only that test-fixture assumption.
- Canonical `NODE_ENV=test npm test` passed 73/73; `npm run check`, `npm run build`, tracked whitespace, and no-index whitespace checks for all seven intended untracked files passed.
- Temporary `rag-retention-*` directories were removed and no `tsx --test` or Node test process remained. No live database, OCR, embedding, vector, or external service was used; `ocr-service/.venv` was preserved.
- Unit 11 adds 277 functional changed lines and 50 metadata changed lines, 327 total, below the 400-line budget.
- Start: Unit 10 rejection/UI is complete. End: tasks 6.1–6.2 are complete. OpenAPI, deployment/configuration, E2E, production, commit, push, and PR work remain untouched.
- No specification or design deviation.
## Unit 12: Contracts and Deployment Wiring
### Implementation Summary
- Documented OCR `202` ingestion variants and authenticated status, review, correction/approval, and rejection routes with behavior-matched success and error responses.
- Added progress, review evidence, optimistic correction, decision, and structured error schemas while preserving the legacy-disabled `202` variants.
- Added private durable artifact defaults and OCR limits/timeouts; the root image now declares `/data/ingestions`, runs as `node`, and applies catalog migrations before startup without embedding secrets.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 6.3 | `tests/ocr/contracts-deploy.test.ts` | OpenAPI contract | Existing lifecycle/dispatcher/review/retention suites passed 42/42. | Genuine initial RED was 0/3: OCR `202` referenced the native schema and OCR routes/schemas were absent. | Focused suite passed 3/3. | A second RED required legacy-disabled `202` variants and accurate help authentication; final suite remained 3/3. | Shared schema references kept nested contracts reviewable; no behavior changed. |
| 6.4 | `tests/ocr/contracts-deploy.test.ts` | Config/static container | Same 42/42 safety net. | Initial RED found the old relative artifact root, absent limits/timeouts, volume, non-root user, and migration-aware command. | Focused suite passed 3/3; sanitized Docker build and image inspection passed. | A second RED required explicit runtime artifact-root wiring; final suite remained 3/3. | No further refactor needed. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test command and exact result | `NODE_ENV=test npx --no-install tsx --test tests/ocr/contracts-deploy.test.ts` — exit 0; 3 passed, 0 failed. |
| Relevant regression command and exact result | Lifecycle, dispatcher, review, retention, and contract suites — exit 0; 45 passed, 0 failed. |
| Runtime harness | N/A — Unit 12 is static-only by task forecast. A sanitized-context Docker build succeeded and inspection proved user `node`, durable volume, production artifact root, and migration-before-server command. |
| Rollback boundary | Remove `tests/ocr/contracts-deploy.test.ts` and revert only Unit 12 deltas in `src/api/openapi.ts`, `src/config/env.ts`, and `Dockerfile`; preserve Units 1–11. |
### Validation and Boundary
- Canonical `NODE_ENV=test npm test` passed 76/76; `npm run check`, `npm run build`, tracked whitespace, and all intended-untracked whitespace checks passed.
- Docker build `rag-service:unit12-contract` succeeded from a sanitized 430.31 kB context; inspection matched the deployment contract. The image and temporary context were removed, no test process remained, and `ocr-service/.venv` was preserved.
- Unit 12 adds 219 functional lines and 50 metadata lines, 269 total, below the 400-line budget.
- Start: Unit 11 retention is complete. End: tasks 6.3–6.4 are complete. Unit 13 / all 7.x E2E and production work remains untouched.
- `docs/CONTRATO_CICLO_VIDA_Y_OCR.md` was read as authoritative and remained unchanged. No specification or design deviation.
## Unit 13: Local Deterministic E2E and Complete Gate
### Implementation Summary
- Added a bounded localhost HTTP suite covering native `201`, scanned `202 → review → approve → active`, and exact mixed native/OCR page reporting.
- Added resend identity reuse, fail-closed OCR exhaustion, and catalog-unavailable `503` coverage without external services.
- Made no production-code changes; the new scenarios passed immediately as baseline/characterization evidence, so no RED was fabricated.
### TDD Cycle Evidence
| Task | Test File | Layer | Safety Net | RED / Baseline | GREEN | TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|---|---|
| 7.1 | `tests/ocr/e2e.test.ts` | Local HTTP E2E | Existing OCR suites passed 28/28. | Baseline/characterization: the test was written first and passed 2/2 immediately; no implementation gap existed at the local fake boundary. | Focused suite passed 2/2. | Native, scanned, corrected approval, post-activation status, and mixed page methods exercise distinct paths. | Test-only state naming improved; focused suite remained 2/2. |
| 7.2 | `tests/ocr/e2e.test.ts` | Local HTTP/dispatcher E2E | Same 28/28 safety net. | Baseline/characterization: resend, OCR-down, and catalog-down assertions passed immediately; no production code was written. | Focused suite passed 2/2. | Duplicate resend, terminal dispatch failure, status `503`, and retrieval `503` cover independent outcomes. | None needed beyond the shared test harness. |
| 7.3 | Gate commands below | Complete gate | Focused E2E passed 2/2. | N/A — verification-only task with no production behavior to implement. | All required gates passed. | Node type/build/test and offline Python tests provide independent stack evidence. | N/A — no production change. |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused test | `NODE_ENV=test npx --no-install tsx --test tests/ocr/e2e.test.ts` — exit 0; 2 passed, 0 failed. |
| Runtime harness | The focused suite started bounded ephemeral localhost Express servers and exercised real HTTP requests through ingest, status, review, approval, and retrieval routes; all servers closed through test cleanup. |
| Complete gate | `npm run check`, `npm run build`, and `NODE_ENV=test npm test` exited 0; canonical Node passed 78/78. `PYTHONDONTWRITEBYTECODE=1 ocr-service/.venv/bin/python -m pytest ocr-service/tests -q` passed 13/13 offline with one dependency deprecation warning. |
| Rollback boundary | Remove `tests/ocr/e2e.test.ts` and revert only tasks 7.1–7.3 plus this Unit 13 progress/history metadata; preserve Units 1–12. |
### Validation and Boundary
- Unit 13 adds 183 functional lines. Required tasks/progress/history metadata adds 52 changed lines, for 235 total, below the 400-line budget.
- Tracked and complete intended-untracked whitespace checks passed. No matching `tsx --test`, E2E Node, or OCR Uvicorn process remained after the corrected self-excluding process check.
- Intended untracked inventory: `src/modules/ocr/{dispatcher,indexing,retention,review}.ts`; `tests/ocr/{contracts-deploy,dispatcher,e2e,retention,review}.test.ts`.
- Tasks 7.1–7.3 are complete. Task 7.4 remains unchecked and untouched; no production flag, service, database, vector store, embedding provider, OCR endpoint, commit, push, or PR was used.
## Unit 14a: OCR Job Execution and Result
- Persisted accepted PDF bytes/results in SQLite, resumed queued jobs on idempotent resubmission, executed the existing renderer through an injected engine, recorded terminal status/errors, and added authenticated result retrieval; task 7.4 remains pending.
### TDD Cycle Evidence
| Task | Test File | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|
| Unit 14a | `ocr-service/tests/test_api.py` | 13/13 | 8/10 missing execution/result; recovery triangulation 9/10 remained queued | 10/10; success, restart recovery, unauthorized, missing, not-ready | Guarded duplicate workers; 10/10 remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | API 10/10; OCR 15/15; Node 78/78; `npm run check` and whitespace clean. |
| Runtime harness | Local Uvicorn with deterministic engine returned upload `202`, status `200/succeeded`, result `200`, matching job/document identity; process/temp cleanup verified. |
| Rollback boundary | Revert `ocr-service/app/main.py`, `ocr-service/tests/test_api.py`, and this Unit 14a/history metadata; Units 1–13 remain intact. |
## Unit 14b: OCR Transient Cleanup
- Added authenticated idempotent client deletion after durable RAG result acceptance and automatic 24-hour OCR row/input/result expiry through the existing 30-second readiness healthcheck; cleanup failures fall back to TTL without failing durable candidates.
### TDD Cycle Evidence
| Task | Test Files | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|
| Unit 14b | `ocr-service/tests/test_api.py`; `tests/ocr/{client,dispatcher}.test.ts` | Python 10/10; Node 15/15 | Python 10/11 lacked clock/sweep; Node 15/17 lacked delete client/dispatch | Python 11/11; Node 17/17; pre-persistence failure retained remote state and cleanup failure preserved local success | Reused healthcheck and generic retry/auth boundaries; focused suites remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | Python API 11/11; OCR 16/16; focused Node 17/17; canonical Node 80/80; check/build/whitespace clean. |
| Runtime harness | Local Uvicorn returned pre-expiry `200`, post-24h `404`, unauthorized delete `401`, idempotent authenticated deletes `204/204`, and post-delete `404`; process/temp cleanup verified. |
| Rollback boundary | Revert only Unit 14b deltas in OCR API/client/dispatcher/tests and this progress/history metadata; durable RAG artifacts, active corpus, and Unit 14a remain intact. |
## Unit 14c: Logical Upload Source Reference
- Added an optional trimmed multipart `sourceRef` that remains independent from the temporary physical upload path, preserves original-filename fallback, rejects blank values, and documents the contract without activating content.
### TDD Cycle Evidence
| Task | Test Files | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|
| Unit 14c | `tests/ocr/{upload-source-ref,contracts-deploy}.test.ts` | Existing upload/contracts 13/13 | 2/4; missing contract and blank reference accepted | 4/4; provided/trimmed reuse, physical-name independence, fallback, blank rejection | Shared one parsed value across file/ZIP branches; focused suite remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | Focused 4/4; multipart runtime 1/1; canonical Node 81/81; check/build/whitespace clean. |
| Runtime harness | Ephemeral localhost Express multipart uploads reused `Errores Junio 2026 - OCR verificado.md` across two physical PDF names, preserved `fallback.pdf` when omitted, rejected blank input `400`, kept activation false, and removed temporary files. |
| Rollback boundary | Revert only Unit 14c deltas in `src/{app,api/openapi}.ts`, its two tests, and this progress/history metadata; Units 14a–14b, active content, and corpus remain intact. |
## Unit 16: PDF Raster-Aware Routing
- Added PDF.js painted-area telemetry, `pdf-detection-v2`, a 5% raster threshold, and fingerprint binding; task 7.4 remains pending.
### TDD Cycle Evidence
| Task | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|
| Unit 16 | Parser/detection 10/10 | 8/11; v1, missing telemetry, missing routing | 11/11; exact 5% routes and 4.99% logo stays native | Matrix transform readability; 11/11 remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/canonical | Focused 11/11; canonical Node 82/82; check/build/whitespace clean. |
| Runtime harness | Real 25-page PDF reported raster coverage 1 on every page, selected pages 1–25, and native text still lacked CBG04a/FAT07/DSAU08/NSAV06. |
| Rollback boundary | Revert Unit 16 parser/detection/ingest/test/contract deltas and this metadata; preserve Units 1–15, candidates, and active content. |
## Unit 17a: Durable OCR Result Handoff
- Persisted each fully validated OCR result as canonical private content before PostgreSQL job completion, review transition, or best-effort remote deletion. The envelope binds version, document, remote job, source hash, engine, pages, full page text, metrics, and line text/confidence/boxes.
- Added immediate and restart-safe exact readback with schema, content hash, expected identity, permission, and optional artifact-byte hash validation. Immutable retries accept only byte-identical content; corruption and identity mismatches fail closed.
- Task 7.4 remains pending; no candidate composition, review endpoint restoration, activation, production wiring execution, migration, deployment, commit, or push occurred.
### TDD Cycle Evidence
| Task | Test Files | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|---|
| Unit 17a | `tests/ocr/{client,dispatcher}.test.ts` | Existing focused suites 17/17 | 8/11: missing artifact exports and dispatcher skipped the durable-write boundary | Focused 19/19; success ordering, simulated remote deletion/restart, repeat write, corruption, identity mismatch, and write failure | Centralized artifact integrity errors; focused 19/19 remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | Focused client/dispatcher 19/19; client/dispatcher/E2E regression 21/21; canonical Node 84/84; check/build/whitespace clean. |
| Runtime harness | Real temporary private artifacts survived simulated remote deletion and exact readback after a restart boundary; write failure occurred before job completion/review/deletion and left the active version untouched. |
| Rollback boundary | Revert Unit 17a deltas in `src/app.ts`, `src/modules/ocr/{artifacts,client,dispatcher}.ts`, `tests/ocr/{client,dispatcher,e2e}.test.ts`, and this progress/history metadata; preserve Units 1–16 and task 7.4. |
### Boundary and Accounting
- Forecast: 250 changed lines against the authorized 360-line cap. Actual: 227 additions plus deletions across source, tests, progress, and history.
- Unit 17a does not restore the production review endpoint and cannot by itself satisfy the mandatory failed-evidence remediation binding.
## Unit 17b: Native Evidence and Candidate Composition
- Persisted private canonical native-page evidence beside each original and bound it to version, document, original hash, selected OCR pages, page text hashes, and raster metrics.
- Added OCR `inkCoverage`, exact restart readback of native/OCR evidence, quality classification, existing-domain candidate composition, immutable candidate publication/readback, and lifecycle hash/metrics agreement before `review_required`.
- Added reconciler recovery for the crash window after all OCR jobs succeed but before candidate finalization. Any missing, corrupt, mismatched, blocked, conflicting, or failed evidence leaves the version fail-closed and the remote OCR result undeleted.
### TDD Cycle Evidence
| Task | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|
| Unit 17b | Focused OCR/catalog suites 25/25 | Node failed on missing candidate exports/repository/recovery and skipped finalization; Python failed on missing `inkCoverage` | Focused 36/36 plus runtime 2/2; native/OCR/blank, corruption, missing input, quality block, immutable conflict, readback mismatch, and restart recovery | Low-resolution ink sampling and shared private JSON validation; all gates remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | Focused 36/36; review/E2E/retention 15/15; canonical Node 89/89; offline Python 16/16. |
| Runtime harness | Bounded filesystem/restart scenarios passed 2/2: mixed native/OCR/blank evidence survived exact readback, and a completed version resumed candidate finalization after restart. |
| Gates and cleanup | `npm run check`, `npm run build`, tracked/untracked whitespace passed; temporary artifact directories absent; final process check found no test, Uvicorn, or pytest process. |
| Rollback boundary | Revert only Unit 17b deltas in OCR render/client/artifacts/orchestration/repository/reconciler/ingest code, focused tests, and this metadata; preserve Unit 17a durable results and Units 1–16. |
### Boundary and Accounting
- Forecast: 370 changed lines against the authorized 400-line cap. Actual: 400 additions plus deletions against native begin tree `787b4b55c4d86e401a8b1f195173e3ed0e0e4d94`.
- Task 7.4 remains pending; no candidate or active version, production service, secret, deployment, commit, or push was touched. Production review construction remains unwired, so Unit 17b does not claim the broader production review `503` is fixed.
## Unit 17c: Review Artifacts and Production Candidate Loading
- Added authenticated OCR-service page-image transfer and immutable private `0600` review-image artifacts with version/document/page/hash metadata and containment-safe resolution.
- Added a concrete PostgreSQL lifecycle-context read path and restart-safe durable candidate reader that validates state, identity, page hashes, metrics, risks, image permissions, and image hashes before returning review data.
- Wired and documented production review GET and protected image GET routes while intentionally leaving production approval/rejection/indexing unavailable. Task 7.4 remains pending.
### TDD Cycle Evidence
| Task | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|
| Unit 17c | Node OCR/catalog 37/37; Python 16/16 | Node failed on missing image client/artifact/reader exports and absent OpenAPI image path; Python image route returned 404 | Focused 40/40; relevant 48/48; contract 3/3; authenticated image transfer, restart loading, lifecycle mismatch, corruption, traversal-shaped identity, disabled/unauthorized and decision isolation | Consolidated identifier imports and native-document validation; focused suites remained green |
### Work Unit Evidence
| Evidence | Result |
|---|---|
| Focused/regression | Focused 40/40; relevant OCR/catalog 48/48; image contract 3/3; canonical Node 92/92; offline Python 16/16. |
| Runtime harness | Bounded localhost Express dispatch persisted result and all review images before completion, composed the candidate, entered review, restarted the reader, served authenticated review/image bytes, and kept approval/rejection at `503`; FastAPI integration returned authenticated PNG identity/hash headers and rejected unauthorized/invalid pages. |
| Gates and cleanup | `npm run check`, `npm run build`, canonical tests, offline Python, tracked/untracked whitespace, temporary-artifact cleanup, and final process inspection passed. |
| Rollback boundary | Revert only Unit 17c deltas in OCR API/client/artifacts/review/catalog/app code, focused tests, and this metadata; preserve Units 17a–17b and task 7.4. |
### Boundary and Accounting
- Unit 17c is 377 additions plus deletions against native begin tree `b64f46c0bb4edd8f9878b3920bf4d3372a29073a`, below the 400-line cap.
- No approval, rejection, correction, embedding, Qdrant, activation, production, SSH, secret, migration, commit, or push operation occurred.
## Unit 17d: Transactional Review Decisions
- Added a concrete PostgreSQL review store that locks the candidate/source/page lifecycle rows, reloads durable evidence, and revalidates version, active version, candidate, page, line, and correction identities before any decision write.
- Approval publishes and exactly rereads canonical private immutable `reviewed-pages.json`, records all corrections and reviewed page hashes transactionally, then transitions only `review_required → indexing`. Rejection records its bounded reason transactionally and creates no reviewed artifact.
- Production approval/rejection routes now use the durable store when PostgreSQL is configured. Approval stops at `indexing`; neither decision route invokes indexing, embeddings, Qdrant, activation, or the active pointer. Task 7.4 remains pending.
### TDD Cycle Evidence
| Task | Safety Net | RED | GREEN / TRIANGULATE | REFACTOR |
|---|---|---|---|---|
| Unit 17d | Review/client 20/20 | Missing store/artifact exports failed module loading; route/contract RED failed 2/5 | Focused review/E2E/contracts 16/16; transactional runtime 2/2; approval, rejection, replay, stale state, corrupt path, auth/off gates, and zero-index boundary | Shared correction preparation and immutable publication cleanup; focused suites remained green |
### Work Unit Evidence
- Review/E2E/contracts 16/16; canonical Node 95/95; check/build passed; fake-PostgreSQL transactional runtime 2/2; rejection produced no reviewed artifact; approval stopped at `indexing` with zero indexing calls. Python unchanged.
- Rollback: revert only Unit 17d deltas in `src/modules/ocr/{artifacts,review}.ts`, `src/{app,api/openapi}.ts`, the three focused tests, and this metadata; preserve Units 17a–17c and task 7.4.
### Boundary and Accounting
- Direct delta against native begin tree `5ab7b1eb5fe07cf2025e36b35fcd132f9428eb01`: within the 400-line cap after metadata condensation. Stale/mismatched/replayed/partial/corrupt decisions fail closed; transaction rollback removes newly published reviewed artifacts on downstream failure.
- Migrations 001/002 unchanged. No indexing, embedding, Qdrant, activation, active-pointer, production, SSH, secret, re-OCR, deployment, commit, push, or task 7.4 operation occurred.
## Unit 17e: Reviewed Indexing Store
- Added `PostgresOcrIndexingStore` (`indexing.ts`): loads composed candidate + reviewed-pages artifacts with full identity/integrity validation, chunks reviewed text with the canonical `documentalChunkingPolicy`, embeds via the existing provider, writes canonical versioned Qdrant points, and verifies exact point count before `indexing → ready`.
- Added `OcrReadyIndexingService`: indexes approved reviewed candidates to `ready` only; activation/settlement methods fail closed as reserved for a separately authorized unit. No active-pointer changes.
- Strict TDD: RED from missing exports (module-not-found); GREEN focused `tests/ocr/indexing-store.test.ts` 2/2 (restart-safe indexing, canonical point IDs, count verification, fail-closed identity/embedding/count/corruption). Canonical Node 97/97, check/build/whitespace green. Python unchanged.
- Rollback: revert `src/modules/ocr/indexing.ts`, remove `tests/ocr/indexing-store.test.ts`, and this metadata; preserve Units 17a–17d and task 7.4.
### Boundary and Accounting
- Direct delta against begin tree `f351a3d4514d89c9208c72a93be3f9411b41bedd`: within the 400-line cap. No migration, Qdrant production writes, activation, active-pointer, production/SSH/secret access, deploy, commit, or push. Task 7.4 remains pending.
## Unit 17f: Production Approval→Indexing Wiring
- Production `createApp` now constructs `PostgresOcrIndexingStore` + `OcrReadyIndexingService` when PostgreSQL is configured (overridable via `options.indexingService`), and the authenticated approve route invokes indexing after a successful durable approval, returning the resulting `ready` state; without a configured indexing service it fails closed with 503 `OCR_INDEXING_UNAVAILABLE`. Rejection stays decision-only; activation remains reserved.
- Strict TDD: genuine RED 2/2 (approve returned `indexing` without indexing; missing-service returned 500), GREEN focused wiring 2/2; E2E contract updated to approve→index→ready with exactly one indexing call. Canonical Node 99/99, check/build/whitespace green. Python unchanged.
- Rollback: revert `src/app.ts` wiring, `tests/ocr/approval-indexing-wiring.test.ts`, E2E delta, and this metadata; preserve Units 17a–17e and task 7.4.
### Boundary and Accounting
- No migration, activation, active-pointer, production/SSH/secret access, deploy, commit, or push. Task 7.4 remains pending.