# Argus persistence slice — implementation and acceptance contract

Started 2026-09-09 from `main` at `c054b7c`. Authority: extraction specification section 6,
D-EX-5, P2-05/P2-05b, C6, and `persistence-acceptance-decision-260908.md`.
This is the next implementation slice, not an acceptance result or a production cutover.

## Current boundary

The native fiscal and bank-statement pipelines are connected locally: see checkpoints two and three.
Processed v2 fiscal documents and complete statements schedule persistence; corrections, company
assignment and duplicate release schedule the same path. Missing/ambiguous facts or catalogue
mappings remain explicit review holds. Intercompany paired entry remains a separate workflow.

### Boundary at the first checkpoint

Extraction stores validated result/correction JSON on the intake. `CaptureFacts` still carries
only log/routing metadata. The atomic fiscal hand-off now makes `DocumentCaptureService.RegisterAsync`,
`DocumentEntryService.EnterAsync` and `LinkFinancialRecordAsync` participate in its transaction;
standalone callers retain their own transactions. Bank resolution exists for already stored
movements; it is not a statement ingestion writer.

Read-only Main check on 2026-09-09 found 52 `sync_run` rows for NP Group. The current capture
and entry guards therefore refuse native writes for that owner. Keep that protection. Build
and test on an isolated Preview tenant with synthetic development fixtures. NP Group activation
requires a separate cutover decision; do not delete sync history or make an implicit exception.

## Required behavior

| Stage | Required result | Refusal/hold |
|---|---|---|
| Input | Revalidate `sibyla.extract.v2`; use corrections only when their contract matches the result; bind work to intake ID and input revision/digest | Invalid, superseded, discarded, duplicate, unresolved-company or human-held input cannot enter |
| Identity | Resolve company and the correct counterparty for I/R flow, using governed identities | Unknown/ambiguous entity or inaccessible company remains a reported decision; no invented mapping |
| Fiscal lines | Carry printed facts and server transforms into a draft; each line resolves a known company/entity/item trio and EICode | One unresolved line prevents the entire financial entry; preserve all reported exceptions |
| Duplicate control | Repeat the existing fiscal-key lookup on corrected values immediately before entry, including company-preassigned uploads | A register match holds the intake; a genuine-repeat ruling must remain explicit and attributable |
| Fiscal persistence | Write header, all lines, DOCLOG link and intake completion consistently; preserve date, period rule/evidence, signs, stamp-duty split and nullable printed quantities/prices/rates | No partial header/line entry; no silent defaults or double negation |
| Statements | Resolve printed IBAN/account to BNKACC; store signed CashDelta/Direction and movement identity; derive absent running balance only from a known opening balance, with evidence | Missing account, unavailable opening balance or failed continuity remains a hold/exception, never zero-filled |
| Correction | Extend the existing Uploads review with editable line and movement grids, reconciliation feedback and preserved CorrectedJson provenance | Invalid corrections remain in the form; original extraction/evidence stays immutable |
| Retry | The same intake/revision reaches at most one financial result; a retry after commit returns that result | Re-extraction after entry cannot silently overwrite or create a second record |
| Isolation | All operations use tenant RLS and company access; audit identifies the real actor | Cross-owner/company references fail closed |

## Implementation sequence

1. Define typed hand-off input/result and resolution tests from synthetic fixtures. Keep printed
   extraction facts separate from resolved catalogue identifiers and derived accounting values.
2. Implement a transaction boundary shared by capture, entry, linking and completion. Preserve the
   existing service callers. Lock the intake/revision before work and serialize competing fiscal-key
   checks with insertion; a lookup outside that boundary is insufficient for concurrent arrivals.
   Use a durable intake-to-entry identity. Do not repurpose `RegisterEntryId`, which currently means
   the existing entry a possible duplicate points to.
3. Add fiscal resolution and the persistence-boundary duplicate guard; verify corrected identities,
   genuine repeats, crash/retry and whole-document holds before wiring worker/correction dispatch.
4. Add statement/account resolution, movement persistence and continuity checks. Use the existing
   bank semantics from the extraction spec; do not route statements through the I/R entry writer.
