# C-17, the pre-assigned upload register check — status and decision note

Prepared 2026-09-10. C-17 was stated on 2026-09-08 as a known gap in the extraction slice
([spec §2.6](apollo-argus-extraction-v1-spec.md), [owner guide §8.3](apollo-argus-extraction-v1-owner-guide-260907.md),
[round-2 closure](apollo-argus-extraction-v1-checkpoint-round2-closure-260908.md)): the company gate
runs its register duplicate lookup only for a row it has just assigned, so an upload made *into* a
company returns early at `CompanyGate.cs:39` and gets no register check, no checksum re-run and no
`document.company.*` audit row at the gate. Ruling D-EX-5 (3) scheduled "the second guard" for the
persistence slice, and the owner guide put the question of extending the gate to the owner.

## What has changed since the gap was stated

The second guard landed with the persistence slice and is in production.

- `src\Sibyla.Modules.Argus.Infrastructure\Capture\FiscalIntakeEntryService.cs` lines 103–135
  re-derives the fiscal key from the effective (corrected or raw) result, runs the same register
  predicate as the gate (company, counterparty fiscal number by `CompanyMatcher.SameTaxId`,
  N-2 normalised document number, tombstones excluded) less entries already covered by a
  "verified genuine repeat" declaration, and on a match holds the row as `PossibleDuplicate` with
  `register_entry_id` / `register_entry_code` set and a `document.register.duplicate` audit row.
  It never enters the document.
- It runs for **every** processed v2 fiscal intake that has a company, pre-assigned or
  gate-assigned: `FiscalPersistenceQueue.ScheduleAsync` (filter `company_id IS NOT NULL AND
  processing_status = 3 AND contract = 'sibyla.extract.v2'`) is called from `JobCompletion.cs:68`,
  from `IngestionService.cs:385` after a correction and from `IngestionService.cs:476` after a
  company assignment; the worker dispatches it at `QueueWorker.cs:121`.
- The person's ruling path is shared: `IngestionService.RuleOnDuplicateAsync` keys off the
  register reference only, and the uploads page renders the same branch whichever guard set it.
- Migration `20260909022515_IntakeEntryReceipt` and the ten Argus migrations after it are applied
  on **Main and Preview** (verified 2026-09-10 in `__EFMigrationsHistory_Sibyla`, 35 rows on each).
  The persistence-slice record's sentences "applied to Preview only" and "registered for
  dependency injection but not invoked by a worker" describe 2026-09-09 mid-slice and are stale;
  a dated correction is appended to that record.

So the effect the owner cared about, "the commonest upload shape is unprotected", no longer holds
for a native owner. The gate-level statement in §2.6 is still literally true.

## What remains, precisely

1. **NP Group gets neither guard.** `FiscalIntakeEntryService.cs:95` refuses an owner with
   `sync_run` rows before the register check, and the gate does not look up a pre-assigned row.
   That is the C6 condition, put to the owner separately in the
   [C6 proposal](c6-cutover-proposal-260910.md). No change to C-17 closes it.
2. **Timing.** The persistence guard runs at entry, one poll cycle after completion, so a
   pre-assigned duplicate is briefly visible as `Processed` before it becomes `PossibleDuplicate`.
   A gate-level lookup would hold it inside the completion transaction.
3. **Scope.** Only `invoice | receipt | credit_note` reach the persistence writer; a
   `HeldForPerson` row is asked only after its hold is corrected; a row already carrying a checksum
   question is held before the register check.
4. **Vocabulary.** The persistence hold audits `document.register.duplicate`; the gate audits
   `document.company.inferred | unresolved`. A pre-assigned row still gets no `document.company.*`
   row, which is the "third action" question the spec raised.

## Options

**(i) Close C-17 as covered by D-EX-5 (3) — recommended.** The guard the ruling scheduled exists,
runs for pre-assigned rows, and is deployed. Amend §2.6 and owner guide §8.3 with a dated note
saying so; keep the characterisation test
`APreAssignedRowGetsNoRegisterLookupInThisSlice_KnownGapUntilThePersistenceGuard` because the
gate-level behaviour it pins is unchanged and still deliberate. No code changes.

**(ii) Also extend the gate.** Stop returning at `CompanyGate.cs:39`, run the register lookup with
the pre-assigned company, add a third audit action (for example `document.company.preassigned`),
add the fourth transition row in spec §4.7, delete the characterisation test, keep
`AnAssignedRowIsNotReassigned` green and keep `ClaudeCliTimeoutTests` at exactly one
`document.company.%` row per intake. The open design question is flow: the gate chooses the
counterparty from `flowType`, which for a pre-assigned row must be derived by comparing the
company's tax id against issuer and recipient, or the lookup must be skipped when neither side
matches. Worth doing only if the owner wants the hold at completion rather than at entry.

## Decision requested

| Id | Question | Recommendation |
|---|---|---|
| Q-C17-1 | Close C-17 under option (i), or also extend the gate under (ii)? | (i) now; (ii) can be a later slice if the timing gap ever matters in practice |

Until the owner rules, nothing in code changes. The dated notes in §2.6 and §8.3 record the
current state either way.
