18 KiB
SDD ledger — plan: docs/superpowers/plans/2026-09-18-hybrid-delivery-note-ingestion.md
Workspace: /Volumes/EXT/DEVELOPMENT/MEGAIFRASTRUCTURE/.worktrees/hybrid-dn-ingestion
Branch: codex/hybrid-dn-ingestion
Merge base: 7ab5570
Baseline: npm run test:run — 85 files, 1023 tests passed
Pre-flight scan
| Task(s) | Produce / consume or self-check | Finding |
|---|---|---|
| 1 | Status helpers, schema and PB types consumed by 3–12 | Internally consistent; implementation must remain additive and dry-run only during development. |
| 2 | Canonical PNG pages consumed by 4, 7 and 9 | Internally consistent; original bytes remain in sources, so canonicalisation cannot destroy evidence. |
| 3 | Multi-source upload consumes Task 1 schema | Internally consistent; repeated capture-session lookup may query source rows because capture_session_id is source metadata. |
| 4 | Rich PageRead consumed by 5–7 | Internally consistent; new fields are additive and current commercial fields remain required/defaulted. |
| 5 | Assembly consumes Task 4 and supplies Task 7/9 | Internally consistent; legacy segmentPages remains an adapter so current tests and runner can coexist. |
| 6 | Resolver consumes Task 1 aliases and Task 4 reads; both legacy run.ts and staged runner need it | Ruling: put matching policy in the pure resolver and keep database loading thin; the feature flag preserves legacy production behavior. If wrong, duplicated DB adapters may need consolidation. |
| 7 | Lock/staged runner consumes Tasks 1–6 and switches tick behind a flag | Internally consistent; no production switch without DN_STAGED_INGESTION_ENABLED=true. |
| 8 | Promotion consumes candidate identity from Tasks 1/7 | Ruling: an unposted partial promotion may reconcile its own candidate rows, but a posted candidate is immutable without explicit correction. If wrong, a retry could either leave partial rows or overwrite audited data. |
| 9 | Split UI consumes Task 5 assembly and Task 1 status | Internally consistent; persisted-page route must retain legacy fallback. |
| 10 | Queue consumes Task 1 stages and Task 6 relations | Internally consistent; it must not alter supplier ledger totals. |
| 11 | Review actions consume Tasks 1, 6, 8 and existing dn_item_aliases | Internally consistent after plan correction distinguishing supplier aliases from item aliases. |
| 12 | Manual posting consumes Task 8 and staged state from Task 7 | Internally consistent; auto-post and manual post share the same idempotent promotion function. |
| 1 + 3 | Schema fields against upload payload | Compatible; source_count is updated only after successful source writes. |
| 2 + 4 | Canonical MIME against vision data URL | Compatible; extract must always use image/png after normalisation. |
| 4 + 5 | Printed pagination and continuation fields | Compatible; deterministic assembly uses values actually present on each page. |
| 5 + 8 | Assembly revisions against posted candidates | Compatible; revision endpoint rejects posted candidates. |
| 6 + 8 | Supplier-scoped identity | Compatible; exact PO may derive supplier before duplicate identity is evaluated. |
| 7 + 12 | Automatic and manual posting paths | Compatible if both call the same gate/promotion functions; reviewers must reject duplicated posting logic. |
| 9 + 11 | UI review actions against API validation | Compatible; browser suggestions are never authoritative. |
Rulings
Ruling: Matching policy belongs in a pure resolver shared by legacy and staged adapters — keeps site scoping and auto-match safety in one unit — cost if wrong: a later refactor may be needed to untangle DB loading.
Ruling: Only unposted partial promotions may be reconciled automatically; posted candidates require explicit correction — preserves auditability — cost if wrong: recovery from a mid-write failure may need a manual action.
Task 1: dispatched to /root/luna_task1 at base 7ab55703b4
Task 1: Ruling: retain deployable apply capability but require explicit --apply plus an exact host confirmation; development and automated verification remain dry-run only — the spec requires an additive migration that can eventually be applied — cost if wrong: production schema application has extra operator ceremony.
Task 1: fix round 1/5 (3 addressed, 2 open — replace same-name legacy index instead of appending; align review-event type optionality; commits 34dbdeb..ad23b65)
Task 1: fix round 2/5 (2 addressed, 0 open; commits ad23b65..498b258) Task 1: minor (deferred): index-name parser handles the migration's current conventional SQLite syntax, not every possible quoted/IF-NOT-EXISTS variant. Task 1: complete (commits 7ab5570..498b258, review clean)
Task 2: dispatched to /root/luna_task2 at base 498b25858a
Task 2: review found extractPageRange still trusted declared MIME, breaking mismatched-PDF page evidence; splitPages comment is stale.
Task 2: fix round 1/5 (2 addressed, 0 open; commits 81c8f78..80fbe15) Task 2: complete (commits 498b258..80fbe15, review clean)
Task 3: dispatched at base 80fbe15
Task 3: review found duplicate-only uploads create empty batches, capture session filter interpolation is unsafe, legacy single-file validation regressed, and persisted source_count remains false.
Task 3: fix round 1/5 (2 addressed, 4 open — oversize got detail; source_count update failure; in-request hash dedup; report accuracy; commits e7e92fd..798b804)
Task 3: fix round 2/5 (2 addressed, 2 open — containment if final update and cleanup both fail; same-hash outcome after primary write failure; commits 798b804..a8ed5a1)
Task 3: fix round 3/5 (3 addressed, 0 open; commits a8ed5a1..ba52884) Task 3: complete (commits 80fbe15..ba52884, review clean)
Task 4: dispatched at base ba5288465e
Task 4: Ruling: production runner integration of readOrReusePage is deferred to Task 7 run-staged.ts, as the plan explicitly assigns orchestration there; Task 4 owns the tested primitive — cost if wrong: cache code remains unused until Task 7 and could drift before integration. Task 4: review found failure-state write errors are swallowed, rich types are weaker than the specified parser contract, and cached extraction bypasses validation.
Task 4: fix round 1/5 (3 addressed, 1 open — report fix SHA mismatch; commits 0bd344a..ea791d6)
Task 4: fix round 2/5 (1 addressed, 0 open; commits ea791d6..a1882ac) Task 4: complete (commits ba52884..a1882ac, review clean)
Task 5: dispatched at base a1882ac
Task 5: Ruling: assembly_revision belongs on dn_batches because one revision covers the complete page grouping; retain note-level assembly_revision only as candidate provenance — cost if wrong: future per-candidate revisions may need a second token. Task 5: review found missing batch schema field, unstable note ordering, incomplete posted-state guard, malformed-group gaps, and mobile hints overriding conflicting printed pagination.
Task 5: fix round 1/5 (6 addressed, 2 open — stale candidates survived group merges; revision check was not serialized; commits ab57baf..ab57baf)
Task 5: fix round 2/5 (2 addressed, 2 open — dismissed candidates could be deleted; report retained a dangling final-commit claim; commits ab57baf..297853f)
Task 5: fix round 3/5 (2 addressed, 0 open — dismissed candidates are immutable during regrouping; report provenance corrected; commits 297853f..59c158c) Task 5: complete (commits a1882ac..59c158c, review clean)
Task 6: dispatched to /root/luna_task6 at base 59c158ce37
Task 6: review found supplier evidence was dropped at segmentation, real PocketBase PO projection requested a nonexistent field, supplier identifier schema/loading was absent, and low-confidence fuzzy evidence was discarded.
Task 6: fix round 1/5 (5 addressed, 0 open — supplier evidence preserved; schema/loading aligned; fuzzy evidence retained; commits e598613..927c09b) Task 6: complete (commits 59c158c..927c09b, review clean)
Task 7: dispatched to /root/tara_task7 at base 927c09bbb2
Task 7: Ruling: lease record ids are acquisition-specific and derived from batch plus a fresh owner token; the unique batch index, not a reused record id, is the atomic gate — PocketBase only supports id-based delete, so distinct ids fence old releases from replacement locks — cost if wrong: callers or tests that assumed one fixed id per batch must be adapted.
Task 7: review found unsafe replacement-lock deletion, missing claimed_at on staged work, transient note-index candidate identity, and reused pages not reading their persisted canonical image.
Task 7: fix round 1/5 (2 addressed, 3 open — candidate identity still changed on recovered continuations and ignored keyless rows; PocketBase file URLs lacked collection metadata; commits 1f988a9..02d2cbf)
Task 7: fix round 2/5 (4 addressed, 1 open — surplus reconciliation did not recognize legacy promoted rows as terminal; commits 02d2cbf..66bf2a0)
Task 7: fix round 3/5 (1 addressed, 0 open — legacy promoted and delivery-note-linked rows preserved; commits 66bf2a0..6ff5b59) Task 7: complete (commits 927c09b..6ff5b59, review clean)
Task 8: dispatched to /root/luna_task8 at base 6ff5b59
Task 8: review found legacy reruns could mutate posted candidates, create races did not converge, runner MIME/name provenance was lost, and promotion-failure visibility writes were swallowed.
Task 8: fix round 1/5 (4 addressed, 2 open — candidate read failed open; failure-state writes retained a posted-state TOCTOU race; commits 4820706..43391a8)
Task 8: Ruling: the additive staged-ingestion migration is a deployment prerequisite; missing dn_pipeline_locks fails closed with an actionable schema-required error rather than falling back to unlocked promotion — preserves posting correctness — cost if wrong: deployment order is stricter and unmigrated installations cannot run the new promotion code.
Task 8: fix round 2/5 (2 addressed, 3 open — legacy upsert remained outside lease; lease lacked renewal; missing lock schema was reported as contention; commits 43391a8..e3e7135)
Task 8: fix round 3/5 (3 addressed, 2 open — page-store writes were not lease-aware; ownership assertions were not transactionally fenced with mutations; commits e3e7135..16f42a7)
Task 8: Ruling: protected PocketBase mutations must use the server transactional batch API to update the current acquisition record and the business row together; heartbeats remain liveness only — this closes the expiry/reclaim race without weakening reclaim — cost if wrong: all protected write adapters must support PocketBase batch semantics and older PocketBase servers cannot run this release.
Task 8: fix round 4/5 (2 addressed, 0 open — page/candidate/manual/financial writes transactionally fenced; SDK batch support required; commits 16f42a7..919124f) Task 8: complete (commits 6ff5b59..919124f, review clean)
Task 9: dispatched to /root/luna_task9 at base 919124f
Task 9: review found preparing rows hidden, empty groups after join, open pages not actionable, persisted page groups/dismissals not rehydrated, direct PocketBase image URLs bypassed protection, and missing upper page bound.
Task 9: fix round 1/5 (7 addressed, 2 open — orphan pages absent from both groups/open_pages remained invisible; server page imported a runtime helper from a client module; commits 61c0e60..d22d7b1)
Task 9: fix round 2/5 (2 addressed, 0 open — server-safe model helpers and actionable orphan pages; commits d22d7b1..26143e5) Task 9: complete (commits 919124f..26143e5, review clean)
Task 10: dispatched to /root/luna_task10 at base 26143e5
Task 10: review found staged doc_number/doc_type were not authoritative, raw supplier text incorrectly escaped Unknown supplier, supplier counts were quadratic, and tests did not render/match server mapping.
Task 10: fix round 1/5 (4 addressed, 1 open — empty-string date prevented posted/uploaded fallback; commits 2e5e504..b2582d3)
Task 10: fix round 2/5 (1 addressed, 0 open — date fallback restored; commits b2582d3..bf00cc0) Task 10: complete (commits 26143e5..bf00cc0, review clean)
Task 11: dispatched to /root/luna_task11 at base bf00cc0
Task 11: review found the existing correction UI still used the obsolete request body, readiness failed open when gates were missing, item linking allowed unresolved site/supplier ownership, alias approval metadata was incomplete, and null JSON could raise a server error.
Task 11: fix round 1/5 dispatched (5 findings plus focused regression coverage requested; commits 25f8112..70117628 under review)
Task 11: fix round 1/5 (5 addressed, 3 open — persisted financial checks could be ignored; missing assembly revisions passed; normalized alias keys could be empty; commits 70117628..9ce4250)
Task 11: fix round 2/5 dispatched (3 findings plus focused regression coverage requested)
Task 11: fix round 2/5 (3 production findings addressed, 2 proof gaps open — unknown failed-check coverage and route-level transactional rollback evidence; commits 9ce4250..0846ce5)
Task 11: fix round 3/5 dispatched (tests only)
Task 11: fix round 3/5 (2 proof gaps addressed, 0 open; commits 0846ce5..45b3c66) Task 11: complete (commits bf00cc0..45b3c66, review clean)
Task 12: dispatched at base 45b3c66
Task 12: initial implementation (code commit 5d09418, report commit 25d3dd6; focused and complete verification passed; production migration and feature flag unchanged)
Task 12: review found a missing ready-stage gate, incorrect page selection, non-atomic/racy post auditing, insufficient posted-retry association validation, swallowed comparison read failures, and overstated unit compatibility.
Task 12: fix round 1/5 dispatched (7 findings plus regression coverage requested) Task 12: fix round 1/5 complete (7 addressed; code commit 2914eef; focused tests/typecheck/migration self-test/diff check passed) Task 12: fix round 1/5 migration follow-up complete (code commit 310219d; configured dry-run now passes before new collections exist) Task 12: fix round 2/5 complete (code commit 11bc1a4; 2 findings addressed; focused tests/typecheck/diff check passed) Task 12: fix round 3/5 complete (code commit eed128e; delivery-note audit identity bound; focused posting tests/typecheck/diff check passed)
Final cross-feature fix wave: dispatched to /root/luna_final_fixes at base 9ace0fb
Final cross-feature fix wave: code complete (4 blockers addressed; commit 9ae498a)
Final cross-feature fix wave: focused verification passed — 8 files, 89 tests; npm run typecheck; migration --self-test; configured/offline migration --dry-run; git diff --check
Ruling: staged candidates must persist the aggregate page evidence, line links, and complete canonical checks before they can be ready_to_post — cost if wrong: more candidates enter review, but no incomplete candidate can pass the posting gate.
Ruling: global_page is an additive stable identity and legacy page remapping is allowed only when source ordinals/pages are contiguous and unambiguous — cost if wrong: old ambiguous rows require manual repair instead of risking the wrong scan page.
Ruling: manual split/join invalidates all derived candidate commercial/entity/match/check fields and requires recomputation — cost if wrong: an operator must wait for the staged runner, but stale PO links cannot be posted after page reassignment.
Final fix follow-up: dispatched at base 0f910c1
Final fix follow-up: code complete (shared required-check policy and strict cumulative ledger totals; commit 199dfba)
Final fix follow-up: focused verification passed — 4 files, 92 tests; npm run typecheck; git diff --check
Ruling: staged readiness and posting use the same exported required-check helper; required checks must be true and only explicitly optional checks may be null — cost if wrong: a document remains in review until all financial gates are proven.
Ruling: delivery-note item totals are loaded once per staged run and any read/schema/quantity error adds a failed persisted gate — cost if wrong: the batch pauses for operator repair instead of comparing against an unsafe zero baseline.
Final fix follow-up 2: dispatched at base 9fafec6
Final fix follow-up 2: code complete (ledger totals scoped to site-selected PO item IDs; malformed unrelated rows ignored; malformed relevant and negative quantities fail closed; commit bb70f33)
Final fix follow-up 2: focused verification passed — 4 files, 95 tests; npm run typecheck; git diff --check
Ruling: cumulative delivery totals are filtered to the batch's site-scoped PO item IDs before quantity validation — cost if wrong: an unrelated legacy ledger row cannot block this batch, while a malformed row attached to a relevant PO item still stops posting.
Migration idempotency follow-up: dispatched at base d12ca92
Migration idempotency follow-up: code complete (semantic-normalized index comparison and PocketBase variant self-tests; commit bee2b3d)
Migration idempotency follow-up: verification passed — migration self-test; authenticated live dry-run reports every collection/index already present and no writes; git diff --check
Ruling: CREATE INDEX definitions are compared after case, whitespace, quoting, and punctuation normalization while preserving uniqueness, columns, and WHERE predicates — cost if wrong: formatting-only schema drift no longer causes repeated dry-run reconciliation, but semantic index changes still replace the stored definition.
Migration normalizer follow-up: dispatched at base 485443a
Migration normalizer follow-up: code complete (backtick/double-quote/unquoted identifier equivalence with single-literal preservation; commit 1a97d7d)
Migration normalizer follow-up: verification passed — migration self-test; authenticated live dry-run reports all indexes already present/no changes; git diff --check
Ruling: identifier quote style is formatting, but single-quoted WHERE literal contents remain semantic — cost if wrong: a literal-sensitive index definition could be silently accepted or repeatedly replaced.