5. Extend the correction grids in the current PT/EN Uploads interface. Cancel reads on navigation;
   preserve focus, Escape behavior and desktop/mobile layout.
6. Validate the integrated candidate against the synthetic regression corpus, then prepare the
   separate sealed acceptance sitting. No production activation is implied by local success.

## Acceptance matrix to freeze before opening the new set

Proposed composition: **24 fresh source documents**, not extraction-v1 sample/reserve documents
and not development fixtures. Expected outcomes must be independently verified from their sources.

| Group | Documents | Required coverage |
|---|---:|---|
| Fiscal straight-through | 8 | Payable and receivable; multi-line; VAT/exemption; printed nulls; stamp duty; negative and positive-printed credit notes; explicit/fallback account period |
| Fiscal review | 8 | Entity ambiguity; absent trio; company ambiguity; preassigned-company duplicate; correction creating a duplicate; verified genuine repeat; held input; correction of line/reconciliation facts |
| Statements | 8 | Inflow/outflow/zero; printed and computed balance; missing opening balance; unknown account; repeated movement; period continuity; statement requiring human correction |

Cases may cover multiple conditions, but every listed condition needs an expected result in the
sealed manifest. If fresh documents do not cover a condition, obtain replacements before sealing;
do not quietly remove the condition or alter the denominator.

Proposed pass criteria: every expected entry/hold/refusal is correct; every accepted document has
the expected line/movement count, identities, amounts, signs and periods; zero unauthorized writes,
duplicate entries or partial financial records. Compare stored amounts exactly to the independently
verified canonical expected amounts. Existing extraction reconciliation tolerances do not waive a
wrong stored amount. These criteria and the composition need agreement before the acceptance run.

The operator first submits each sealed source once and records its outcome. For cases requiring
review, record the first hold before applying only the corrections/rulings specified by the sealed
protocol, then record the final outcome. Preserve both measurements. Freeze source hashes,
expected outputs/holds, software commit, configuration, skill/model tuple and scoring rules together.
Keep development reruns and later appeals separate; the first result remains immutable.

Use synthetic fixtures, separately from the 24-document accuracy denominator, for concurrent
submission, cross-tenant attempts, cancellation, failure injection and retry-after-commit proofs.
Extraction-v1 D-EX-7 remains unchanged: its spent reserve provides no held-out validation result.

## Completion evidence

Record the actual candidate SHA, migrations, full-suite and browser results, synthetic concurrency/
retry/isolation results, sealed acceptance manifest and first measured results. A passing local
implementation does not by itself close C6 or authorize activation for NP Group.

## First implementation checkpoint — atomic fiscal persistence

`FiscalIntakeEntryService` accepts `ResolvedFiscalIntake`, an internal trusted-resolver request,
not a client correction DTO. It is registered for dependency injection but is not invoked by a
worker or HTTP/UI endpoint. The extraction-to-draft resolver is still to be implemented.

Implemented:

- Transaction-scoped owner lock, intake row lock, active-company/code check and effective v2
  correction selection. Canonical extraction SHA-256 rejects stale resolution. A request digest
  prevents a completed entry being replayed with a changed resolved draft.
- Fiscal-key/counterparty verification and a register duplicate check serialized with insertion.
  Duplicate holds keep the existing `RegisterEntryId` meaning and emit an attributable audit event.
- `intake_entry_receipt` uniquely binds owner/intake to DOCLOG, with nullable financial entry
  while review is pending. Corrections can resume the bound log; completed retries return the
  existing entry. Capture, line entry, linking, receipt and success audit share a single commit.
- Forced RLS, owner-scoped uniqueness, digest checks and composite owner/company foreign keys
  to intake, log and header. Referencing another tenant's valid UUID is rejected by the database.
- Injected failure after financial writes rolls the entire unit back. Standalone capture tests
  remain in place. Tests use synthetic records owned by newly created Preview tenants and remove
  their own fixtures; no acceptance source documents are involved.

Migration `20260909022515_IntakeEntryReceipt` was applied to **Preview only** on 2026-09-09.
Main has not been migrated. Full migration/table/grant parity therefore remains a release gate;
do not interpret a targeted passing implementation suite as production release acceptance.
EF `has-pending-model-changes` reports no model/snapshot drift.

*Correction 2026-09-10:* the two paragraphs above describe the slice mid-way on 2026-09-09. By the
end of that day the resolver was wired into the worker (`Program.cs`, `QueueWorker.cs` via
`FiscalPersistenceQueue`), and on 2026-09-10 `__EFMigrationsHistory_Sibyla` on **both** Main and
Preview lists all 35 Argus migrations including this one; `PreviewParityTests` pass 10/10. The
C6 protection in the paragraph before them is unchanged.

Validation on 2026-09-09 (working tree based on `c054b7c`, not committed or deployed):

- Unfiltered solution regression: **1,066 passed, 3 failed, 0 skipped**. Argus 255,
  Platform 543, API 56 and Browser 157 passed; TenantIsolation passed 55 and failed only
  `PreviewHasExactlyTheSameMigrations`, `PreviewHasExactlyTheSameTables` and
  `PreviewRoleGrantsEqualTheMainRoleGrants`. These three failures reflect the Preview-only
  migration/table/reader grant and remain open release requirements.
- After the final active-company/code and canonical-document-number guards, the complete
  Argus rebuild passed **256/256**, including 20 new atomic-writer cases. Evidence:
  `%TEMP%/sibyla-persistence/argus-final_net10.0_20260909024134.trx`.
  The earlier solution results use the `persistence-regression` prefix in the same directory.
- `git diff --check` passed. No sealed acceptance run or Main migration was performed.

At the first checkpoint, still required before wiring this service to intake: governed extraction-to-draft resolution
(amounts, signs, stamp duty, periods and item mappings), the verified-genuine-repeat workflow,
and a provenance policy for distinct source files with the same original filename. This boundary
currently refuses an existing filename belonging to another intake, rather than adopting its log.
An unresolved entity or stale fiscal identity is refused here; the resolver must create the
governed review outcome. Unresolved item lines are reported by the existing entry service and
can resume through the pending receipt. Statement persistence remains a later step. The [Stage 19 gap review](invoice-skill-build-gap-review-260909.md)
records the subsequent correction-grid implementation and its remaining structural-authoring gaps. NP Group's C6 guard is unchanged.


## Second implementation checkpoint — governed resolver and dispatch

Implemented locally on 2026-09-09, based on the existing working tree at `c054b7c`:

- `FiscalIntakeResolver` reads a locked intake and revalidates the effective v2 source. It checks
  the active company against both printed fiscal identities, even when the company was assigned
  before extraction. An ambiguous company, missing fiscal key/date, synthetic/retired/ambiguous
  counterparty or unsupported paired intercompany flow cannot silently enter.
- Counterparty resolution requires a unique live fiscal identity. Item resolution uses exact
  normalized printed descriptions against live item names and company/global declared aliases.
  Extraction `item_candidates` never supplies authoritative codes. Every resulting line still
  passes the existing company/entity/item trio and physical-quantity guards in the entry writer.
  Tax-less counterparties remain review cases; no identity is invented from a printed name.
- The existing `StampDutySplit` is now used for persistence: appended duty lines and surcharge
  folds, kind-based credit signs, preserved invoice discount signs, nullable quantities/prices/rates
  and explicit gross-to-net price conversion. Header totals are kept separately from line sums;
  a rounding/reconciliation difference is retained in transformation evidence instead of silently
  changing a printed line. A synthetic duty line also needs a governed item mapping.
- Stated header periods use R1. A credit note with related documents can take their common
  accounting period when every reference resolves within the same company/counterparty/flow.
  Otherwise unresolved referenced periods hold; absent stated/reference periods use the existing
  R2/R3 ladder. Partial stated periods hold rather than becoming invented dates.
- Missing/ambiguous mappings create a `Fiscal resolution` DOCRQE item and a held intake with
  all detected reasons. A pending source log survives and is reused after correction. Successful
  entry supersedes unresolved machine review items without inventing a human decision.
- Native log identity is `intake/<intake UUID>/<original filename>`. The original label is retained
  in SourceFilename, SHA-256 remains bound to the intake, and two different intakes called scan.pdf
  cannot adopt each other's log. Corrected date/type and completed archive location refresh the
  pending log. A storage transfer finishing after entry updates the log through its receipt.
- `duplicate_classification` gains IntakeId, InputSha256, FileSha256 and DeclaredByUserId.
  A verified-genuine-repeat ruling is consulted only for that exact intake, canonical revision,
  content hash and matched entry. Historic filename-only declarations cannot authorize it.
  Corrections invalidate the permission; another intake needs its own ruling. Existing signed
  declarations and audit evidence are preserved. A separate checksum question stays pending and
  becomes the next displayed duplicate question; one answer never resolves both questions.
- A transactional outbox schedules `fiscal-persistence` on the IO lane after successful fiscal
  extraction and after correction/assignment/duplicate release. Held statements are not scheduled.
  Each worker job creates a fresh company-scoped tenant context. Queue lease/attempt fencing is
  checked before resolver effects and again inside atomic financial entry. Lost leases cannot
  write financial records or new review holds. Queue completion does not overwrite extraction JSON
  or the persistence outcome; exceptions use the existing bounded retry/dead-letter path.
- Completed persistence replays the receipt for unchanged input. Changed extraction after financial
  entry is refused and requires a separate decision; no automatic ledger correction is introduced.
  Existing processed intakes can resume through their correction confirmation; there is no bulk
  historical import or automatic backfill in this change.

Migration `20260909101317_FiscalRepeatRulingIdentity` is applied to **Preview only**.
The Main/Preview release-parity gate remains open. No source invoices were imported, no sealed
acceptance documents opened, no Docker containers created, and no production service deployed.

Final per-suite validation: **1,100 passed, 3 failed, 0 skipped**. Argus **278/278**, Platform
**552/552**, API **56/56**, Browser **159/159**; TenantIsolation **55 passed / 3 failed**,
only the known Main/Preview migration, table and AI-reader grant parity gates. The final Argus
run includes the actual scoped fiscal worker, replay, lost-lease refusal, payable/receivable
identity, credit signs/period, duty/surcharge transformation, gross prices, revision-bound repeat
permissions, independent filename provenance, pending-log correction and review supersession.
Platform proves one fenced completion creates one persistence outbox job and fiscal completion
cannot overwrite the extraction or its held outcome.

The browser rerun used matching Playwright Chromium revision 1234 after the earlier forced
1228 executable produced injected-script errors. No application UI change was needed for that
runtime mismatch. Evidence: `%TEMP%/sibyla-resolver-final.log` (Platform/API/tenant results),
`%TEMP%/sibyla-resolver-verification.log` (final Argus/Browser), TRX files with the
`resolver-final` and `resolver-verified` prefixes under `%TEMP%/sibyla-resolver/`.
EF reports no pending model changes; `git diff --check` passed. This is implementation evidence,
not a fresh sealed acceptance result or authorization to deploy.

Remaining planned work: statement/account and
movement persistence; Stage 19 matching parity; structural correction authoring; release parity,
fresh sealed acceptance, and the explicit NP Group C6 cutover. The conservative identity and
period holds above are intentional supported outcomes, not straight-through acceptance claims.


## Third implementation checkpoint — native bank statements (2026-09-09)

The persistence worker now dispatches validated bank statements to `BankStatementIntakeService`.
It uses the same owner/company job scope and lease fence as fiscal persistence. C6 remains enforced;
there is no native import beside an owner's sync history. This checkpoint is local/Preview only.

- Account identity comes from an exact normalized IBAN/account number in the selected company.
  Conflicting identifiers, multiple accounts, unknown account kinds and currency conflicts hold
  for review. Bank/account names and number suffixes are never identity fallbacks.
- `DocumentCaptureService` logs bank statements without treating their fiscal DOCTYP Exclude
  routing as DOCFAI. A statement never creates FDCHDR. A resolvable company/type can retain its
  provenance-scoped log while account or balance questions remain held.
- One transaction creates the immutable `bank_statement_receipt`, BM codes, BNKMOV rows,
  per-period BNKCHK results and audit. Receipt/movement links include owner, company and account
  foreign keys; the new receipt table enforces owner RLS. An injected movement-write failure
  rolls back the log, receipt, checks and code issuance too.
- Native printed signs remain in Amount; current accounts keep that sign for CashDelta and card
  accounts negate it, including the Portuguese `Cartão de Crédito` catalogue spelling. Direction
  follows CashDelta. Original print sequence and established chronological order are both stored.
  DocDate prefers embedded date, then transaction date, then posting date; Period follows DocDate.
- Printed running balances are retained. Missing balances are calculated only along an established
  chain anchored in an opening/preceding printed balance, with a computed-balance flag. Oldest-
  and newest-first statements are supported. Ambiguous ordering, unexplained balances, incomplete
  extraction or mismatched printed counts hold the entire statement, with no partial movement import.
- Overlapping statement periods and pre-existing movement tuples hold for human review. This
  conservative path does not silently merge overlaps or collapse repeated identical source rows.
  Existing movements without native statement provenance also require a separate coverage decision.
  Adjacent statements must agree on closing/opening balance. Checks distinguish balanced entered
  movements with partial calendar coverage from a fully covered balanced month; empty printed
  statements with equal balances still create a check. Independently maintained checks are retained.
- Replaying identical intake/input/file evidence returns the receipt without touching movements or
  human matches. Changed completed statements are refused, including in correction submission.
  Corrections before entry release review holds and queue another persistence attempt without a
  fresh extraction. Existing unhuman statement-resolution reviews are superseded on success.
  Transfers completed after entry also attach the archive location to the statement's log.
- The correction interface can append, insert and remove movement rows, renumbering print sequence.
  New factual cells start blank; validation requires their completion. Structural edits clear stale
  row error pointers. PT/EN hold guidance explains correction and retry; partial bank checks show
  a warning. The extraction contract's 150-movement cap remains: larger statements need a separate
  complete-import design and must not be marked complete merely to bypass that cap.

Migration `20260909112326_NativeBankStatementPersistence` is applied to Preview only, continuing
this development checkpoint's boundary. Main/Preview parity and the explicit C6 cutover remain
release gates. No production deployment, source-document carryover or sealed acceptance occurred.

Still separate: Stage 19 reconciliation/receipt-date matching, FX conversion, native overlap rulings,
large-statement ingestion, optional-object/fiscal-row authoring, intercompany paired entry, and fresh
sealed acceptance. Historical synced card CashDelta repair is not included in this native writer.


Final per-test validation for checkpoint three: **1,122 passed, 3 failed, 0 skipped**.
Argus **299**, Platform **552**, API **56**, Browser **160**, TenantIsolation **55 passed / 3 failed**.
Only the known Main/Preview migration, table and AI-reader-grant parity gates remain failed.
The browser aggregate uses the complete 160-test run plus the final four-test confirmation:
that full run had two intermittent pre-existing failures (Playwright injected `elementsFromPoint`
exception and a lease-renewal timing assertion); both passed in the final targeted run. The new
statement interaction test passed in both runs and covers insert/remove, renumbering, required-cell
validation and page containment at 1440px/390px in EN/PT. Final screenshots also confirm that removing
an invalid row clears its obsolete error pointer. No unrelated application or harness change was
made to conceal either intermittent failure.

Evidence: `%TEMP%/sibyla-bank-verification.log` (initial full suites, including API/tenant),
`%TEMP%/sibyla-bank-final.log` (final Argus/Platform and full browser),
`%TEMP%/sibyla-bank-ui-final.log` (final four-test confirmation), and TRX under `%TEMP%/sibyla-bank/`.
Screenshots: `local/artifacts/playwright/statement-review/`. EF reports no pending model changes;
`git diff --check` passed. The invoice-skill-build source checkout remains clean at `616f10b`.
