# Argus extraction contract v1 — specification review record

Two independent read-only reviewers per round, no coordination, ACCEPT requires Critical 0 and
High 0 from both. Findings are tabled verbatim; the author's dispositions follow each round.

## Round 1 — revision 1, head `c8e1a88` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low |
|---|---|---|
| contract | REVISE | 0 / 5 / 12 / 10 |
| feasibility | REVISE | 0 / 10 / 12 / 9 |

### Reviewer: contract — report verbatim

**Verdict:** REVISE — Counts: Critical 0 / High 5 / Medium 12 / Low 10

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| H1 | High | §5.2, §5.3 S-2 | Movements are paired positionally by `seq ↔ statement order`, but no BNKMOV column records statement order and the spec's `movements.csv` names no ORDER BY. The existing answer keys order movements by `(doc_date, bm_code)`, which is not print order. | INDEX.md: "movements[] (ordered by bnkmov.doc_date, then bm_code)". In `BPI-DO-GOT-EUR_202507.json` that order breaks the running-balance chain 15 times and `bm_code` is out of sequence; in `BPI-CC-ROA-EUR_202603.json` PAGAMENTOS/MOVIMENTOS sections interleave. Positional pairing cascades misses across ≈1,180 movement cells. | Define the key order (`mov_date`, then ingestion order) and pair by the FDR's own natural key `(posting_date, amount, N-4 description, occurrence index)`; fall back to position only within ties. |
| H2 | High | §2.4 `embedded_date`, §3 #10 | For credit-card statements the FDR's `DocDate` is a printed column, not a date embedded in the description. Under the spec the model would return it as `value_date` (unscored) and `DocDate = embedded_date ?? posting_date` yields the posting date. | `Scripts/build_bnkmov.py` lines 81–82: "CC statements give this directly as DATA DA TRANSACÇÃO, no inference needed. MovDate — CC: DATA DO MOVIMENTO". Verified on `Gott/Bank/202603/BPI-CC-ROA-EUR_202603.pdf`. Key #37: 5 of 10 rows have doc_date ≠ mov_date; the 202602/202603 straddle is by DocDate. | Contract field `transaction_date?` = the printed transaction-date column when the statement has one, else the date embedded in the description; `DocDate = transaction_date ?? posting_date`. Keep `value_date` for BCP's `DataValor` only. |
| H3 | High | §5.3 S-6 | `account_period` is scored "R1 over service_period, else R3" on rows whose key rule is blank, but the key holds blank-rule rows R3 cannot reproduce; guaranteed misses by scorer design. | Keys: I26070078 DateDoc 2026-07-02 → R3 202606, key 202607; I26080034; I26080035; I26030064 (the ACR names it an open divergence). 4 of ≈34 scorable cells → 88 % ceiling. | Score `account_period` only where the key row carries an R1 or R3 stamp; blank/R2/R4 rows reported, not scored; 2025 rows report-only. |
| H4 | High | §5.3 S-6 | "`service_period` scored as null iff rule ≠ R1" penalises a correct read: documents with a blank/R3 key rule print a period. | pdftotext: EDP I26080035 prints "Período de faturação: 2 de julho a 1 de agosto 2026"; Avis R26080004 prints "Rentals 2026-07"; Tranquilidade CN I25040004 prints "Periodo do estorno"; Via Verde I26020038 is R3 in the key. | Score `service_period` only on R1 rows; elsewhere report agreement, or hand-key the stated period on the 40. |
| H5 | High | §5.3 S-4 | `date_due` null is accepted whenever the key equals `date_doc` or `date_doc + 30`: 29 of 35 header rows, so an extractor that never returns `date_due` scores 83 % and "null when unsure" is rewarded even where the date is printed. | Sample CSV / keys: `date_due` vs `date_doc`. | Accept null only where the key row's own flag records the assumption (schema.md FDCHDR rule 3) or the receipt rule; otherwise exact. |
| M1 | Medium | Q-EX-5, S-3 | Sign rule undefined for printed-negative values: "printed sign" plus "credit note → negated" double-negates a credit note that prints negatives; negative lines on positive invoices exist (#6 stamp −0.03; #31 stamp −17.75). | Keys `I26030044.json` line 3; `I25040004.json` line 2. | Define S-3 on magnitudes: `sign_by_kind × |extraction|` vs key; or require magnitudes for credit notes. |
| M2 | Medium | §2.2 `date_pay`, S-5 | The FDR sets `DatePay = DateDoc` at capture when the document proves payment; the contract says "the printed payment date, else date_doc" and S-5 scores exact. | schema.md FDCHDR DatePay rule (1). All 6 receipt keys have datePay = dateDoc. | Carry `payment_proof {kind, printed_date?}`; derive `DatePay = DateDoc` server-side and score the derived value. |
| M3 | Medium | S-10, Q-EX-7 | 85 % per-field floor with tiny denominators: `date_pay` 6 rows, quantity/unit price 6 lines, `vat_rate` 8 lines. | Key summary. | Floor applies to fields with ≥ 20 scored cells; smaller fields reported only. |
| M4 | Medium | S-1 | The aggregate is dominated by movements (≈1,180 cells vs ≈525 header + ≈330 line cells). | INDEX.md counts. | Gate per kind at 95 % each, or keep movements out of the ruling's 95 % with their own gate. |
| M5 | Medium | S-7 | "where the statement prints one" is decided by the extraction's null; the CC key's running balances are FDR-computed, not printed. | schema.md BNKMOV; key #37. | Declare per statement in the key (`prints_running_balance`, `prints_opening`, `prints_closing`). |
| M6 | Medium | §5.2, Q-EX-11 | The spec designs a fresh CSV export and never mentions the 40 JSON answer keys that already exist; on these 40, `fdchdr.fiscal_no == entmst.fiscal_no` on every row, so the Q-EX-11 re-cut is a no-op here. | INDEX.md; the 40 JSONs. | Adopt the JSON keys as the oracle (versioned per Q-EX-8), state the no-op, fix their movement order (H1). |
| M7 | Medium | §5.6 | "drawn by `build_sample.py`'s selection logic with a recorded seed" — the script has no selection logic, seed or randomness. | `build_sample.py`. | Name the 10 reserve documents explicitly now, or write the drawing script at RED. |
| M8 | Medium | §5.7, §7 | A `[Trait("Category","Bench")]` class is not excluded from `local\test.ps1`'s default run. | `local/test.ps1`. | Console project, or `--filter Category!=Bench`. |
| M9 | Medium | §2.6, §3 #14 | Redefining `Doclog.DocDate` = `header.date_doc` gives one column two meanings; should be an owner question. | 10 of 35 keys have `doclog.docDate ≠ dateDoc`. | Add Q-EX-13. |
| M10 | Medium | §0 | The owner ruled "the Invoice Skill Build's section 5 field list"; the spec substitutes schema.md's four column lists without the owner's confirmation. | Plan line 822; SKILL.md lines 95–155. | Add Q-EX-0. |
| M11 | Medium | Q-EX-4, §2.2 | Having the model apply the stamp-duty reclassification contradicts "as printed"; the split is deterministic from printed figures. | SKILL.md §6. | Model returns printed header figures plus `stamp_duty_amount` and stamp-tax lines as printed; server and scorer apply the identical transform. |
| M12 | Medium | S-7 | Movement `description` scored exact against text the FDR sliced by fixed column positions will produce systematic misses. | `bnk_statement_parsers.py` line 158. | Score with a normalised token-overlap/prefix threshold (≥ 0.9); report exact. |
| L1 | Low | §4.1 | Commit subject is "Advance Stage 16 through Round 30". | `git log -1`. | Correct. |
| L2 | Low | §4.2 vs App. A | Appendix cites `Specs/*.txt`; §4.2 packages the `.md` rules (canonical). | diff. | Cite the .md only. |
| L3 | Low | §4.2 | schema.md "lines 17 and 78–99" also contain OFDGAP, Forecast, BNKACC, BNKMOV classification. | schema.md. | State the cut exactly; include BNKACC on purpose. |
| L4 | Low | §7 | Golden path `golden/extract-v2/` vs evidence path `evidence/extraction-v1/`. | repo. | One id in both. |
| L5 | Low | §2.6 | `CompanyMatcher.Match` takes one id; the two-sided gate is an extension. | `CompanyMatcher.cs`. | Say so. |
| L6 | Low | S-9 | Header cells for `bank_statement` documents undefined. | sample CSV. | Exclude explicitly. |
| L7 | Low | §5.5 | Known corrections should be pre-declared: Telles I24120001, Regus I26070005. | keys. | `answer-key.corrections.csv` at RED. |
| L8 | Low | §2.4, §6.1 | `direction` debit/credit vs Apollo's inflow/outflow/zero; `SyncEngine.ToCashDelta` tests `"CC"` but tenant #1's type is "Cartão de Crédito" → synced CC rows carry `cash_delta = +amount`. | `SyncEngineWave3.cs` 14–15; key #37. | Note the mapping; raise the sync defect separately. |
| L9 | Low | §3 #1, §5.1 | #32's R2 comes from a key stamp predating the ACR. | `I25120001.json`. | State that rule labels are taken as stored. |
| L10 | Low | §2.1, V-4 | `summary` length not stated (today 500). | `ExtractionContract.cs`. | Add. |

Owner questions the contract reviewer adds: Q-EX-0 (confirm schema.md's four lists = "section 5"), Q-EX-13 (`Doclog.DocDate` meaning for Apollo-native rows), Q-EX-14 (does a CLI/model change reopen the gate). Would answer differently: Q-EX-4 (server applies the stamp split), Q-EX-7 (95 % per kind; floor only on fields with ≥ 20 cells), Q-EX-9 (name the ten now), Q-EX-11 (no-op on the 40).

### Reviewer: feasibility — report verbatim

**Verdict:** REVISE — Counts: Critical 0 / High 10 / Medium 12 / Low 9

Runtime estimate: package ≈ 228 KB ≈ 60–65k tokens plus Read line-number overhead; a 7-page PDF ≈ 14k text tokens + page images ≈ 10k; system prompt + tools ≈ 10–15k; context at answer time ≈ 100–110k tokens over ~12 Read turns; output for 87 movements ≈ 12–14k tokens → 3.5–4 min of generation at ~60 tok/s before extended thinking. Worker today: `ClaudeTimeoutSeconds` 240, `LeaseSeconds` 300, `Slots` 2, `MaxAttempts` 5, backoff 30/120/600/1800/3600 s, no `--model`/`--effort`. What breaks first: the 240 s timeout on statements and 9–11-line invoices; the subscription's 5-hour window mid-bench; the "whole tree" fallback (≈360k tokens) fits no context.

| id | sev | section | finding | evidence | suggested fix |
|---|---|---|---|---|---|
| F-1 | High | §4.2 | The CLI's `Read` truncates lines > 2,000 chars; "byte-for-byte" delivery loses the field list: schema.md 78–99 has 5 lines over 2,000 chars (max 8,014), SKILL.md §1–7 has 3, one engagement rule has 1. | `awk length>2000` at `a558523`. | Build script reflows long lines (≤1,900) and records the transformation in `manifest.json`; or deliver via `--append-system-prompt`. Verify on the pinned CLI with a `stream-json` trace. |
| F-2 | High | §4.3 | 240 s timeout vs 5–7 min for an 87-movement statement; a timeout is a plain Retry → 5 attempts burned. | `ClaudeCli.cs`; `WorkerOptions.cs`; `QueueWorker.cs`. | Per-kind timeout (≥ 900 s for statements / > 5 pages), `LeaseSeconds` ≥ timeout, timeouts counted toward a lane pause. |
| F-3 | High | §4.3, §2.5 | Neither model nor effort is pinned or recorded; the 95 % is not reproducible. | `ClaudeCli.cs`; `ProcessingEvidence` has `CliVersion` only. | `WorkerOptions.Model/Effort` → `--model`/`--effort`; record both plus `modelUsage` in `ProcessingEvidence`. |
| F-4 | High | §2.6, §8 | The worker's live company gate reads top-level `recipient_tax_id`; a v2 answer yields "no recipient tax id" → every gateway document lands in triage. | `QueueWorker.AssignCompanyFromResultAsync`. | In this slice: read by contract id; test #22/#23 through the worker path. |
| F-5 | High | §8 | Release order unstated; `Documents.razor` reads flat v1 fields → v2 rows render blank; corrections stamp `apollo.extract.v0`. | `Documents.razor` 310–366, 524–525; `IngestionService.cs` 307. | Web reads by contract id and ships with or before the worker; corrections stamp the source contract; §8 orders web → worker. |
| F-6 | High | §2.4, §3 #10 | For card statements the FDR's `DocDate` is the printed DATA DA TRANSACÇÃO. | `build_bnkmov.py`; key BPI-CC. | Add `transaction_date`; `DocDate = embedded_date ?? transaction_date ?? posting_date`. |
| F-7 | High | §5.3 S-2 | The Revolut export is newest-first, the key oldest-first; all 51 Revolut cells miss by construction. | REV pdftotext; INDEX.md. | Pair on `(posting_date, amount, N-4 description, occurrence)`; `seq` only for V-6. |
| F-8 | High | §2.3 | The MEO line-count rule is wrong: the summary page prints one line; the key's 8 lines are the detail's category subtotals. | MEO dump; key `flagReviewNotes`. | Itemise a detail section's category subtotals, never per-subscription rows; summary-only when no detail exists. |
| F-9 | High | §5.3 S-6 | "service_period null iff rule ≠ R1" and account_period via R1 punish honest reads: MEO prints "PERÍODO CONTAGEM 01/06/2026 A 30/06/2026" but the key is R3/202607. | key I26070038; MEO dump. | Score `service_period` on R1 rows only; `account_period` under the key's own rule. |
| F-10 | High | §4.5 | Exfiltration path: `--allowedTools Read` is unscoped, the CLI runs as `.\SibylaWorker` which reads `worker.json` and the key ring; a document can ask for them within a shape-valid answer (~120 KB free text). | `ClaudeCli.cs`; `Program.cs`; V-4 caps. | Path-scoped `Read(<sandbox>/**)`, `Read(<package>/**)` plus explicit denies for the secrets roots; corpus (b) gains the out-of-sandbox read case; evidence scan for secret markers. |
| F-11 | Medium | §2.8 V-4 | `movements ≤ 500` cannot be emitted (≈75k output tokens vs 32k). | estimate. | Cap ≈150 per answer; paging or "held for a person" beyond. |
| F-12 | Medium | §5.3 S-7 | `running_balance` "where the statement prints one" decided by the extraction's own null; CC key rows carry computed balances. | BPI-CC dump; key. | Per-file `prints_running_balance` flag in the key; CC balances not scored. |
| F-13 | Medium | §2.4, §6 | Tenant #1's CC rows have `cashDelta = amount`, `direction = inflow` for purchases; `Bnkmov.RunningBalance` non-nullable, `Direction` required; `debit|credit` ≠ `inflow|outflow`. | keys; `Bank.cs`. | §6 states the mapping and the computed-chain fallback. |
| F-14 | Medium | §5.3 S-9 | `company`/`origin_class` via the gate on documents that print no recipient tax id (Alibaba, eSIMGo, Regus; VFX name only). | dumps. | Score the gate only where the document prints an id (key flag). |
| F-15 | Medium | §5.3 S-10 | 85 % floor on tiny denominators (`date_pay` 6, `service_period` 12). | key stats. | Floor gates at n ≥ 10, otherwise reported. |
| F-16 | Medium | §2.2, §2.6 | `date_doc` nullable (VFX prints only a due date) but `Doclog.DocDate` is required and R3 needs it. | VFX dump; `DocumentCapture.cs`. | `DocDate = date_doc ?? date_due` flagged; pre-registered key correction for #12. |
| F-17 | Medium | §4.2 | Package carries tenant #1 content (a NIF example, company names, two private individuals) into every tenant's run. | grep over the cut ranges. | Owner ruling: scrub or accept and record; never personal names. |
| F-18 | Medium | §4.3 | `--output-format json` leaves no tool trace; EXTRACT.md's read order unspecified. | `ClaudeCli.cs`. | Bench uses `stream-json`; evidence records `num_turns` and files read; EXTRACT.md lists the mandatory reads. |
| F-19 | Medium | §5, §8 | No re-processing path for v1 rows. | `IngestionService.cs` 440–460. | Define "re-extract under v2" or state v1 rows are not re-run. |
| F-20 | Medium | §4.3 | Deployment unnamed: package location, ACL, `WorkerOptions` keys, module step, rollback pairing. | run record 7o; `provision-production.ps1`. | Add §4.6 deployment/rollback. |
| F-21 | Medium | §2.8, §6.3 | Dead-lettered v2 documents fall to a v1 header form; statements have no human completion path. | `SubmitReviewAsync`. | State the interim or pull a minimal grid forward. |
| F-22 | Medium | §5.7 | "One sitting" for 50 docs × ~12 turns ≈ a full 5-hour window; `LanePauseMinutes` 10 re-fails. | `QueueWorker.PauseLaneAsync`. | Record the plan; bench resumes from `actual/`; longer pause on usage-limit signals. |
| F-23 | Low | §5.2 | Two oracles: JSON keys already exist while the spec re-cuts CSVs. | INDEX.md. | Adopt/version the JSON keys; record the sync run id. |
| F-24 | Low | §3 #12, §5.1 #11 | Placeholders the `XX-SYN` rule misses: `BEL999999`; eSIMGo's document id. | keys. | Explicit placeholder list → expected `null`. |
| F-25 | Low | §5.7 | "Up to 3 attempts" vs `MaxAttempts` 5. | `QueueJob.cs`. | Correct or set `MaxAttempts` for document jobs. |
| F-26 | Low | §5.6 | No draw or seed in `build_sample.py`. | grep. | Define the draw (SQL `ORDER BY sha256(entry_code||salt)`) and record it. |
| F-27 | Low | §4.2 | schema.md 78–99 also carries OFDGAP, BNKACC and a Stage 5 note. | lines 89/93/99. | Name exact ranges per paragraph in the manifest. |
| F-28 | Low | §5.3 S-4 | S-4 cannot tell printed from assumed; 15 keys are exactly +30. | key stats; MEO dump. | Key flag `date_due_printed` from the FDR flag notes. |
| F-29 | Low | §4.2 | The CLI auto-loads `CLAUDE.md` from `--add-dir`. | find. | Build script refuses `CLAUDE.md`/`.claude` in the package. |
| F-30 | Low | §4.2 | The "whole tree" fallback cannot fit any context. | zip listing. | Strike it. |
| F-31 | Low | §6, Q-EX-10 | `DocumentCaptureService.RegisterAsync` refuses while sync runs exist (C6). | `DocumentCaptureService.cs` 44–47. | Say so in §6. |

Spot-check (document / field / key / PDF / match): MEO document id, dates, totals yes; MEO line count = detail subtotals (F-8); MEO service period printed but key R3 (F-9); Locarent NC yes; VFX date_doc not printed (F-16); eSIMGo placeholder id (F-24) and a company number as fiscal id; Tranquilidade id/date/total yes, due via +30; Alibaba yes, synthetic → null, gate unresolvable (F-14); AWS yes (converted USD detail, V-9 holds); BPI-CC count yes, V-14 holds, statement prints opening/closing (spec says none), DocDate derivation wrong (F-6); Revolut opening/closing printed (spec's "not printed" wrong), newest-first (F-7).

Owner questions the feasibility reviewer adds: model and effort pinned and recorded, and which Claude plan (F-3, F-22); a v2-aware review page in this slice or worker waits for §6.3 (F-5, F-21); card-statement `DocDate` = printed transaction date (F-6); re-processing policy for v0/v1 rows (F-19); tenant #1 content in the package (F-17); line-count doctrine for summary-plus-detail bills (F-8); path-scoped `Read` with explicit denies before the hostile run (F-10).

### Author's dispositions — revision 2

Revision 2 of `apollo-argus-extraction-v1-spec.md` (2026-09-06). Dispositions: **fixed** (the spec now says it), **owner question** (folded into §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 2.

**Contract reviewer**

| id | Sev | Disposition | Where in revision 2 |
|---|---|---|---|
| H1 | High | fixed — movements paired by the natural key `(posting_date ↔ movDate, amount, N-4 description, occurrence)`, position never used; the JSON keys' `(doc_date, bm_code)` order is stated as not print order; `seq` serves V-6 only | §5.3 S-2, §5.2, §2.4 |
| H2 | High | fixed — `transaction_date` added; `DocDate = embedded_date ?? transaction_date ?? posting_date`; `value_date` kept for BCP's `DataValor` only; owner question Q-EX-17 confirms the card rule | §2.4, §3 #10, §1.2 Q-EX-17 |
| H3 | High | fixed — `account_period` scored only on rows stamped R1 or R3 (24); blank/R2/R4 reported; 2025 rows report-only | §5.3 S-6 |
| H4 | High | fixed — `service_period` scored on the 12 R1 rows only; agreement elsewhere reported (MEO/EDP/Avis named) | §5.3 S-6, S-8, §3 #1 |
| H5 | High | fixed — `date_due` null accepted only where the key's `dateDuePrinted = false` (the FDR's own "defaulted… assumption" notes, 7 rows, verified against the PDFs for the other +30 rows) or on a receipt with `dateDue = dateDoc`; else exact | §5.3 S-4, §5.2 flags file |
| M1 | Medium | fixed — magnitude rule: `sign_by_kind × |value|` compared and stored; printed negatives on positive invoices (#6, #31) keep their sign; Q-EX-5 rewritten | §5.3 S-3, §2.6 Signs, Q-EX-5 |
| M2 | Medium | fixed — `payment_proof {kind, printed_date}` replaces `date_pay`; `DatePay = DateDoc` derived server-side (FDR rule 1) and the derived value scored on the 6 receipt rows | §2.2, §2.6, §5.3 S-5 |
| M3 | Medium | fixed — per-field floor only on fields with ≥ 20 scored cells; smaller fields reported | §5.3 S-10, Q-EX-7 |
| M4 | Medium | fixed — three gates, 95 % per kind (header / lines / movements); no single aggregate | §5.3 S-1, S-10, Q-EX-7 |
| M5 | Medium | fixed — per-statement `printsOpeningBalance` / `printsClosingBalance` / `printsRunningBalance` flags in `answer-key.flags.json`; #37's computed balances unscored | §5.2, §5.3 S-7 |
| M6 | Medium | fixed — the 40 JSON keys adopted as the versioned oracle (copied with `INDEX.md` and `_sql`); Q-EX-11 recorded as closed by evidence (`fdchdr.fiscal_no == entmst.fiscal_no` on all 40); the order defect handled by H1 | §5.2, Q-EX-11 |
| M7 | Medium | fixed — the ten reserve documents named (I26060063, I26080036, I26070080, I26070007, I26070026, I26010011, I26010042, I25080001, BPI-CC-HEC-EUR_202507, BCP-DO-GOT-EUR_202506); the non-existent draw struck | §5.6, Q-EX-9 |
| M8 | Medium | fixed — bench is a console project `Sibyla.Tools.ExtractionBench`, outside `local\test.ps1` | §5.7 |
| M9 | Medium | owner question — Q-EX-13 added with the recommendation (document date on Apollo-native rows, synced rows keep the FDR value; alternative `CapturedAt`) | §1.2 Q-EX-13, §2.6, §3 #14 |
| M10 | Medium | owner question — Q-EX-0 added: confirm that "section 5" means the four schema.md column lists | §0, §1.2 Q-EX-0 |
| M11 | Medium | fixed — the model returns printed header figures, `stamp_duty_amount` and any printed stamp-tax line; server and scorer apply the identical split function; Q-EX-4's recommendation changed to "server" | §2.2, §2.3 `kind`, §2.6, §5.3 S-3, Q-EX-4 |
| M12 | Medium | fixed — movement description scored by N-4 token overlap ≥ 0.9 or prefix; exact rate reported | §5.3 S-11, S-8, §2.7 N-4 |
| L1 | Low | fixed — "Advance Stage 16 through Round 30" | §4.1 |
| L2 | Low | fixed — `.md` rules only; `.txt` copies never used; Appendix A cites the `.md` files | §4.2, Appendix A |
| L3 | Low | fixed — exact ranges: schema.md 17, 78–87, 93 (BNKACC on purpose), 95; 89/91/97/99 excluded by name | §4.2 |
| L4 | Low | fixed — one id `extract-v2` for goldens and evidence paths | §7, §5.7, §4.5 |
| L5 | Low | fixed — stated that `CompanyMatcher.Match` takes one id and the two-sided gate is an extension | §2.6 |
| L6 | Low | fixed — bank statements have no header cells; their statement cells are named | §5.3 S-9 |
| L7 | Low | fixed — pre-declared corrections: Telles I24120001, Regus I26070005, VFX I26050010 `dateDoc` | §5.5 |
| L8 | Low | fixed (mapping) / raised separately (defect) — `debit|credit` → `CashDelta` → Apollo's `inflow|outflow|zero` stated; the `ToCashDelta("CC")` vs "Cartão de Crédito" sync defect named as its own plan item, outside this contract | §2.4, §3 #15, §6.1 |
| L9 | Low | fixed — rule labels taken as stored (I25120001's R2 predates the ACR) | §5.3 S-6 |
| L10 | Low | fixed — `summary` ≤ 500 | §2.1, §2.8 V-4 |

**Feasibility reviewer**

| id | Sev | Disposition | Where in revision 2 |
|---|---|---|---|
| F-1 | High | fixed — build script reflows lines > 1,900 chars at word boundaries (never inside a backtick span), records every reflowed line in `manifest.json` with source and package hashes; the reflow manifest is part of the tree hash; `SkillPackageTests` asserts no package line exceeds 1,900. `--append-system-prompt` rejected: files are hashable evidence, a prompt argument is not | §4.2, §4.4, §7 |
| F-2 | High | fixed — per-kind timeouts (`TimeoutSecondsInvoice` 600, `TimeoutSecondsStatement` 900, also for > 5 pages); `LeaseSeconds` validated ≥ largest timeout + 60 at start; timeouts count toward the lane pause; a statement retries once at 1.5 × then dead-letters held | §4.3, §7 |
| F-3 | High | fixed + owner question — `WorkerOptions.Model` / `.Effort` passed to the CLI and recorded with `ModelUsage`, `NumTurns`, `TimeoutSeconds` in `ProcessingEvidence`; the tuple is the gate configuration; Q-EX-15 asks the owner to name model, effort and plan | §4.3, §4.6, §1.2 Q-EX-15 |
| F-4 | High | fixed — `ExtractionResult.ReadIdentities` switches on the contract id (v0/v1 top-level, v2 header identities, two-sided) in `QueueWorker.AssignCompanyFromResultAsync`, in this slice; tested through the worker path on the BICS pair and Regus | §4.7, §7 |
| F-5 | High | fixed + owner question — web reads by contract id and ships before the worker; `SubmitReviewAsync` stamps the source contract instead of the `apollo.extract.v0` literal; release order web → worker in §4.6/§8; Q-EX-16 records the choice (v2-aware page in this slice) | §4.7, §4.6, §8, Q-EX-16 |
| F-6 | High | fixed — as H2 (`transaction_date`, the three-way `DocDate` rule); Q-EX-17 | §2.4, §3 #10 |
| F-7 | High | fixed — natural-key pairing (H1); the Revolut newest-first order named; `printOrder` flag in the key | §5.3 S-2, §5.1 #40, §5.2 |
| F-8 | High | fixed + owner question — line doctrine rewritten: a detail section's category subtotals, never per-subscription rows; summary-only when no detail exists; aggregator sub-invoices are lines; written into `EXTRACT.md`; Q-EX-20 | §2.3, Q-EX-20 |
| F-9 | High | fixed — as H3/H4 (`service_period` on R1 rows only; `account_period` under the key's own stamp; MEO named) | §5.3 S-6, §3 #1 |
| F-10 | High | fixed + owner question — path-scoped `Read` on the sandbox and the package with explicit denies on the secrets roots, `worker-claude`, `dp-keys`, `staging`, `C:\Apps\Sibyla` outside the package and `local\`; the hostile run gains the out-of-sandbox read case; an evidence scan for secret markers gates acceptance; Q-EX-21 | §4.5, §7, §8, Q-EX-21 |
| F-11 | Medium | fixed — `movements ≤ 150`; `movements_truncated` + `movements_printed_count` (V-19): accepted, held for a person, no import; bench scores missing movements as misses | §2.4, §2.8 V-4/V-19, §4.7 |
| F-12 | Medium | fixed — as M5 (`printsRunningBalance` per statement; #37 unscored) | §5.2, §5.3 S-7 |
| F-13 | Medium | fixed — `CashDelta`/`Direction` mapping to Apollo's vocabulary stated; `RunningBalance` non-nullable → computed-chain fallback flagged; `Direction` required | §2.4, §6.1 |
| F-14 | Medium | fixed — the gate (`company`, `origin_class`) and `fiscal_no` scored only where the key's `printsRecipientTaxId` / `printsIssuerTaxId` flag is true; Alibaba, eSIMGo, Regus excluded and their triage outcome reported | §5.3 S-9, §5.4 |
| F-15 | Medium | fixed — floor only at ≥ 20 cells (the contract reviewer's M3 threshold, the stricter of the two proposals); smaller fields reported | §5.3 S-10 |
| F-16 | Medium | fixed — `date_doc` nullable; server fallback `date_doc ?? date_due` flagged; VFX pre-declared correction; `Doclog.DocDate` fallback in Q-EX-13's recommendation | §2.2, §2.6, §5.5, Q-EX-13 |
| F-17 | Medium | owner question — Q-EX-19: ship the excerpt unscrubbed but recorded, personal names masked by the build script and listed in `manifest.json` | §1.2 Q-EX-19, §4.2 rule (3) |
| F-18 | Medium | fixed — bench runs `stream-json`; evidence records `NumTurns` and `FilesRead`; `EXTRACT.md` states the mandatory read order | §4.3, §4.2 |
| F-19 | Medium | owner question — Q-EX-18: no automatic re-processing; "re-extract under v2" as a person's action with a new job and appended evidence | §1.2 Q-EX-18, §4.7 |
| F-20 | Medium | fixed — §4.6 added: package inside the worker release (`C:\Apps\Sibyla\worker\<release>\skill\`, existing `.\SibylaWorker` read ACL), `WorkerOptions` keys, module deployment per run record §7o, rollback pairing, the gate-configuration policy | §4.6 |
| F-21 | Medium | fixed + owner question — dead-lettered v2 documents show the v2 form; statements that dead-letter or truncate are held for a person with the page saying no completion path exists until §6.3; Q-EX-16 | §4.7, Q-EX-16 |
| F-22 | Medium | fixed — bench plan: two sittings, resume from `actual/`, usage-limit signal = stop until the window resets (not the 10-minute lane pause), every attempt recorded | §5.7 |
| F-23 | Low | fixed — as M6 (JSON keys adopted; sync run id in `answer-key.meta.json`) | §5.2 |
| F-24 | Low | fixed — closed placeholder list in N-7 (`XX-SYN-…`, `BEL999999`, the eSIMGo id) → expected `null`; kept in the flags file | §2.7 N-7, §5.2 |
| F-25 | Low | fixed — `MaxAttempts` = 5 stated; statement retry budget in §4.3 | §2.8, §4.3, §4.7, §5.7 |
| F-26 | Low | fixed — the draw struck; the ten reserve documents named explicitly (M7) | §5.6 |
| F-27 | Low | fixed — exact per-paragraph ranges (L3) | §4.2 |
| F-28 | Low | fixed — `dateDuePrinted` per document in the flags file, seeded from the FDR notes and verified against the PDFs | §5.2, §5.3 S-4 |
| F-29 | Low | fixed — build refuses `CLAUDE.md` / `.claude/` / any file not in the manifest | §4.2 rule (2), §7 |
| F-30 | Low | fixed — whole-tree fallback struck | §4.2, Q-EX-2 |
| F-31 | Low | fixed — the sync-era refusal (C6) named in §6.1 with its consequence for tenant #1 | §6.1 |

Owner questions added or changed on the reviewers' word, all in §1.2 with a recommendation: Q-EX-0 (schema.md lists = "section 5"), Q-EX-13 (`Doclog.DocDate`), Q-EX-14 (a CLI/model/effort/package change reopens the gate), Q-EX-15 (model, effort, plan), Q-EX-16 (v2-aware review page in this slice; dead-letter interim), Q-EX-17 (card `DocDate`), Q-EX-18 (re-processing), Q-EX-19 (tenant content in the package), Q-EX-20 (line doctrine), Q-EX-21 (path-scoped `Read`). Recommendations changed from revision 1: Q-EX-4 (server applies the split), Q-EX-7 (95 % per kind, floor at ≥ 20 cells), Q-EX-9 (ten named now), Q-EX-11 (closed by evidence, no ruling needed).

## Round 2 — revision 2, head `e83445d` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-1 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 1 / 5 / 8 | 57 of 58 (M1 partially) |
| feasibility | REVISE | 0 / 1 / 9 / 4 | 52 of 58 (H1, F-2, F-7, F-19 partially) |

Both reviewers raise the same High independently: S-2 makes the exact N-4 description a pairing precondition, and the Revolut key's descriptions are the FDR's column-sliced cells (USD amount and exchange-rate sub-line included), so 17 of 51 Revolut movements can never pair. Finding ids below are per reviewer (contract N-1..N-14 and feasibility N-1..N-14 are different lists).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** e83445d (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 5 / Low 8

Every round-1 High is CLOSED as specified. The REVISE comes from one new High (S-2's pairing key is not computable on the Revolut answer key as it exists) and five Mediums, four of which make tests named in §7 impossible to type with an expected value.

#### Round-1 revisit

Contract reviewer (H1–H5, M1–M12, L1–L10)

| id | State | Evidence in revision 2 |
|---|---|---|
| H1 | CLOSED | §5.3 S-2 pairs on `(posting_date ↔ movDate, amount, N-4 description, occurrence)`, "position is never used"; §5.2 says the JSON order `(doc_date, bm_code)` is not print order; §2.4 `seq` "used by V-6 only". The suggested key was adopted verbatim; its description component fails on the keys as they exist — new finding N-1. |
| H2 | CLOSED | §2.4 `transaction_date` row; bold rule `DocDate = embedded_date ?? transaction_date ?? posting_date`; `value_date` = BCP `DataValor` only; §3 #10; Q-EX-17. Verified against `build_bnkmov.py` 77–84 at a558523. |
| H3 | CLOSED | S-6: `account_period` compared "only on rows stamped R1 or R3 (24)"; blank/R2/R4 reported; 2025 report-only. Counts verified on the keys: R1 12, R3 12, blank 10, R2 1. |
| H4 | CLOSED | S-6: `service_period` "scored on the 12 R1 rows only"; S-8 reports agreement elsewhere; §3 #1 names MEO/EDP/Avis. Verified EDP prints "Período de faturação: 2 de julho a 1 de agosto 2026", Avis "Rentals 2026-07", MEO "PERÍODO CONTAGEM 01/06/2026 A 30/06/2026" (key R3). |
| H5 | CLOSED | S-4: null matches only where `dateDuePrinted = false` or a receipt with `dateDue = dateDoc`; §5.2 flags file. Verified: all 6 Invoice-Receipt keys have dateDue = dateDoc; the FDR notes carry 5 "+30 assumption" rows and 2 credit-note "= DateDoc" rows (wording, N-11). |
| M1 | PARTIALLY | S-3 magnitude rule and §2.6 Signs exist, but §2.6 gives `stamp_tax` lines "the document's kind" while #6's key and PDF hold −0.03 on a positive invoice — the finding's own case still misses. N-3. |
| M2 | CLOSED | §2.2 `payment_proof.kind/printed_date`; §2.6 "DatePay (FDR rule 1)"; S-5 scores the derived value on the 6 receipt rows. Matches schema.md line 82. |
| M3 | CLOSED | S-10 floor "≥ 20 cells"; Q-EX-7. |
| M4 | CLOSED | S-1 "Three scores, one per kind … No single aggregate"; S-10. |
| M5 | CLOSED | §5.2 `printsOpeningBalance/printsClosingBalance/printsRunningBalance`; S-7 "#37: false — FDR-computed". Verified: the card PDF prints no balance column; key balances run from 270.03. |
| M6 | CLOSED | §5.2 adopts the 40 JSON keys with `INDEX.md` and `_sql`; Q-EX-11 "closed by evidence". Verified `fdchdr.fiscal_no == entity.fiscal_no` on all 35 fiscal keys. |
| M7 | CLOSED | §5.6 names ten; all eight entry codes are in `05-doclog-all.txt`, both bank PDFs are on disk. Two entries have duplicate doclog rows (N-12). |
| M8 | CLOSED | §5.7 console project `Sibyla.Tools.ExtractionBench`; `local/test.ps1` has no trait filter (verified). |
| M9 | CLOSED | Q-EX-13 with recommendation and alternative; §2.6; §3 #14. Count is 11 not 10 (N-10). |
| M10 | CLOSED | §0 last paragraph; Q-EX-0. Plan line 822 says "section 5 field list"; SKILL.md §5 = lines 95–155, no field list (verified). |
| M11 | CLOSED | Q-EX-4 "Server"; §2.2 `stamp_duty_amount`; §2.3 `kind`; §2.6 split; S-3. The transform's condition is not decidable as written — N-4. |
| M12 | CLOSED | S-11 overlap ≥ 0.9 or prefix; exact rate in S-8; N-4. |
| L1 | CLOSED | §4.1 "Advance Stage 16 through Round 30" — verified `git log -1 a558523`. |
| L2 | CLOSED | §4.2 "`.md` canonical copies only", rule (4); Appendix A cites `.md`. The five `.txt` copies exist at a558523. |
| L3 | CLOSED | §4.2 ranges 17, 78–87, 93, 95; 89/91/97/99 excluded — verified line contents. |
| L4 | CLOSED | `extract-v2` in §4.4, §4.5, §5.7, §7. |
| L5 | CLOSED | §2.6 "takes one id today; the gate extends it" — `CompanyMatcher.Match(string?, IEnumerable<Candidate>)` verified. |
| L6 | CLOSED | S-9 "a bank statement has no header cells". |
| L7 | CLOSED | §5.5 pre-declared Telles, Regus, VFX. |
| L8 | CLOSED | §2.4 mapping to `inflow/outflow/zero`; §3 #15 raises the `"CC"` defect separately. Verified `SyncEngineWave3.cs` 14–15 and key accountType "Cartão de Crédito". |
| L9 | CLOSED | S-6 last sentence. |
| L10 | CLOSED | §2.1 "≤ 500 chars"; V-4; matches `ExtractionContract.cs` line 117. |

Feasibility reviewer (F-1…F-31)

| id | State | Evidence in revision 2 |
|---|---|---|
| F-1 | CLOSED | §4.2 build rule (1) reflow ≤ 1,900, manifest with hashes; §4.4 `reflow=` in the tree hash; §7 `SkillPackageTests`. Verified: 5 lines > 2,000 in schema.md 78–99 (max 8,014), 3 in SKILL.md 16–409, 1 in Financial Document Entry Policy. |
| F-2 | CLOSED | §4.3 `TimeoutSecondsInvoice` 600 / `TimeoutSecondsStatement` 900, `LeaseSeconds` ≥ timeout + 60, pause on two timeouts, statement retry at 1.5×. Today's values 240/300 verified. |
| F-3 | CLOSED | §4.3 `Model`/`Effort` options and evidence fields; §4.6 tuple; Q-EX-15. CLI version itself not enforced — N-6. |
| F-4 | CLOSED | §4.7 `ExtractionResult.ReadIdentities` by contract id; §7 `QueueWorkerGateTests`. Verified `QueueWorker.cs` 276 reads top-level `recipient_tax_id`. Both-match case undefined — N-5. |
| F-5 | CLOSED | §4.7 review page by contract id, `SubmitReviewAsync` stamps the source contract; §4.6/§8 web → worker. Verified `Documents.razor` 324/362/525 and `IngestionService.cs` 307 literal `apollo.extract.v0`. |
| F-6 | CLOSED | As H2. |
| F-7 | CLOSED | S-2 natural key; §5.1 #40 "printed newest-first"; `printOrder` flag. Verified newest-first in the PDF. See N-1. |
| F-8 | CLOSED | §2.3 doctrine (a)–(e); Q-EX-20. Verified MEO's detail prints "Total de ADC001897 …" category subtotals. |
| F-9 | CLOSED | As H3/H4. |
| F-10 | CLOSED | §4.5 path-scoped `Read` with the deny list; hostile run gains the out-of-sandbox read; evidence scan; Q-EX-21. Verified `ClaudeCli.cs` 78–79 `--allowedTools Read` unscoped. |
| F-11 | CLOSED | V-4 `movements ≤ 150`; V-19; §4.7 held for a person. |
| F-12 | CLOSED | As M5. |
| F-13 | CLOSED | §2.4 derived list; §6.1 computed-chain fallback, `Direction` required. Verified `Bank.cs` 40–46. |
| F-14 | CLOSED | S-9 gate/fiscal_no under `printsIssuer/RecipientTaxId`; §5.4 excludes #13/#17. |
| F-15 | CLOSED | S-10 ≥ 20 cells. |
| F-16 | CLOSED | §2.2 `date_doc` nullable; §2.6 fallback flagged; §5.5 VFX. Verified VFX prints only "Data de vencimento 02/05/2026"; key dateDoc = dateDue = 2026-05-02. |
| F-17 | CLOSED | Q-EX-19; §4.2 rule (3). Verified the NIF example at SKILL.md line 36 is PT513362061. |
| F-18 | CLOSED | §4.3 bench `stream-json`, `NumTurns`, `FilesRead`; §4.2 read order in `EXTRACT.md`. |
| F-19 | CLOSED | Q-EX-18; §4.7 "Re-extract under v2", job id `docint:<id>:process:v2` (today's key is `docint:<id>:process`, verified). |
| F-20 | CLOSED | §4.6. |
| F-21 | CLOSED | §4.7 dead-letter interim; Q-EX-16. |
| F-22 | CLOSED | §5.7 two sittings, resume from `actual/`, usage-limit = stop. |
| F-23 | CLOSED | §5.2 `answer-key.meta.json` with the sync run id. |
| F-24 | CLOSED | N-7 closed list (`XX-SYN`, `BEL999999`, eSIMGo id) — all three verified in the keys. |
| F-25 | CLOSED | V-n header and §4.7 `MaxAttempts` = 5 (verified `QueueJob.cs` 43). |
| F-26 | CLOSED | Draw struck; §5.6. |
| F-27 | CLOSED | As L3. |
| F-28 | CLOSED | §5.2 `dateDuePrinted`. |
| F-29 | CLOSED | §4.2 rule (2). |
| F-30 | CLOSED | §4.2 "There is no whole-tree fallback"; Q-EX-2. |
| F-31 | CLOSED | §6.1 names C6; verified `DocumentCaptureService.cs` 42–45. |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| N-1 | High | §5.3 S-2 (also S-11, §7 "natural-key pairing on a shuffled Revolut key") | Pairing requires N-4 **equality** of the description. The Revolut key's descriptions are the FDR's slice of the whole multi-line cell — description **plus the USD amount column plus the exchange-rate line** — so 17 of 51 Revolut movements and 1 of 87 BPI movements can never equal what the statement prints as the description; each unpaired movement misses every cell (≈ 18 × 7 ≈ 126 of ≈ 1,380 movement cells, ≈ 9 %). The 95 % movement gate is unreachable by scorer design. S-11 already concedes equality is unreliable and uses ≥ 0.9/prefix for *scoring*; S-2 contradicts it for *pairing*. | Key `REV-DO-GOT-EUR_202512.json`: `"Para Sinch Mobile AB – Payment Ref: 410353417, Invoice No.: 410353417   $127.51 Taxa de câmbio 1 EUR = 1.038078 USD, Taxa: 5.00"`; PDF (pdftotext raw) prints the description on its own line and the rate on the next. Key `BPI-DO-GOT-EUR_202507.json`: `"TRF CRED NÃO SEPA+ EMITIDA 07865284 FAT-20250422904233   -2 5"` (amount fragment). Verbatim test after N-4 over the five PDFs: 0/46, 1/87, 0/3, 17/51, 0/10 not found. | Pair on `(posting_date ↔ movDate, amount, currency)`; break ties by S-11 similarity, then occurrence; the description is never a pairing precondition. Additionally pre-declare the 18 defective key descriptions in `answer-key.corrections.csv` (1.3 % of scored cells, page refs) so S-11 scores them against what is printed. |
| N-2 | Medium | §5.3 S-7 (statement balances), §5.2 | `opening_balance/closing_balance ↔ bnkchk` names no bnkchk row and has none for two of five statements. Revolut prints "Saldo de abertura 40.06" for 1 Dec 2024–31 Dec 2025; the key's `bnkchk` starts at 202501 (opening 30.06) — no 202412 row (INDEX.md observation). The card statement prints "Saldo em dívida à data do extracto anterior 270,03" = `bnkchk[202603].opening`, but its first period by DocDate is 202602 (`bnkchk[202602].opening` = 599.30). A "first period" reading misses both; a "file-name period" reading misses Revolut. | `REV-DO-GOT-EUR_202512.json` `bnkchk` (12 rows, 13 periods); `BPI-CC-ROA-EUR_202603.json` `bnkchk`; PDFs. | Hand-key `printedOpeningBalance`/`printedClosingBalance` (with page refs) into `answer-key.flags.json` beside the `prints*` flags; score against those, not `bnkchk`. V-14 verified to hold on the card with native signs (270.03 + 349.19 = 619.22). |
| N-3 | Medium | §2.6 Signs vs §5.3 S-3, Q-EX-5, §7 ("the magnitude sign rule on #6") | #6 prints its stamp as a totals-block footnote "(*) O valor indicado inclui um ajuste no Imposto do Selo no valor de −0,03" → per §2.3 it is `header.stamp_duty_amount = −0.03` and the server creates the `stamp_tax` line; §2.6 then says "`stamp_tax` lines follow the document's kind" → +0.03; the key holds −0.03 on line 3 → net and total miss. The "printed negative … keeps its printed sign" exception is written for item lines only. The test named in §7 cannot be typed to pass. The spec also never says where the server-created line is numbered, which S-2 needs. | PDF `Gott_Invoice_Tranquilidade_202603_02.pdf` line "(*) … − 0,03"; key `I26030044.json` line 3 = −0.03; keys #6/#25/#31 all carry "Stamp Tax" as the **last** line. | On invoices a `stamp_tax` line keeps the printed sign of the duty; on credit notes it is negative; the server appends the created line as `line_no = n + 1`. |
| N-4 | Medium | §2.6 stamp split; §5.3 S-3; §7 ("the split function on #25") | The split's condition "when the document folded the duty into its tax total" is not decidable from any contract field, so "server and scorer apply the identical function" is not yet a function. On #25 the printed figures are "Prémio antes de impostos", "Imposto de selo 19,15", "Total 411,54"; neither 392.39 (key line 1) nor 411.54-as-net is printed, so `net_amount` (key 411.54) and line 1 (392.39) are misses under "as printed" unless the no-net branch is defined. | PDF `Gott_InvoiceReceipt_Tranquilidade_202608_01.pdf`; key `I26080026.json` (net 411.54, vat 0, lines 392.39 + 19.15). | Make the split arithmetic: `net + vat + duty = total (± 0.02)` → duty outside VAT (Net += duty, VAT unchanged); `net + vat = total` and `vat ≥ duty` → folded (Net += duty, VAT −= duty); else no split, reconciliation finding. Add the no-net branch: `net_amount = null` and `vat_amount ∈ {null, 0}` → `Net = Total − VAT` before the split, and a single printed line whose amount is the total less the duty is the item line. Or exclude #25's net/line-1 cells by flag like #6 (N-7). |
| N-5 | Medium | §2.6 gate; §4.7; §5.3 S-9; §7 `QueueWorkerGateTests` | "Both → the intercompany pair" defines no company, flow or origin for the intake row, yet `AssignCompanyFromResultAsync` must assign one `CompanyId` and S-9 scores `company`/`origin_class` on #29 (issuer id printed, R row). #29 R26030003 has issuer Gott PT513362061 and recipient Itoorer IE3559150RH — both licence companies — and is stored Company GOTT / R / Internal. | Key `R26030003.json`; `QueueWorker.cs` 268–332. | Rule: when both match, the **issuer's** company owns the row as R/Internal (the FDR's #29 and SKILL.md §11); the counter entry belongs to the persistence slice. Add #29 to the gate tests beside #22/#23/#17. |
| N-6 | Medium | §4.3, §4.6 gate policy, Q-EX-14 | The CLI version is in the gate tuple and "production runs only that tuple", but nothing pins or enforces it: `ClaudeCliPath` = `claude` on PATH, `CliVersion` is only recorded, and the lane's refuse-to-start covers the package hash only. A CLI update silently changes the configuration that passed. | `WorkerOptions.cs` line 7; `ProcessingEvidence.cs` 25; §4.3 "refuses to start when the package hash does not match". | `WorkerOptions.CliVersion` (expected); the lane refuses to start when `claude --version` differs, same pattern as the package hash; the version named in the R-EX-3 report. |
| N-7 | Low | §5.3 S-9 | The #6 exclusion is keyed on the phrase "recorded the gross as Net pending the paired document"; the key's note reads "recorded the gross total as NetAmount with VATAmount=0.00 pending that document" — a phrase match never fires. | `I26030044.json` `flagReviewNotes`. | A per-document flag `netVatNotPrinted` in the flags file (also for #25), not a note phrase. |
| N-8 | Low | §5.3 S-1/S-2/S-9 | Statement cells (`movement_count`, opening/closing) belong to none of the three gates; S-2 calls them "scored header/statement cells", S-1 defines header/lines/movements only. | Text of S-1, S-2, S-9. | Assign them to the movements score or mark them report-only. |
| N-9 | Low | Q-EX-7 vs D-EX-3 | The ruling reads "field-level exact match on header and lines at … 95 percent"; the movements gate at 95 % is the spec's addition and Q-EX-7 does not say so. | Plan line 822. | State in Q-EX-7 that the movements gate goes beyond the ruling and ask the owner to confirm it. |
| N-10 | Low | Q-EX-13 | "10 of 35 keys have `doclog.docDate ≠ dateDoc`" — the keys hold 11. | Computed over the 35 keys. | Correct. |
| N-11 | Low | §5.2 flags, S-4 | "7 of 35 say 'DateDue defaulted … assumption'": 5 say +30 (I26020038, I26030044, I26040053, I26050001, I26060014) and 2 credit notes say "set equal to DateDoc" (I26030019, I26030031). S-4's parenthetical "(the FDR assumed +30)" is narrower than the flag. | Key notes. | "assumed (+30, or = DateDoc on a credit note)". |
| N-12 | Low | §5.6 | I26060063 and I26070007 each have two live doclog rows (LG000015 vs LG000914 "Duplicate"; LG000795 vs LG001770 unarchived); the reserve export selects by `doclog_code`, which the spec does not name. | `05-doclog-all.txt`. | Name LG000015 and LG000795 in `reserve-set.csv`. |
| N-13 | Low | §4.1 | "zip, 26 files, 1,433,312 bytes" is the uncompressed total; the file is 559,999 bytes. | `ls -l`, `unzip -l`. | Say "uncompressed". |
| N-14 | Low | §2.7 N-4, S-11, §5.5 | One BPI key description carries an amount fragment ("… FAT-20250422904233 -2 5") — a key defect to pre-declare as a correction, not to absorb through the 0.9 threshold. | `BPI-DO-GOT-EUR_202507.json`. | Pre-declared correction row. |

#### Owner questions Q-EX-0..21

All 22 recommendations are defensible as written. Q-EX-17 and Q-EX-20 are verified against the FDR's own code and the MEO PDF; Q-EX-11 is verified on the 35 fiscal keys. Two I would answer differently and one addition:

- **Q-EX-13** — the recommendation keeps one column with two meanings (Apollo-native = document date, synced = capture time), which is what M9 objected to. Answer with the alternative: `CapturedAt` as its own column and `DocDate` = document date for Apollo-native rows, with synced rows' meaning recorded on the entity until cutover.
- **Q-EX-7** — confirm the movements gate explicitly (N-9); the ruling names header and lines.
- **Add Q-EX-22** — the intercompany ownership rule (N-5): the issuer's company owns a both-match row as R/Internal. It is the FDR's practice (#29) but it decides which company a gateway document lands in, so the owner should rule it, not the author.

N-6 (CLI version enforcement) is design, not an owner question; a reviewer would insist on it before code because Q-EX-14's answer is otherwise unenforceable.

#### Readiness for RED oracles (§7)

Not yet. Sufficient and unambiguous: `ExtractionContractV2Tests` (accept/reject corpus per V-rule), `SkillPackageTests`, `ProcessingEvidenceTests`, `ClaudeCliTimeoutTests`, `ClaudeCliPermissionTests`, `EvidenceSecretScanTests`, `CompanyMatcherTests` (two-sided), `AccountPeriodTests`, `DocumentsReviewV2Tests`. Cannot be typed with an expected value from the spec as it stands: `ExtractionScoringTests` "natural-key pairing on a shuffled Revolut key" (N-1), "the magnitude sign rule on #6" (N-3), "the split function on #25" (N-4), the statement-balance cells (N-2), and `QueueWorkerGateTests` for a both-match row (N-5). Fixing N-1…N-5 is a handful of sentences each; nothing structural.

#### Verified myself

Pinned commit hash, date and subject; SKILL.md heading anchors 16/95/155/156/409/410/581/594/595 and the long-line counts; schema.md lines 17, 78–87, 89, 91, 93, 95, 97, 99 and the five > 2,000-char lines (max 8,014); the seven engagement-rule `.md` files and the five `.txt` copies; `build_bnkmov.py` 77–84; `bnk_statement_parsers.py` 150–165; `skill_currency.json`; the zip sizes; the plan's ruling at line 822 (and that it names header and lines only); `ExtractionContract.cs` (v1 id, `summary` 500, `recipient_tax_id`); `QueueWorker.AssignCompanyFromResultAsync` (top-level `recipient_tax_id`, `CompanyMatcher.Match` one id); `IngestionService.cs` 307 and 440–460; `WorkerOptions` (240/300/2/10); `ClaudeCli.cs` argument list; `QueueJob.MaxAttempts` 5; `SyncEngineWave3.ToCashDelta("CC")`; `DocumentCaptureService.cs` 42–45 (C6); `Bank.cs` (`RunningBalance` non-nullable, `Direction` required); `DocumentCapture.DocDate` required; `DocumentTypeRouter` map; `ProcessingEvidence` fields; `local/test.ps1` has no filter; all 40 keys: 35/5, 109 lines, 197 movements, rule counts, 6 receipts (all dateDue = dateDoc = datePay), qty/unit/vat-rate 6/6/8, fiscal-no equality, the three placeholders, the eSIMGo id, 11 doclog date differences, the DateDue notes; no natural-key collisions on `(movDate, amount, description)` in any bank key; #37's 10 rows are the whole statement (payment −270.03 and interest 0.06 included), print no running balance, and reconcile 270.03 + 349.19 = 619.22; Revolut prints 40.06/164.94, newest-first, 1 Dec 2024–31 Dec 2025, one 202412 movement without a bnkchk row; the description verbatim test over the five PDFs; VFX, MEO, EDP, Avis, Locarent NC, Tranquilidade #6 (footnote −0,03) and #25 (stamp 19,15 in the tax block, no printed net) text; the ten reserve entries exist (two with duplicate doclog rows).

#### Taken from the spec without verification

The CLI `Read` 2,000-character truncation and `CLAUDE.md` auto-load from `--add-dir`; the pinned CLI's support for per-path allow/deny and an effort flag; the token and context estimates (§4.2, §5.7) and the 12-turn/5-hour plan; the Continente gross unit price and Locarent per-line periods; DT000020 routing to DOCFAI; the module deployment and rollback mechanics of §4.6 (run record §7o); that `git archive` on this machine yields CRLF; the content of `EXTRACT.md` (not yet written).

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** e83445d (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 9 / Low 4

Revision 2 is a substantial and mostly verifiable rewrite; 52 of the 58 round-1 findings are closed by spec text I could check against code, PDFs or the pinned CLI. It is not ready for RED because the adopted movement-pairing key re-creates the H1/F-7 defect on the same Revolut file (High), and because several §4.3/§4.5/§4.7 operational statements cannot be built as written (kind hint before an answer exists, "held for a person" without a status, "previous result untouched" against an overwriting `UPDATE`, a bench that must read `commst` but may not read secrets).

#### Round-1 revisit

| id | Status | Spec section and text relied on |
|---|---|---|
| H1 | PARTIALLY | §5.3 S-2 pairs by "(posting_date ↔ movDate, amount, N-4 description, occurrence index)"; position is gone and the `(doc_date, bm_code)` order is stated as not print order (§5.2). But the key includes the exact N-4 description, which fails on the Revolut key by construction (N-1). |
| H2 | CLOSED | §2.4 `transaction_date`; "`DocDate = embedded_date ?? transaction_date ?? posting_date`"; §3 #10; Q-EX-17. BPI-CC PDF verified: "DATA DA TRANSACÇÃO / DATA DO MOVIMENTO" columns. |
| H3 | CLOSED | S-6: "`account_period` … compared to the key only on rows stamped R1 or R3 (24)". Key stats verified: R1 12 / R3 12 / blank 10 / R2 1. |
| H4 | CLOSED | S-6: "`service_period`: scored on the 12 R1 rows only … on other rows agreement is reported"; §3 #1 names MEO/EDP/Avis. |
| H5 | CLOSED | S-4: null matches only where "`dateDuePrinted = false`" or receipt with `dateDue = dateDoc`. Flag derivable (spot-check below); the "7 rows" count is wrong (N-12). |
| M1 | CLOSED | S-3 "`sign_by_kind(doc, line) × \|extraction\|`"; §2.6 Signs; Q-EX-5. |
| M2 | CLOSED | §2.2 `payment_proof {kind, printed_date}`; §2.6 "DatePay = DateDoc"; S-5 derived on 6 receipt rows (all 6 keys have `datePay = dateDoc`, verified). |
| M3 | CLOSED | S-10 "per-field floor of 85 % on any scored field with ≥ 20 cells". |
| M4 | CLOSED | S-1 "Three scores, one per kind … No single aggregate is the gate". |
| M5 | CLOSED | §5.2 `printsOpeningBalance/ClosingBalance/RunningBalance`; S-7 "#37: false — FDR-computed" (PDF confirms no balance column). Statement-level opening vs per-period `bnkchk` gap is N-10. |
| M6 | CLOSED | §5.2 "the 40 JSON files … copied unchanged"; Q-EX-11 "closed by evidence" — verified `fiscalNo == entity.fiscalNo` on all 35 fiscal keys. |
| M7 | CLOSED | §5.6 ten named; R9/R10 PDFs exist at the pinned commit (69,299 B and 449,890 B). |
| M8 | CLOSED | §5.7 "console project `Sibyla.Tools.ExtractionBench`"; `local\test.ps1` is `dotnet test Sibyla.slnx` with no filter — a console project is outside it. |
| M9 | CLOSED | Q-EX-13 with recommendation and alternative. |
| M10 | CLOSED | §0 and Q-EX-0; SKILL.md §5 at lines 95–155 verified as classification, no field list. |
| M11 | CLOSED | Q-EX-4 "Server"; §2.2 `stamp_duty_amount`; §2.3 `kind`; §2.6 split; S-3 applies the same function. |
| M12 | CLOSED | S-11 overlap ≥ 0.9 or prefix; exact rate reported (S-8). |
| L1 | CLOSED | §4.1 "Advance Stage 16 through Round 30" = `git log -1` at `a558523`. |
| L2 | CLOSED | §4.2 rule (4) ".txt copies … never used"; Appendix A cites `.md`. |
| L3 | CLOSED | §4.2 "17, 78–87, 93, 95; 89, 91, 97, 99 not included" — verified line contents (OFDGAP, Forecast, BNKMOV history, Stage 5 note; 93 = BNKACC). |
| L4 | CLOSED | `extract-v2` in §4.5, §5.7, §7. |
| L5 | CLOSED | §2.6 "`CompanyMatcher.Match` takes one id today; the gate extends it" (code: `Match(string? recipientTaxId, …)`). |
| L6 | CLOSED | S-9 "a bank statement has no header cells". |
| L7 | CLOSED | §5.5 three pre-declared rows. |
| L8 | CLOSED | §2.4 `debit\|credit` → `CashDelta` → `inflow\|outflow\|zero`; §3 #15 raises the `"CC"` defect separately. |
| L9 | CLOSED | S-6 "Rule labels are taken as stored". |
| L10 | CLOSED | §2.1 and V-4 "`summary` ≤ 500". |
| F-1 | CLOSED | §4.2 reflow at 1,900, recorded in `manifest.json`, part of the tree hash (§4.4). Counts verified: selected schema.md lines over 2,000 = 17, 78, 87, 93, 95 (five, max 8,014); SKILL.md §1–7 = 89, 164, 228 (three; 408 is 1,993 and is caught by the 1,900 threshold); rules = Financial Document Entry Policy line 53 (one). |
| F-2 | PARTIALLY | §4.3 timeouts 600/900, lease check, lane pause, statement retry at 1.5×. Not buildable as written: the kind hint has no source before the first answer and the worker has no page counter (N-2). |
| F-3 | CLOSED | §4.3 `--model`, effort, `ModelUsage`, `NumTurns` in evidence; Q-EX-15. `--model` and `--effort <low\|medium\|high\|xhigh\|max>` verified on CLI 2.1.259. |
| F-4 | CLOSED | §4.7 `ExtractionResult.ReadIdentities` by contract id; §7 tests #22/#23/#17. The both-match case is undefined (N-5). |
| F-5 | CLOSED | §4.7 review page by contract id; `SubmitReviewAsync` stamps the source contract (code today: line 307 literal `apollo.extract.v0`); §4.6/§8 web → worker. |
| F-6 | CLOSED | as H2. |
| F-7 | PARTIALLY | Order dependency removed (S-2) and `printOrder` flagged (§5.2); the exact-description key still misses ≥ 10 of 51 Revolut rows by construction (N-1). |
| F-8 | CLOSED | §2.3 doctrine (a)–(e); Q-EX-20. |
| F-9 | CLOSED | as H3/H4. |
| F-10 | CLOSED | §4.5 allow two roots, six denies, out-of-sandbox hostile case, evidence scan; Q-EX-21. Gaps in the flag set and retention are new (N-3, N-4). |
| F-11 | CLOSED | V-4 "`movements` ≤ 150"; V-19; §4.7. The hold has no status (N-6). |
| F-12 | CLOSED | as M5. |
| F-13 | CLOSED | §2.4 derivations; §6.1 "`RunningBalance` (non-nullable) = printed, else computed from the opening balance by the chain and flagged". |
| F-14 | CLOSED | S-9 gate cells "only where `printsRecipientTaxId` on I rows / `printsIssuerTaxId` on R rows"; §5.4 excludes #13, #17. |
| F-15 | CLOSED | S-10 ≥ 20 cells. |
| F-16 | CLOSED | §2.2 `date_doc` nullable; §2.6 fallback; §5.5 VFX correction. |
| F-17 | CLOSED | Q-EX-19; §4.2 rule (3). Company names verified present in the cut (Gott ×5, Confidencial ×4, Itoorer, Factor Matriz). |
| F-18 | CLOSED | §4.3 bench `stream-json`, `FilesRead`; §4.2 `EXTRACT.md` read order. |
| F-19 | PARTIALLY | Q-EX-18/§4.7 define the action, but "the previous result … kept untouched" contradicts `CompleteAsync` (`UPDATE docint SET processing_status = 3, result_json = $2`) and nothing says where the v2 result goes (N-7). |
| F-20 | CLOSED | §4.6 location, ACL (verified `provision-production.ps1` line 176: worker ReadAndExecute on `C:\Apps\Sibyla\worker`), keys, module, rollback (matches run record §7o: bare ids, `-Execute -Resume`). Publish mechanics unnamed (N-11). |
| F-21 | CLOSED | §4.7 dead-letter interim; Q-EX-16. Status representation is N-6. |
| F-22 | CLOSED | §5.7 two sittings, resume from `actual/`, usage-limit = stop. Bench identity unnamed (N-9). |
| F-23 | CLOSED | §5.2 `answer-key.meta.json` with the sync run id. |
| F-24 | CLOSED | N-7 closed list (`CN-SYN-000002`, `BEL999999`, eSIMGo id) — all three verified in the keys. |
| F-25 | CLOSED | §2.8/§4.3 `MaxAttempts` = 5 (`QueueJob.cs:43`). |
| F-26 | CLOSED | §5.6. |
| F-27 | CLOSED | §4.2 exact ranges. |
| F-28 | CLOSED | §5.2 `dateDuePrinted`; count corrected in N-12. |
| F-29 | CLOSED | §4.2 rule (2); no `CLAUDE.md`/`.claude` anywhere in the skill-build tree (find). |
| F-30 | CLOSED | §4.2 "There is no whole-tree fallback"; Q-EX-2. |
| F-31 | CLOSED | §6.1 names C6; `DocumentCaptureService.cs` 44–47 verified. |

#### New findings (revision 2)

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| N-1 | High | §5.3 S-2, N-4 | The pairing key includes the exact N-4 description. The Revolut key's descriptions are the FDR's column-sliced text and carry the FX sub-line and the USD amount at varying positions; an honest `description_printed` ("Para Sinch Sweden AB • Sinch INV100462170") never equals it, so ≥ 10 of 51 Revolut movements are unpaired by construction — every cell a miss (≈ 5 % of all 197 movements, i.e. the movements gate is at its 95 % floor before one real error). S-11's prefix tolerance applies to scoring, not to pairing. | Key rows e.g. `2025-01-31 -127.84 "Para Sinch Mobile AB • Payment Ref: 410353417, Invoice No.: 410353417 <100 spaces> $127.51 Taxa de câmbio 1 EUR = 1.038078 USD, Taxa:€5.00"`; `2025-09-12 -73.63 "… Sinch INV100429071 Taxa de câmbio … $80.56"` (token order differs between rows). PDF wraps the same text over two lines. `(movDate, amount)` is unique on all five keys (0 collisions in 197). | Pair on `(posting_date, amount, occurrence)`; use S-11 similarity only to break ties among equal (date, amount); state that the description never decides pairing. |
| N-2 | Medium | §4.3 | "`TimeoutSecondsStatement` (900, also for any document over 5 pages)" and "a timed-out statement retries once at 1.5 ×" need a kind hint before the first answer. An intake row has no `doc_type` until an answer validates; a first-attempt timeout yields none; the worker has no page counter (`DocumentContentInspector` reads a 64 KB head; no `pdfinfo` on the host). `LeaseSeconds ≥ largest timeout + 60` is also redundant: `RenewLeaseLoopAsync` renews every `LeaseSeconds/3` while the CLI runs, and 1.5 × 900 = 1,350 > 960 anyway. | `ClaudeDocumentProcessor.ProcessAsync`; `QueueWorker.RenewLeaseLoopAsync`; `DocumentContentInspector.cs` 30–82. | Name the hint's source (a PDF page count in the worker — name the method or library — plus the previous attempt's `doc_type` on retries), or use one 900 s timeout for every document; drop or restate the lease rule (renewal covers it). |
| N-3 | Medium | §4.5 | The permission set is `--allowedTools` + deny rules only. On the pinned CLI (2.1.259) that leaves Bash, WebFetch, WebSearch and MCP in the tool set, relying on print-mode prompt denial; the hostile corpus has no "fetch this URL" case (egress of package content). The same CLI offers `--tools Read` (removes the rest), `--restricted` ("confines the file tools to the working directories (--add-dir included)", removes code-running tools and WebFetch, ignores user/project/local settings), `--strict-mcp-config`, `--disable-slash-commands`, `--permission-prompts none`, `--settings <file>` for the deny rules. Windows absolute-path syntax for `Read(...)` rules is unverified. | `claude.exe --help` output; `ClaudeCli.cs` today passes `--allowedTools Read` only. | Name the full production flag set in §4.3/§4.5 (recommend `--tools Read --restricted --strict-mcp-config --disable-slash-commands --permission-prompts none --settings <per-job denies>`); add a web-egress document to the hostile run; make the Windows path-rule syntax an explicit R-EX-2 item. |
| N-4 | Medium | §4.5, §4.6 | The CLI persists every session (tool results = the document's full text) under `CLAUDE_CONFIG_DIR` = `D:\ApolloData\worker-claude\projects\…`; tenant documents accumulate there for every job, indefinitely. The spec denies reading that directory and says nothing about writing to it; today's worker already does this. | `provision-production.ps1` line 224 (`CLAUDE_CONFIG_DIR`); `--no-session-persistence` on the pinned CLI ("only works with --print"). | `--no-session-persistence` in production and bench; state it in §4.6 as a retention rule; ask the owner whether existing transcripts are purged (Q-EX-22). |
| N-5 | Medium | §2.6, §4.7, S-9 | "Both → the intercompany pair" is undefined for `docint.company_id` (one column) and for the scored `company` cell on #29 (R26030003: Gott issues to Itoorer; key `company = GOTT`, flow R). `AssignCompanyFromResultAsync` assigns one company or triage. | `QueueWorker.AssignCompanyFromResultAsync`; key `R26030003.json`. | State the rule (recommend: both match → triage with both candidates named and the pair recorded in the audit detail; the bench reports #29 as "pair" and does not score a company cell), and the test in §7. |
| N-6 | Medium | §4.7, V-19 | "Accepted, held for a person" has no representation: `IntakeProcessingStatus` is Received/Queued/Processing/Processed/Failed/DeadLetter/PossibleDuplicate/Duplicate/Quarantined; a V-19 answer is a Success → status 3 with `result_json`, indistinguishable from processed; `SubmitReviewAsync` accepts Processed/DeadLetter only. | `DocumentIntake.cs` 11–39; `IngestionService.cs` 302–304; `QueueWorker.CompleteAsync` Success branch. | Add a status (e.g. `HeldForPerson`) or a hold flag on the row, name it in §4.7 and in the `DocumentsReviewV2Tests` row of §7; say the company gate does or does not run on a held row. |
| N-7 | Medium | §4.7 (Q-EX-18) | "Re-extract under v2 … the previous result and any corrections kept untouched" contradicts the completion path, which overwrites `result_json`; a v0/v1 `CorrectedJson` would then sit beside a v2 result and the page "reads by contract id" from a mixed pair. | `QueueWorker.CompleteAsync`: `UPDATE docint SET processing_status = 3, result_json = $2`. | State the mechanics: `result_json` replaced (the previous answer survives as `attempt-N-result.json` on disk and in `evidence_json`), `CorrectedJson` kept with its own contract stamp and shown as superseded — or a result history. |
| N-8 | Medium | §5.4 vs §8 | §5.4 reads company codes and tax ids "from `commst` at bench time" (production `gott_sibyla`), while §8 forbids the bench reading `local\secrets` — the only place a connection string lives. The keys already carry `company.{code,name,taxId}` (six companies across the 40). | `INDEX.md` company mapping; §8 "Operational restrictions". | Build the candidate list from the keys; drop the DB read from the bench. |
| N-9 | Medium | §5.7 | The bench's CLI identity is unnamed: the production login (`D:\ApolloData\worker-claude`) is off-limits by §8, the pinned CLI's ACL is worker-only (`provision-production.ps1` line 177), and the "5-hour window" plan is that of whichever account runs it. | `worker-claude-login.ps1`; §8. | Name a bench `CLAUDE_CONFIG_DIR` and login (the owner's), record it in the score report; the configuration tuple stays the same. |
| N-10 | Medium | §5.3 S-7 | Statement opening/closing are scored against `bnkchk`, a per-period table. Revolut prints "Saldo de abertura 40.06" but the key's first `bnkchk` row is 202501 with opening 30.06 (no 202412 row — noted in `INDEX.md`); the printed opening has no key value and would miss. | Revolut PDF p.1; key `bnkchk[0]`; INDEX.md observation. | Define: closing ↔ last period's `bnkchk.closing`; opening ↔ first period's `bnkchk.opening` only when the statement's first period has a row, else derived as first key movement's `runningBalance − amount` (= 40.06) and recorded as a fourth pre-declared correction. |
| N-11 | Low | §4.6 | "published by `publish-release.ps1` from the build script's output" — the script only runs `dotnet publish` and the worker csproj declares no content, so `skill/` reaches no release without a csproj `Content` item or a script change; `D:\ApolloData\worker-skill` is not in `provision-production.ps1`. "The lane refuses to start" should be a host-stopping failure so the SCM and the module see it. Committing the built package (216 KB measured) would let the release gate's clean-export fingerprint bind it. | `publish-release.ps1`; `Sibyla.Worker.Documents.csproj`. | Name the mechanism (csproj content include, or commit the package) and make the hash mismatch throw at host start. |
| N-12 | Low | §5.2, S-4 | "7 of 35 say 'DateDue defaulted … assumption'" — five notes record a DateDue default (I26020038, I26030044, I26040053, I26050001, I26060014); the other "documented assumption" notes concern the service period. Ten of the fifteen +30 rows therefore rest on the PDF check. | Key `flagReviewNotes`, regex over `DateDue` + assum/default. | Correct the count; the rule stands. |
| N-13 | Low | §5.2 | `printOrder` ∈ {oldest_first, newest_first} cannot describe the BPI card statement, printed by section (PAGAMENTOS, MOVIMENTOS per card, COMISSÕES, JUROS): a 06/03 payment prints before a 24/02 purchase. | BPI-CC PDF p.2. | Add `sectioned`; the flag stays informational. |
| N-14 | Low | Q-EX-15, §4.6 | "The current default Claude Code model" cannot be passed as `--model` and can drift; `--fallback-model` exists on the pinned CLI and would silently break the tuple. | `--help`. | Pin the full model id read from the first bench's `modelUsage`; state that `--fallback-model` is never passed. |

#### Spot-check (flags derivable from FDR data + PDFs?)

| Flag | Document | Key / FDR note | PDF | Derivable |
|---|---|---|---|---|
| `dateDuePrinted` | MEO I26070038 | dateDue 2026-08-16 (+30), no DateDue note | prints "Data Limite de Pagamento 16 agosto 2026" | yes → `true`, exact match |
| `dateDuePrinted` | BICS I26080006 | 2026-09-14 (+30), no note | "Payment should be made before 14 SEP 2026" | yes → `true` |
| `dateDuePrinted` | Tranquilidade I26030044 | note "DateDue defaulted to DateDoc+30 (no due date printed)" | no due date found | yes → `false` |
| `dateDuePrinted` | Lari I26060014 | note "DateDue = DateDoc+30 default" | none printed | yes → `false` |
| `printsOpening/Closing/RunningBalance` | Revolut #40 | bnkchk 202501…202512 (no 202412) | "Saldo de abertura 40.06", "Saldo de encerramento 164.94", per-row "Saldo" column | `true/true/true`; opening has no key value (N-10) |
| `printsOpening/Closing/RunningBalance` | BPI-CC #37 | bnkchk 202603 270.03 → 619.22 | "Saldo em dívida … anterior 270,03 / actual 619,22"; no balance column | `true/true/false`; V-14 holds with native signs (270.03 + 619.16 − 270.03 + 0.06 = 619.22) |
| `printOrder` | Revolut #40 | key oldest-first | first row 15 dez. 2025, last 10 dez. 2024 | `newest_first` |
| `printOrder` | BPI-DO #39 | — | 01/07 first, "SALDO ACTUAL 31/07" last | `oldest_first` |
| `printOrder` | BPI-CC #37 | — | by section (payment 06/03 before purchase 24/02) | vocabulary lacks it (N-13) |
| `hasTransactionDateColumn` | BPI-CC #37 | 5 of 10 rows docDate ≠ movDate | "DATA DA TRANSACÇÃO / DATA DO MOVIMENTO" | `true` |
| Natural-key pairing | all five statement keys | `(movDate, amount)` unique on all 197; `(movDate, amount, description)` unique | Revolut descriptions column-sliced with FX sub-lines | computable on (date, amount); not on the exact description (N-1) |

No hand-keying is needed beyond the three pre-declared corrections **except** the Revolut opening balance (N-10).

#### Verified myself
- Spec and review record at `e83445d`; worktree clean.
- Worker code on main: `WorkerOptions` (240 s, lease 300, slots 2, pause 10), `ClaudeCli.RunAsync` flags (`-p`, `--output-format json`, `--add-dir <jobDir>`, `--allowedTools Read`), `ClaudeDocumentProcessor` evidence and sandbox, `QueueWorker` (claim, renewal loop, lane pause, `AssignCompanyFromResultAsync` on top-level `recipient_tax_id`, `CompleteAsync` overwriting `result_json`, backoff 30/120/600/1800/3600), `Program.cs` (secrets file, dp-keys), `ExtractionContract` v1 (11 fields), `ProcessingEvidence` fields, `QueueJob.MaxAttempts = 5`, `IntakeProcessingStatus` values, `Documents.razor` flat v1 form, `IngestionService.SubmitReviewAsync` literal `apollo.extract.v0`, `ReleaseHeldProcessing`, `DocumentCaptureService` C6 refusal, `DocumentTypeRouter`, `Bank.cs` (`RunningBalance` decimal non-null, `Direction` required), `CompanyMatcher.Match` one id.
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: version 2.1.259; `--help` only. Flags present: `--model`, `--effort`, `--output-format json|stream-json`, `--add-dir`, `--allowedTools`, `--disallowedTools`, `--tools`, `--restricted`, `--settings`, `--setting-sources`, `--strict-mcp-config`, `--permission-prompts`, `--no-session-persistence`, `--fallback-model`, `--json-schema`, `--bare` (API-key auth only, unusable under the subscription), `--safe-mode`.
- Skill build: HEAD `a558523f…`, subject and date as §4.1; `skill_currency.json` Stage 16 Round 30, `PackageMatchesSource = true`, 20/20 rules; zip 26 files, 1,433,312 bytes; no `CLAUDE.md`/`.claude`; range boundaries (SKILL.md 16 = §1, 410 = §8, 581 = §11, 595 = §12); long-line counts per F-1; schema.md lines 17/78–87/89/91/93/95/97/99 contents; the seven rule `.md` files exist and differ from the `.txt`; cut size 216,127 bytes.
- Production layout: run record §7o (module, bare-id pointers, `-Execute -Resume`), `provision-production.ps1` (worker ACLs, `SIBYLA_SECRETS_FILE`, `CLAUDE_CONFIG_DIR`), `publish-release.ps1` (dotnet publish only), `worker-claude-login.ps1`, plan §6 risks 5 and 6, plan lines 822–824 ruling text.
- Answer keys: 40 files, INDEX.md; `(movDate, amount)` uniqueness; `fiscalNo == entity.fiscalNo` on all 35; placeholders; receipts' dates; +30 rows (15) and DateDue notes (5); Revolut description contents; `bnkchk` rows.
- PDFs via `pdftotext`: Revolut, BPI-CC, BPI-DO 202507, MEO, BICS, Tranquilidade #6, Google Cloud, Lari, Via Verde, Regus.

#### Taken from the spec without verification
- That the CLI's `Read` truncates lines over 2,000 characters and that the `json` envelope carries `num_turns`, `usage`, `modelUsage` (documented behaviour; no extraction run).
- That print mode auto-denies tools outside `--allowedTools` when no permission host is attached, and the Windows syntax of path-scoped `Read(...)` allow/deny rules.
- The existence in `gott_sibyla` of the eight reserve entry codes (only the two reserve PDFs were checked).
- Page counts of the sample PDFs (no `pdfinfo` on the host) and the §4.2 token estimate.
- `SKILL.md` §5/§6/§7 wording cited in §2 (I checked §5's location, not the wording of the flag list or the stamp rule).
- The sync-era `ToCashDelta("CC")` defect and `build_bnkmov.py` lines 77–84 (round-1 evidence, not re-read).

#### Owner questions
Q-EX-0..21 are defensible as written, with three notes: Q-EX-21's recommendation should offer `--restricted`/`--tools Read` as the ruling's form (N-3); Q-EX-15 should also name the account the bench runs under (N-9); Q-EX-18's recommendation is not implementable as stated until N-7 is answered.

I add:
- **Q-EX-22 — CLI session transcripts.** The worker's CLI writes each document's text into `D:\ApolloData\worker-claude\projects\…` and keeps it. Rule: `--no-session-persistence` from v2 on, and whether existing transcripts are purged now. Recommendation: both.
- **Q-EX-23 — Intercompany documents at the gate.** When issuer and recipient both match licence companies (R26030003), which book does the intake row land in, or does it triage? Recommendation: triage with both candidates named; the bench reports the pair and scores no company cell for such rows.

#### Ready for RED oracles?
No. N-1 must be fixed before the scoring fixtures of §7 ("natural-key pairing on a shuffled Revolut key") are written, or the oracle encodes the defect; N-2, N-5, N-6, N-7 and N-8 each change a §7 test's expected behaviour; N-3/N-4 change the invocation the hostile run must be recorded under. The remaining Lows can be folded into the same revision.

### Author's dispositions — revision 3

Revision 3 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 2. Ids: `C-N-n` = the contract reviewer's new findings, `F-N-n` = the feasibility reviewer's. Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 3. Decisions the coordinator took for this revision are written in as fixed and not re-opened.

**Contract reviewer (C-N-1 … C-N-14)**

| id | Sev | Disposition | Where in revision 3 |
|---|---|---|---|
| C-N-1 | High | fixed — S-2 pairs movements on `(posting_date ↔ movDate, amount, currency)`, unique across all 197 key movements; ties among equal `(date, amount, currency)` broken by S-11 similarity, then occurrence; the description is never a pairing precondition; the §7 Revolut fixture must contain a key row carrying the FX sub-line; the 18 defective key descriptions (17 Revolut + 1 BPI) pre-declared as corrections scored against what is printed, 18 of ≈ 1,380 movement cells | §5.3 S-2, S-11; §5.2 statistics; §5.5; §7 scoring row |
| C-N-2 | Medium | fixed — `printedOpeningBalance` / `printedClosingBalance` hand-keyed into the flags file with page refs; S-7 scores against those, never `bnkchk`; V-14 kept; worked cases Revolut 40.06 → 164.94 (no 202412 `bnkchk` row) and card 270.03 → 619.22 | §5.2 flags; §5.3 S-7; §7 |
| C-N-3 | Medium | fixed — on an invoice a `stamp_tax` line keeps the printed sign of the duty (#6 −0.03); on a credit note it is negative (#31); the server appends a created line as `line_no = n + 1`; S-2 pairs it as `n + 1` on both sides; the §7 test is typed to #6 and #31 | §2.3 `kind`; §2.6 Split and Signs; §5.3 S-2, S-3; §7 |
| C-N-4 | Medium | fixed — the split is the function `Split(net, vat, total, duty, lines)` on contract fields: branch (1) `net + vat + duty = total` → duty outside VAT; (2) `net + vat = total` and `vat ≥ duty` → folded; (3) otherwise no split and a reconciliation finding; plus the no-net branch (0): `net_amount` null and `vat_amount` null/0 → `Net = Total − VAT` first, a single printed line equal to `Total − duty` is the item line (#25 → 392.39 + 19.15) | §2.6 Split; §5.3 S-3; §7 (branch (0) on #25) |
| C-N-5 | Medium | owner question — Q-EX-23: the issuer's company owns a both-match row as R/Internal (the FDR's #29, SKILL.md §11), both candidates in the audit detail; alternative: triage with both named. `QueueWorkerGateTests` gains #29 with the recommendation's expected value and the alternative's noted | §1.2 Q-EX-23; §2.6 gate; §4.7; §5.3 S-9; §5.4; §7 |
| C-N-6 | Medium | fixed — `WorkerOptions.CliVersion` (expected) verified against `claude --version` at start; mismatch (or a package-hash mismatch) is a host-stopping failure; the version and the full model id are named in the R-EX-3 report; the gate tuple is enforced at start | §4.3; §4.6 Gate policy; §7 `WorkerStartupTests`; §8 R-EX-3 |
| C-N-7 | Low | fixed — per-document flag `netVatNotPrinted` in the flags file (set for #6 and #25) replaces the note-phrase match | §5.2 flags; §5.3 S-9 |
| C-N-8 | Low | fixed — the statement cells (`movement_count`, opening, closing) belong to the movements score | §5.3 S-1, S-2 |
| C-N-9 | Low | fixed — Q-EX-7 states that the movements gate goes beyond the ruling's "header and lines" and asks the owner to confirm it | §1.2 Q-EX-7 |
| C-N-10 | Low | fixed — 11 of 35 keys | §1.2 Q-EX-13 |
| C-N-11 | Low | fixed — "assumed (`+30`, or `= DateDoc` on a credit note)"; the count is 5 + 2 with the entry codes | §5.2 flags; §5.3 S-4 |
| C-N-12 | Low | fixed — `reserve-set.csv` names the doclog codes: R1 = LG000015 (not LG000914), R4 = LG000795 (not LG001770) | §5.6 |
| C-N-13 | Low | fixed — "1,433,312 bytes uncompressed; the file is 559,999 bytes" | §4.1 |
| C-N-14 | Low | fixed — the BPI "-2 5" fragment is one of the 18 pre-declared description corrections | §5.5; §5.3 S-11 |

Owner-question notes from the contract reviewer: Q-EX-13's recommendation switched to the alternative (`CapturedAt` as its own column, `DocDate` = document date for Apollo-native rows, synced rows' meaning recorded on the entity until cutover); Q-EX-7 confirms the movements gate explicitly; the intercompany rule is Q-EX-23 (the number 22 went to the feasibility reviewer's transcript question, both reviewers having proposed a Q-EX-22).

**Feasibility reviewer (F-N-1 … F-N-14)**

| id | Sev | Disposition | Where in revision 3 |
|---|---|---|---|
| F-N-1 | High | fixed — as C-N-1 (pairing on `(date, amount, currency)`, S-11 tie-break, occurrence; the description never decides pairing; the 18 corrections) | §5.3 S-2, S-11; §5.5; §7 |
| F-N-2 | Medium | fixed — one 900 s timeout for every document, no kind hint (no `doc_type` before an answer, no page counter); a retry at 1.5 × (1,350 s) only on a second timeout of the same job, a third lands the row in `HeldForPerson`; the `LeaseSeconds` rule dropped — `RenewLeaseLoopAsync` renews every `LeaseSeconds/3` and covers it; `ExtractionRunner` takes no kind hint | §4.3; §4.7; §7 `ClaudeCliTimeoutTests` |
| F-N-3 | Medium | fixed — the full production flag set named: `--tools Read --restricted --strict-mcp-config --disable-slash-commands --permission-prompts none --settings <per-job deny file> --no-session-persistence` plus the two `--add-dir`; a web-egress document added to the hostile run; the Windows path-rule syntax for `Read(...)` allow/deny is an explicit R-EX-2 verification item; Q-EX-21's recommendation is this set | §4.3; §4.5; §1.2 Q-EX-21; §8 R-EX-2; §7 `ClaudeCliPermissionTests` |
| F-N-4 | Medium | owner question — Q-EX-22: `--no-session-persistence` in production and bench from v2 on, stated in §4.6 as a retention rule; recommendation adds the purge of the existing transcripts under `D:\ApolloData\worker-claude\projects` as an owner action (never read by this work) | §1.2 Q-EX-22; §4.6 Retention rule; §4.3 flag set |
| F-N-5 | Medium | owner question — as C-N-5 (Q-EX-23; #29 in the gate tests) | §1.2 Q-EX-23; §2.6; §4.7; §7 |
| F-N-6 | Medium | fixed — `IntakeProcessingStatus.HeldForPerson` added: a V-19 answer or a timeout-exhausted job lands there with `result_json` kept; `SubmitReviewAsync` accepts it; the company gate runs on a held row; named in §4.7 and in the `DocumentsReviewV2Tests` and `QueueWorkerGateTests` rows | §4.7; §2.6 gate; §7 |
| F-N-7 | Medium | fixed — re-extract mechanics: `result_json` is replaced by `CompleteAsync`; the previous answer survives as `attempt-N-result.json` on disk and in `evidence_json`; `CorrectedJson` is kept with its own contract stamp and shown as superseded until a new correction | §4.7 Re-processing; §1.2 Q-EX-18; §7 review-page row |
| F-N-8 | Medium | fixed — the bench builds the company candidate list from the keys' `company.{code,name,taxId}` and never opens a database connection or reads a connection string | §5.4; §5.7; §8 restrictions |
| F-N-9 | Medium | fixed + owner question — Q-EX-15 names the bench account: a bench `CLAUDE_CONFIG_DIR` and login owned by the owner, recorded in the score report; the configuration tuple is unchanged by the identity | §1.2 Q-EX-15; §5.7 |
| F-N-10 | Medium | fixed — as C-N-2: printed opening/closing hand-keyed into the flags file, scored against those, never `bnkchk`; the Revolut 40.06 is a flags value, not a correction (the coordinator's decision over the reviewer's "fourth correction" suggestion, same effect on the score) | §5.2; §5.3 S-7 |
| F-N-11 | Low | fixed — the built package is committed to the repository (`src/Sibyla.Worker.Documents/skill/`, the clean-export fingerprint binds it) and reaches the release as csproj `Content` (`CopyToPublishDirectory`), `publish-release.ps1` unchanged; the hash/version mismatch is a host-stopping failure the SCM and the module see | §4.6; §4.3; §7 `SkillPackageTests`, `WorkerStartupTests` |
| F-N-12 | Low | fixed — as C-N-11 (5 + 2 with entry codes; the remaining ten +30 rows rest on the PDF check, MEO and BICS named) | §5.2 flags; §5.3 S-4 |
| F-N-13 | Low | fixed — `printOrder` gains `sectioned` (the card statement's PAGAMENTOS / MOVIMENTOS / COMISSÕES / JUROS sections); informational | §5.2 flags |
| F-N-14 | Low | fixed — `WorkerOptions.Model` is the full model id read from the first bench's `modelUsage`; `--fallback-model` is never passed; asserted by `ClaudeCliPermissionTests` on the argument list | §1.2 Q-EX-15; §4.3; §7 |

Owner questions after revision 3: Q-EX-0…21 as before (Q-EX-7, Q-EX-13, Q-EX-15, Q-EX-18, Q-EX-21 reworded as above), plus **Q-EX-22** (CLI session transcripts: `--no-session-persistence` from v2 on and the purge of existing transcripts) and **Q-EX-23** (intercompany ownership at the gate: the issuer's company owns the row as R/Internal; alternative triage with both named).

## Round 3 — revision 3, head `ae9883f` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-2 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 2 / 3 / 5 | 27 of 28 (C-N-4 partially) |
| feasibility | REVISE | 0 / 2 / 8 / 6 | 26 of 28 (C-N-4, F-N-7 partially) |

Shared High: the stamp-duty split cannot reproduce #25 (the key folds a 9.57 parafiscal surcharge into the item line per SKILL.md §6, which the contract has no field for) and the contract reviewer traces #6 into the "no split" branch. Feasibility's second High: a re-extract's evidence files overwrite the original job's (`attempt-N` names restart per job, copied with overwrite). Finding ids below are per reviewer (contract P-1..P-10 and feasibility P-1..P-16 are different lists; referenced as C-P-n and F-P-n).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** ae9883f (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean)
**Verdict:** REVISE
**Counts:** Critical 0 / High 2 / Medium 3 / Low 5

All 28 round-2 findings are addressed in the revision-3 text and 27 hold up against the keys, PDFs and code; C-N-4 is only partially closed. The two Highs are both in the stamp-duty `Split` of §2.6, which I traced with the printed figures of #6, #25 and #31: the function as written lands #6 in its "no split, reconciliation finding" branch, and its worked case for #25 rests on a printed line that does not exist — the key's 392.39 is premium 382.82 plus a parafiscal surcharge 9.57 that SKILL.md §6 folds into the item line and the contract carries only as text.

#### Round-2 revisit

| id | State | Revision-3 text relied on and what I verified |
|---|---|---|
| C-N-1 | CLOSED | S-2: pairs on `(posting_date ↔ movDate, amount, currency)`, "ties … broken by S-11 similarity, then by occurrence order; the description is never a pairing precondition". Verified with python over the five keys: 0 collisions on `(movDate, amount, currency)` and on `(docDate, amount, currency)` — 46/10/3/87/51 = 197. §7 scoring row requires the FX-sub-line key row in the fixture. |
| C-N-2 | CLOSED | §5.2 flags `printedOpeningBalance`/`printedClosingBalance` "hand-keyed from the PDF with page references"; S-7 "never against `bnkchk`". Verified printed: Revolut p.1 "Saldo de abertura 40.06 / Saldo de encerramento 164.94" (bnkchk starts 202501 at 30.06); card "Saldo em dívida … anterior 270,03 / actual 619,22"; BPI-DO #39 SALDO ANTERIOR 15 324,78 / SALDO ACTUAL 31/07 4 525,08; BCP #36 SALDO INICIAL 5 985.92 / final 6 813.72; BPI-USD #38 415,88 / 1 012,18 (the last three shifted rows in `pdftotext -layout`, chain-checked against the first movement). V-14 holds on all five: 40.06+124.88, 270.03+349.19, 15324.78−10799.70, 5985.92+827.80, 415.88+596.30. |
| C-N-3 | CLOSED | §2.3 `kind`: "on an invoice the `stamp_tax` line keeps the printed sign of the duty (#6: −0.03); on a credit note it is negative (#31)"; "appended as `line_no = n + 1`"; S-2 "n + 1 on both sides". Verified: PDF #6 p.2 "(*) O valor indicado inclui um ajuste no Imposto de Selo no valor de − 0,03"; key #6 line 3/3 = −0.03, #25 line 2/2 = 19.15, #31 line 2/2 = −17.75 — the stamp line is last on all three. The sign rule is closed; the split path #6 takes is new (P-2). |
| C-N-4 | PARTIALLY | §2.6 Split is now a function with branches (0)–(3) and S-3 applies it. But branch (0)'s worked case — "when the document prints a single line whose amount equals Total − duty, that line is the item line (#25 … → item 392.39)" — is false on the PDF: #25 prints 382,82 / 0,00 / 0,00 / 382,82 / 19,15 / 9,57 / 28,72 / 411,54; no 392.39 anywhere (P-1). And #6 traced through the branches lands in (3) (P-2). |
| C-N-5 | CLOSED | Q-EX-23 (issuer owns as R/Internal; alternative triage), §2.6 both-match rule, §4.7, S-9, §5.4, §7 `QueueWorkerGateTests` "#29 (both match) → Gott as R/Internal". Verified PDF R26030003: issuer "VAT Number: 513362061", customer "VAT Number IE3559150RH"; keys: GOTT = PT513362061, ITOO = IE3559150RH (I26070005 company), #29 stored Company GOTT / R / Internal. `CompanyMatcher.SameTaxId` tolerates the missing PT prefix. |
| C-N-6 | CLOSED | §4.3: `CliVersion` (expected) verified against `claude --version` at start; mismatch "host-stopping failure"; §4.6 gate policy "enforced at start for the CLI version and the package"; §7 `WorkerStartupTests`. Code today: `WorkerOptions.ClaudeCliPath = "claude"`, no version option (verified). |
| C-N-7 | CLOSED | §5.2 `netVatNotPrinted` "(set for #6 I26030044 and #25 I26080026)"; S-9 "scored only where … `netVatNotPrinted` is false". |
| C-N-8 | CLOSED | S-1: movements score = "movement cells plus the statement cells `movement_count`, `opening_balance`, `closing_balance`"; S-2 last sentence; S-9 consistent. |
| C-N-9 | CLOSED | Q-EX-7: "a movements gate is this spec's addition … The owner confirms the movements gate, which goes beyond the ruling's words; without it, movements are reported only." Plan line 822 verified: "field-level exact match on header and li[nes]". |
| C-N-10 | CLOSED | Q-EX-13 "11 of 35 keys". Verified 11 with python over the 35 keys. |
| C-N-11 | CLOSED | §5.2 "5 + 2 — five '+30' (I26020038, I26030044, I26040053, I26050001, I26060014) and two credit notes 'set equal to DateDoc' (I26030019, I26030031)"; S-4 "(`+30`, or `= DateDoc` on a credit note)". Verified by regex over `flagReviewNotes`: exactly those 7; 15 rows are +30 in total. |
| C-N-12 | CLOSED | §5.6 "R1 = `LG000015` (not LG000914, the Duplicate copy) and R4 = `LG000795` (not LG001770)". Verified in `05-doclog-all.txt` lines 18/767 and 684/1565. |
| C-N-13 | CLOSED | §4.1 "1,433,312 bytes uncompressed; the file is 559,999 bytes". Verified `ls -l` and `unzip -l` (26 files). |
| C-N-14 | CLOSED | §5.5 names the BPI row among the 18; S-11 scores against the corrected value. The quoted fragment is inexact (P-6). |
| F-N-1 | CLOSED | As C-N-1. |
| F-N-2 | CLOSED | §4.3: "`ClaudeTimeoutSeconds` becomes 900 for every document — there is no kind hint"; second timeout of the same job → one retry at 1,350 s → `HeldForPerson`; lease rule dropped ("`RenewLeaseLoopAsync` renews every `LeaseSeconds/3`"); §7 `ClaudeCliTimeoutTests`. |
| F-N-3 | CLOSED | §4.3/§4.5 flag set `--tools Read --restricted --strict-mcp-config --disable-slash-commands --permission-prompts none --settings … --no-session-persistence`; Q-EX-21 = that set; web-egress document in the hostile run; Windows path-rule syntax an R-EX-2 item. Code today passes `--allowedTools` only (verified `ClaudeCli.cs` 72–78). |
| F-N-4 | CLOSED | Q-EX-22; §4.6 "Retention rule (Q-EX-22)". |
| F-N-5 | CLOSED | As C-N-5. |
| F-N-6 | CLOSED | §4.7 `IntakeProcessingStatus.HeldForPerson` "(new value)", `SubmitReviewAsync` accepts it, "The gate runs on `HeldForPerson` rows too"; §7 review-page and gate rows. Enum today ends at `Quarantined = 8` (verified). |
| F-N-7 | CLOSED | §4.7 Re-processing: "`CompleteAsync` replaces `result_json` as it does today; the previous answer survives as `attempt-N-result.json` … `CorrectedJson` is kept … shown as superseded". Verified `QueueWorker.cs` 391 `UPDATE docint SET processing_status = 3, result_json = $2::jsonb`. |
| F-N-8 | CLOSED | §5.4 "candidate list built from the keys' `company.{code,name,taxId}`"; §5.7 "needs no database"; §8. |
| F-N-9 | CLOSED | Q-EX-15 "bench `CLAUDE_CONFIG_DIR` and login owned by the owner"; §5.7. |
| F-N-10 | CLOSED | As C-N-2 (40.06 a flags value, not a correction — same score effect). |
| F-N-11 | CLOSED | §4.6: package committed under `src/Sibyla.Worker.Documents/skill/`, csproj `Content` with `CopyToPublishDirectory`; §4.3 host-stopping. Verified the worker csproj has no `Content` items today. |
| F-N-12 | CLOSED | As C-N-11. |
| F-N-13 | CLOSED | §5.2 `printOrder` "(`oldest_first` / `newest_first` / `sectioned`)". Verified the card prints PAGAMENTOS (06/03) before MOVIMENTOS (24/02). |
| F-N-14 | CLOSED | Q-EX-15 full model id from `modelUsage`; §4.3 "`--fallback-model` is never passed"; §7 `ClaudeCliPermissionTests`. |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| P-1 | High | §2.6 Split branch (0); §2.2 `other_taxes_printed`; §5.1 #25, #31; §7 "#25 exercising branch (0)", "the negative stamp line on #31" | The parafiscal surcharge (INEM/FAT, "Outros encargos e taxas") has no numeric field and no term in `Split`, yet the keys need it. SKILL.md §6 rule: "A bundled non-tax parafiscal surcharge (INEM/FAT/ANPC …) folds back into the original line's NetAmount". #25's key line 1 = 392.39 = 382.82 premium + 9.57 surcharge — not printed; branch (0)'s "a single printed line whose amount equals Total − duty" never fires. #31's key stamp line −17.75 = 12.38 stamp + 5.37 surcharge (its flag: "Imposto de Selo + parafiscal surcharge folded in") — contradicts SKILL.md §6 and the contract's `stamp_duty_amount` = printed "Imposto de selo" 12.38. Under "as printed" both tests have no expected value a builder can derive, and #25 line 1 / #31 line 2 miss by design. | SKILL.md a558523 line 160; PDF `Gott_InvoiceReceipt_Tranquilidade_202608_01.pdf` totals block (382,82 / 19,15 / 9,57 / 28,72 / 411,54); PDF `Gott_CreditNote_Tranquilidade_202504_01.pdf` (137,65 / 12,38 / 5,37 / 17,75 / 155,40); keys `I26080026.json` lines 392.39 + 19.15, `I25040004.json` lines −137.65 + −17.75 (flag text). | Add `other_taxes_amount` (d?) beside `other_taxes_printed`; in `Split`, fold `other_taxes_amount` into the item line (single item line → `Net = Total − duty`; several → the largest, flagged) per SKILL.md §6; state that on #25 the expected line 1 is `Total − duty`. Pre-declare #31 line 2 as a key correction (stamp 12.38, surcharge folded into line 1 → −143.02 / −12.38) or exclude #31's line cells by flag, since the key contradicts the pinned skill. |
| P-2 | High | §2.6 Split branches (0)–(3); §7 "the split function's four branches", "printed-sign rule on #6" | Traced #6 with its printed figures: `net = null, vat = null, total = 767.02, duty = −0.03`. Branch (0) → Net 767.02. Branch (1): 767.02 + 0 − 0.03 = 766.99, off by 0.03 > 0.02 → no. Branch (2): 767.02 = total but `vat ≥ |duty|` is 0 ≥ 0.03 → no. Branch (3): "no split, `reconciliation_note` required (V-12), DPRCHK finding" — on a document that reconciles exactly (634.72 + 132.33 − 0.03 = 767.02). Whether the stamp line is still appended under "no split" is ambiguous ("In every branch … appends one" vs "no split"); if not, `line_count` and line 3 miss. The flagship case of the function falls through it. | PDF #6 p.1–2 (OPÇÃO A0 634,72; A1 132,33; "Total(*) 767,02"; footnote −0,03); key `I26030044.json` net 767.02 / lines 634.72, 132.33, −0.03. | Make branch (0) terminal: when `net_amount` is null the printed Total already contains the duty — `Net := Total − (VAT ?? 0)`, append the stamp line (`Net = duty`), done; branches (1)–(3) apply only when a net is printed. State explicitly that the line is appended in every branch including (3). Note that no sample document exercises (1)/(2) (only #6/#25/#31 carry stamp lines) — they are synthetic-fixture branches. |
| P-3 | Medium | §0 / Q-EX-0 vs §2 | §0 says "§2 maps every one of those columns [FDCHDR/FDCDTL/BNKMOV/DOCLOG] to extracted, derived later or system, so 'unchanged' is a checkable claim". §2.2–2.4 do map the 32/18/26 columns (I checked each against schema.md lines 78/87/95: all accounted). DOCLOG's 24 columns (schema.md line 17: `LGCode, Date, Filename, SourceFilename, EntryCode, Source, Company, Entity, ItemCode, ItemDesc, DocumentType, OriginClass, CaptureQuality, Flag, FlagCategory, RiskFactor, ReviewedBy, ReviewDate, ArchiveStatus, ArchivePath, ArchiveDate, FileHash, EnteredBy, EnteredAt`) are classified nowhere; only `Date` (Q-EX-13), `DocumentType`, `OriginClass` are touched. `captureQuality` is `null` in all 40 keys (INDEX.md). | Spec lines 21, 37; grep for DOCLOG/CaptureQuality in the spec; `schema.md` line 17; INDEX.md. | Add a §2.5-adjacent DOCLOG table: extracted (DocumentType, OriginClass via the gate, Date via Q-EX-13), derived (Company, Entity, ItemCode/ItemDesc, Flag/FlagCategory/RiskFactor, CaptureQuality — not computed by Sibyla), system (the rest). |
| P-4 | Medium | §2.3 line doctrine (a)–(e); §5.1 #2; §7 golden #2 | The Continente key has 2 lines — the receipt's VAT-summary groups ("Groceries 13% VAT" 3.62/0.47, "Groceries 23% VAT" 56.93/13.09) — while the receipt prints 10 product lines, gross, plus a card discount 2,10. No doctrine clause covers a POS receipt; "as printed" yields 10 lines and misses every #2 line cell and `line_count`. | PDF `Gott_InvoiceReceipt_Continente_202601_01.pdf`: 10 `(B)/(C)` product lines, "SUBTOTAL 76,21 / Desconto Cartao 2,10 / TOTAL 74,11", VAT block "3,62 0,47 4,09 (B) 13,00% / 56,93 13,09 70,02 (C) 23,00%"; key `I26010026.json`. | Doctrine clause (f): a POS receipt with a VAT summary is itemised at its VAT-rate groups (net, VAT, total from the summary block), product rows and discounts summarised in a note; the golden #2 typed to that. |
| P-5 | Medium | S-9 `document_type`; §5.1 #6, #12 | The expected `doc_type` on #6 and #12 is not derivable from the documents: #6 is a "Detalhe dos Movimentos do Recibo" whose own glossary says only Fatura/Recibo or Nota de Crédito count fiscally (key: Invoice); #12 is an Asaas portal snapshot "Cobrança Vencida" with no issue date (key: Invoice). An honest `other` on #6 forces `lines = []` (V-3) and zeroes 3 lines + header cells. | PDF #6 p.1 title; key note "not itself a Fatura/Recibo"; key #12 note "not a formal invoice/NF-e"; V-3. | State in EXTRACT.md that an itemised receipt detail / a payment-portal charge is `invoice`, or flag `documentTypeExpected` for #6/#12 in the flags file and report the cell. |
| P-6 | Low | §5.5 counts and quotes | "17 Revolut rows" — the key holds 15 descriptions carrying `$…`/"Taxa de câmbio" (listed by python; the other 36 are clean). The BPI row reads "… FAT-20250422904233 [spaces] -2 500,00 USD", not "-2 5". So 16 pre-declared description corrections, ≈ 1.2 %. | `REV-DO-GOT-EUR_202512.json` (15 rows: 31 Jan, 14 Mar, 7 Apr, 8 May, 29 May, 13 Jun, 17 Jul, 7 Aug, 19 Aug, 12 Sep, 8 Oct, 14 Oct, 3 Nov, 12 Nov, 5 Dec); `BPI-DO-GOT-EUR_202507.json` 2025-07-30 −2186.06 (currency EUR). | Correct to 15 + 1 and the quote; the flags file lists them by `(movDate, amount)`. |
| P-7 | Low | §2.4 `amount`, `description_printed` | Column-formatted statements print no sign: Revolut "Saída de dinheiro / Entrada de dinheiro", BCP "DEBITO / CREDITO" (key amounts negative for debits). "spending is negative" implies it; say it. Also whether the Revolut FX sub-line ("Taxa de câmbio 1 EUR = …", "$127.51") belongs to `description_printed` is unstated — S-11's prefix rule absorbs either reading, but EXTRACT.md should choose. | Revolut p.1 column headings; BCP header line "DEBITO CREDITO SALDO"; PDF rows 195–198 and 223–226. | "An amount printed in the debit/outflow column is negative; in the credit/inflow column positive." Sub-lines under a Revolut transaction are not the description. |
| P-8 | Low | S-2 tie-break | "then by occurrence order" is undefined between a newest-first answer and a `(doc_date, bm_code)` key. No ties on the 40 (verified), so only the §7 fixture is affected. | Key order per INDEX.md; §5.1 #40. | "occurrence order = print order of the answer against `bm_code` order of the key". |
| P-9 | Low | S-9 `fiscal_no`, N-3 | Every key `fiscalNo` carries an FDR country prefix (PT500940231, BR19628811000160, GB12465777) while the documents print "NIPC: 500 940 231", "19.628.811/0001-60"; N-3 prefixes "when the country is evident" — a judgment the exact compare then punishes. | Keys; PDF #6 "Matriculada na CRC/NIPC: 500 940 231"; `CompanyMatcher.SameTaxId`. | Compare `fiscal_no` under `CompanyMatcher.SameTaxId` semantics (prefix tolerated on either side), or make the prefix mandatory in N-3. |
| P-10 | Low | §2.3 `unit_price_basis` | "Continente's '2 X 11,99'" — the receipt prints "4 X 11,99" (BUONDI NESPRESSO). | PDF #2. | Correct. |

#### Owner questions Q-EX-0..23

All 24 recommendations are defensible as written. Q-EX-23's recommendation is verified against the document and the key: #29 prints both ids, the FDR stored it GOTT / R / Internal, and `CompanyMatcher` matches the unprefixed 513362061. Q-EX-13 (`CapturedAt`), Q-EX-7 (movements gate named as the spec's addition), Q-EX-15, Q-EX-18, Q-EX-21, Q-EX-22 read as the round-2 reviewers asked. Two changes I would make: **Q-EX-4** should say that the server's deterministic transform includes the parafiscal fold of SKILL.md §6 (P-1) — otherwise "the identical transform the FDR runs" is not what the keys hold; **Q-EX-20** needs the POS-receipt clause (P-4). Nothing to add as a new question: P-1's key defect on #31 is a correction, not a ruling.

#### Readiness for RED oracles

Not yet, on two tests. Cannot be typed with an expected value from the spec: `ExtractionScoringTests` "the split function's four branches, #25 exercising branch (0)" (P-1 — 392.39 is not printed and no contract field yields it) and "the negative stamp line on #31" (P-1 — printed duty 12.38, key −17.75); "the printed-sign rule on #6" is typeable only if the builder reads branch (3) as still appending the line (P-2). Typeable now: the accept/reject corpus, `SkillPackageTests`, `ProcessingEvidenceTests`/`WorkerStartupTests`, `ClaudeCliTimeoutTests`, `ClaudeCliPermissionTests`, `EvidenceSecretScanTests`, `CompanyMatcherTests`, `AccountPeriodTests`, `DocumentsReviewV2Tests`, `QueueWorkerGateTests` including #29, and the scoring fixtures for pairing (verified unique key), statement cells (printed 40.06 → 164.94, 270.03 → 619.22), S-4, S-6, S-11, the floor rule and the description corrections. P-1/P-2 are a few sentences each; P-3–P-5 should ride the same revision.

#### Verified myself

Worktree head `ae9883f`, clean; the spec and review record in full. Skill build at `a558523` (HEAD, clean): schema.md lines 17, 78–87, 93, 95 and the three column lists against §2.2–2.4 column by column; SKILL.md §6 lines 156–162 (stamp and parafiscal rules); zip 559,999 / 1,433,312 / 26. Plan line 822. Code on main `2c8e5f2`: `ExtractionContract.cs` (v1 id, 11 fields, `summary` 500), `CompanyMatcher` (one id, prefix-tolerant), `IntakeProcessingStatus` (0–8), `QueueWorker.AssignCompanyFromResultAsync` (top-level `recipient_tax_id`) and `CompleteAsync` line 391, `WorkerOptions` (240/300/2/10, no version or model), `ClaudeCli.cs` flags, `IngestionService.cs` `apollo.extract.v0` literal and Processed/DeadLetter check, `Bank.cs` (`RunningBalance` non-null, `Direction` required), `DocumentTypeRouter` map, `SyncEngineWave3.ToCashDelta("CC")`, worker csproj (no Content). Keys (40): pairing-key uniqueness; 35/5, 109 lines, 197 movements; rules R1 12 / R3 12 / blank 10 / R2 1; qty/unit/vat 6/6/8; 11 doclog-date differences; 6 receipts with dateDue = dateDoc = datePay; 15 +30 rows and the 7 DateDue notes by entry code; stamp lines last on #6/#25/#31 with the flags quoted; V-7/V-8/V-9 hold on all 35 keys (goldens hand-authored to the key will validate); Revolut and BPI description contents and currencies; bnkchk rows; docDate/movDate patterns (BPI-DO 13 embedded dates, card 5 transaction dates, Revolut none). PDFs via `pdftotext -layout`: Tranquilidade #6, #25, #31 (every figure quoted above), Revolut p.1 and the 31 Jan FX block, BPI card, BPI-DO 202507 (balances, the FAT row), BCP 202509, BPI-USD 202604, Itoorer R26030003, Continente. Reserve doclog codes in `05-doclog-all.txt`.

#### Taken from the spec without verification

The CLI's `Read` 2,000-character truncation, `CLAUDE.md` auto-load, and the pinned CLI's flag semantics (`--restricted`, `--tools`, `--settings` path rules, `--no-session-persistence`); token and context estimates and the two-sitting bench plan; the module deployment and rollback mechanics (run record §7o) and `provision-production.ps1` ACLs; that `git archive` yields CRLF here; `build_bnkmov.py` 77–84 (round-1/2 evidence, not re-read); the content of `EXTRACT.md` (unwritten); MEO/EDP/Avis/VFX/Locarent/AWS/Stripe wording cited in §3 and §5.1 (round-2 verified, not re-read); the eight reserve entries' existence beyond their doclog rows; page counts.

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** ae9883f (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, clean; code read on main at `2c8e5f2`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 2 / Medium 8 / Low 6

Revision 3 closes 26 of the 28 round-2 findings by text I could check against the code, the pinned CLI's `--help`, the skill build at `a558523`, the 40 keys and the PDFs. Every flag in the §4.3/§4.5 set exists on CLI 2.1.259 as named; `HeldForPerson` fits the enum; the package route through csproj `Content` works with `publish-release.ps1` unchanged; the bench needs no secrets; lease renewal (every `LeaseSeconds/3` = 100 s) covers a 1,350 s run. Two things contradict what I measured: the #25 example that the no-net split branch is built on (the document prints no 392.39 line) and the claim that a re-extract leaves the previous answer on disk (the evidence file names restart at `attempt-1` per job and are copied with overwrite).

#### Round-2 revisit

| id | State | Revision-3 text relied on; what I verified |
|---|---|---|
| C-N-1 | CLOSED | S-2 "pairs on `(posting_date ↔ movDate, amount, currency)` — unique across all 197"; S-11 tie-break; §5.5 18 corrections. Verified 0 collisions on `(movDate, amount, currency)` in all five keys (46/10/3/87/51). |
| C-N-2 | CLOSED | §5.2 `printedOpeningBalance/printedClosingBalance` hand-keyed with page refs; S-7 "never against `bnkchk`". Verified PDFs: Revolut "Saldo de abertura 40.06 / encerramento 164.94"; card "anterior 270,03 / actual 619,22"; key `bnkchk` starts 202501 (30.06) with no 202412 row. |
| C-N-3 | CLOSED | §2.3 `kind` row: invoice keeps the printed sign (#6 −0.03), credit note negative (#31); server appends `n + 1`; S-2 pairs `n + 1`. Verified PDF #6 footnote "(*) … − 0,03" and key line 3 = −0.03 (last); key #31 line 2 = −17.75 (last). |
| C-N-4 | PARTIALLY | §2.6 `Split(net, vat, total, duty, lines)` branches 0–3 are now decidable on contract fields (the finding's core). But branch (0)'s worked case — "when the document prints a single line whose amount equals `Total − duty` … (#25 … → item 392.39)" — is false on the PDF: it prints Prémio comercial 382,82 / Custos 0,00 / 0,00 / Prémio antes de impostos 382,82 / Imposto de selo 19,15 / Outros encargos e taxas (INEM, FAT) 9,57 / Total outras entidades 28,72 / Total 411,54. The key's 392.39 = 382.82 + 9.57 (the FDR folded the parafiscal charge into the item line). P-1. |
| C-N-5 | CLOSED | Q-EX-23 with recommendation and alternative; §2.6, §4.7, S-9, §5.4, §7 both branches. Key `R26030003`: company GOTT, flowType R, originClass Internal, issuer PT513362061, entity IE3559150RH — matches the recommendation's expected value. |
| C-N-6 | CLOSED | §4.3 `CliVersion` verified at start, "host-stopping failure (the host throws)"; §4.6 gate policy; §7 `WorkerStartupTests`. `ClaudeCliProcess.VersionAsync` exists (20 s cap, swallows failures to "unavailable (…)"); see P-14 for the 20 s health window. |
| C-N-7 | CLOSED | §5.2 `netVatNotPrinted` (#6, #25); S-9 scores net/vat only where false. Keys: #6 net 767.02 / vat 0 (gross as net), #25 net 411.54 / vat 0. |
| C-N-8 | CLOSED | S-1 "movements (movement cells plus the statement cells `movement_count`, `opening_balance`, `closing_balance`)". |
| C-N-9 | CLOSED | Q-EX-7 "The owner confirms the movements gate, which goes beyond the ruling's words". |
| C-N-10 | CLOSED | Q-EX-13 "11 of 35 keys" — computed 11. |
| C-N-11 | CLOSED | §5.2 and S-4 "5 + 2" with entry codes. Verified notes: five "+30" (I26020038, I26030044, I26040053, I26050001, I26060014), two credit notes "= DateDoc" (I26030019, I26030031). |
| C-N-12 | CLOSED | §5.6 `reserve-set.csv` names LG000015 (not LG000914) and LG000795 (not LG001770). Verified in `05-doclog-all.txt`: LG000914 = "Duplicate / Re-render - not archived", LG001770 = unarchived `pdf-886122461.pdf`. |
| C-N-13 | CLOSED | §4.1 "1,433,312 bytes uncompressed; the file is 559,999 bytes" — measured 26 entries, 1,433,312 uncompressed, file 559,999. |
| C-N-14 | CLOSED | §5.5 "1 BPI-DO #39 row carrying an amount fragment" is among the 18 corrections. The quoted fragment is truncated — the key holds "… FAT-20250422904233 -2 500,00 USD" (P-13). |
| F-N-1 | CLOSED | As C-N-1. |
| F-N-2 | CLOSED | §4.3 "`ClaudeTimeoutSeconds` becomes 900 for every document — there is no kind hint"; retry at 1.5 × "only on a second timeout of the same job"; lease rule dropped ("`RenewLeaseLoopAsync` renews every `LeaseSeconds/3`" — code line 139: `Math.Max(10, LeaseSeconds/3)`, lease 300 → 100 s, so 1,350 s is covered). The detection and hold mechanics are not named — P-4. |
| F-N-3 | CLOSED | §4.3 flag set. Each verified in `claude.exe --help` (2.1.259): `-p`, `--output-format json\|stream-json`, `--add-dir`, `--tools`, `--restricted` ("confines the file tools to the working directories (--add-dir included)", drops code tools and WebFetch, ignores user/project/local settings; `--settings` still applies), `--strict-mcp-config`, `--disable-slash-commands`, `--permission-prompts none` ("anything that would prompt is denied automatically"), `--settings <file-or-json>`, `--no-session-persistence` ("only works with --print" — the worker uses `-p`), `--model` (full name accepted), `--effort low\|medium\|high\|xhigh\|max`, `--fallback-model` (exists, never passed). Web-egress document in §4.5; Windows path syntax an R-EX-2 item. Wording slip in §4.5 — P-11. |
| F-N-4 | CLOSED | Q-EX-22; §4.6 retention rule; `--no-session-persistence` in the production and bench flag set. |
| F-N-5 | CLOSED | As C-N-5. |
| F-N-6 | CLOSED | §4.7 `IntakeProcessingStatus.HeldForPerson`, `result_json` kept, `SubmitReviewAsync` accepts it, the gate runs on held rows; §7 rows. Enum `DocumentIntake.cs` 11–39 ends at `Quarantined = 8` (9 is free); no CHECK constraint on `processing_status` in the migrations; `Documents.razor` `StatusLabel` has a default arm. Post-correction transition and the API's candidate mapping are open — P-10. |
| F-N-7 | PARTIALLY | §4.7 "`CompleteAsync` replaces `result_json` … the previous answer survives as `attempt-N-result.json` beside the original and as its element of `evidence_json`". The DB half is right (`UPDATE docint SET processing_status = 3, result_json = $2`; evidence appended with `\|\|`). The disk half is wrong: `ClaudeDocumentProcessor.cs` 111 and 145 name the files `attempt-{job.AttemptCount}-stdout.txt` / `-result.json` in `<source dir>/evidence` and copy with `overwrite: true`; a new job's `AttemptCount` restarts at 1, so the v2 run's first attempt overwrites the original job's `attempt-1-*` files — P-2. A second re-extract collides on the unique idempotency key — P-3. |
| F-N-8 | CLOSED | §5.4 candidates "built from the keys' `company.{code,name,taxId}`"; §5.7 "needs no database"; §8. Verified every key carries `company.{code,name,taxId}`; six codes (CONF, FMAT, GOTT, ITOO, SILA, VIGA). |
| F-N-9 | CLOSED | Q-EX-15 "a bench `CLAUDE_CONFIG_DIR` and login owned by the owner … recorded in the score report"; §5.7. The binary the bench runs is not named — P-7. |
| F-N-10 | CLOSED | As C-N-2. |
| F-N-11 | CLOSED | §4.6 package committed under `src/Sibyla.Worker.Documents/skill/`, csproj `Content` with `CopyToPublishDirectory`, `publish-release.ps1` unchanged. Verified: the script is `dotnet publish -c Release -o <target>` only (lines 30–37); the worker csproj (Sdk.Worker) declares no `Content` today; `provision-production.ps1` 176 grants `.\SibylaWorker` ReadAndExecute on `C:\Apps\Sibyla\worker` with ContainerInherit/ObjectInherit, so `<release>\skill\` is readable. P-16 for the `None Remove`. |
| F-N-12 | CLOSED | As C-N-11. |
| F-N-13 | CLOSED | §5.2 `printOrder` ∈ {`oldest_first`, `newest_first`, `sectioned`}. |
| F-N-14 | CLOSED | Q-EX-15 "full model id read from the first bench run's `modelUsage`"; §4.3 "`--fallback-model` is never passed"; §7 `ClaudeCliPermissionTests` asserts the argument list. `--model` help: "Provide an alias … or a model's full name". |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| P-1 | High | §2.6 Split branch (0); §5.3 S-3; §7 scoring row ("#25 exercising branch (0)") | The no-net branch's item-line clause needs "a single printed line whose amount equals `Total − duty`" (392.39). #25 prints no such line; the printed lines are 382.82 (premium), 0.00, 0.00, 19.15 (stamp), 9.57 (INEM/FAT), 28.72 (other-entities subtotal), 411.54 (total). An honest §2.3 extraction yields lines [382.82 item, 9.57 item, 19.15 stamp] (or 2 + stamp), so by `line_no` pairing every #25 line cell and `line_count` miss by construction, and the §7 fixture "typed to #25" cannot be typed from the document to reach the key (392.39 + 19.15). The 9.57 is captured only as the header string `other_taxes_printed`; nothing folds it into a line. | `pdftotext -raw Gott/Payables/202608/Gott_InvoiceReceipt_Tranquilidade_202608_01.pdf` lines 42–48, 53; key `I26080026.json` lines (392.39, 19.15), net 411.54, vat 0. | Either exclude #25's line cells and `line_count` by a flag (the contract reviewer's alternative in round 2) and let the §7 branch-(0) fixture be synthetic, or define the parafiscal fold (server: a printed `other_taxes` amount joins the item line's net when `Σ printed items + duty + other = total`), as an owner question since it changes stored Net. |
| P-2 | High | §4.7 Re-processing; Q-EX-18 | "The previous answer survives as `attempt-N-result.json` on disk" contradicts the processor: evidence files are named by the job's attempt number, which restarts at 1 for the new `docint:<id>:process:v2` job, and are copied with `overwrite: true` into the same `<staging dir>/evidence/` folder. The v2 run overwrites `attempt-1-stdout.txt` and `attempt-1-result.json`; the old `evidence_json` element's `RawStdoutPath` then names a file whose content no longer matches its `RawStdoutSha256`. | `ClaudeDocumentProcessor.cs` 109–112, 144–145; `QueueWorker.ClaimAsync` `attempt_count = q.attempt_count + 1` on a fresh job. | Name the evidence files per job (`<jobId>-attempt-N-…` or a `<contract>/` subfolder) in §4.3/§4.7 and make the review-page test assert both files exist after a re-extract. |
| P-3 | Medium | §4.7 Re-processing | `ix_jobque_idempotency_key` is UNIQUE, so `docint:<id>:process:v2` allows exactly one re-extract per row for all time; a v2 job that dead-letters (or a later contract change) cannot be re-run. | Migration `20260827172608_IntakeAndQueue.cs` 88–92 (`unique: true`); jobs are never deleted. | Key = `docint:<id>:process:<contract>:<n>` (n = count of existing jobs for the row + 1) or the enqueue instant; state it. |
| P-4 | Medium | §4.3 Timeouts; §7 `ClaudeCliTimeoutTests` | The retry-at-1.5× and the hold need seams the code lacks and the spec does not name: (a) `ClaimedJob` carries no `last_error`, so "a second timeout of the same job" must be read from `docint.evidence_json` (prior element with `ExitCode = -1`) or added to the claim's RETURNING; (b) `IClaudeCli.RunAsync(prompt, jobDir, ct)` has no timeout argument — the 900/1,350 s choice cannot reach `ClaudeCliProcess`, which reads `opts.ClaudeTimeoutSeconds`; (c) `JobOutcomeKind` is Success/Retry/Dead and `CompleteAsync` maps Dead to status 5 — a hold outcome that writes status 9 with `result_json` kept (V-19 is a Success-with-hold; the third timeout has no result) and still runs the gate is a new kind; (d) "two consecutive timeouts pause the lane" is a per-process counter shared by the two slots, and `PauseLaneAsync` fires only on a message containing "Claude unavailable/limited". | `ClaimedJob.cs`; `QueueWorker.cs` 111–116, 222, 340–424; `ClaudeCli.cs` 14, 85–86. | Name each seam in §4.3/§4.7 so the timeout and gate tests have an expected call: a `timeoutSeconds` parameter on `RunAsync`, the timeout count read from `evidence_json`, a `JobOutcome.Hold(resultJson?, evidence)` kind, and the lane counter's scope. |
| P-5 | Medium | §4.5 deny list | "`C:\Apps\Sibyla\**` except the package" is not expressible in the CLI's permission model: deny rules take precedence over allow rules, so a deny on `C:\Apps\Sibyla\**` also denies `Read(C:\Apps\Sibyla\worker\<release>\skill\**)`. | Documented Claude Code permission precedence (deny > allow); `--help` text for `--settings`; not runnable here (rule syntax is already an R-EX-2 item). | Enumerate the denies (`C:\Apps\Sibyla\tools\**`, `web\**`, `api\**`, `worker\<other releases>\**`, `worker\<release>\*.json`, `*.dll`) and rely on `--restricted`'s working-directory confinement as the primary control; the R-EX-2 probe reads the package and a sibling `appsettings.json` and records both outcomes. |
| P-6 | Medium | §4.3 `ExtractionRunner`; §5.7; §8 | The processor writes `evidence/` beside the source file. A runner "taking a file path" run over `D:\fileStorage\repos\invoice-skill-build\<archive_path>` writes `evidence/` folders into the skill-build tree — §8's "the invoice-skill-build repository is not modified" (a hard rule) — unless the runner takes an evidence root. The spec names only the bench's outputs under `tests/…/evidence/extract-v2/`. | `ClaudeDocumentProcessor.cs` 109 (`Path.GetDirectoryName(sourceFile)/evidence`). | `ExtractionRunner(filePath, sandboxRoot, evidenceRoot, …)`; the bench copies each file into its own sandbox and writes evidence under `tests/Sibyla.Tests.Argus/evidence/extract-v2/<run>/`; `git -C invoice-skill-build status --porcelain` empty is a bench post-condition. |
| P-7 | Medium | §5.7; Q-EX-15 | The bench's CLI binary is unnamed. `WorkerOptions.ClaudeCliPath` defaults to `claude`, which `ResolveCliExecutable` turns into `%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe` of the account running it — the owner's npm install, whatever version — while production's path lives in `worker.json` (`Worker:ClaudeCliPath`). The tuple's `CliVersion` would be recorded but not enforced by the bench. | `ClaudeCli.cs` 105–115; run record §4 line 72; `provision-production.ps1` 158–170 (Administrators FullControl on `tools`, so the owner can run `C:\Apps\Sibyla\tools\claude\claude.exe`). | §5.7: the bench sets `ClaudeCliPath = C:\Apps\Sibyla\tools\claude\claude.exe` and refuses to start on a `CliVersion` mismatch exactly as the worker does. |
| P-8 | Medium | §5.1 #21/#27; §5.6 R2/R3; §2.1 | Two sample PDFs carry two invoices each with their own ATCUD (Águas do Porto: water + a second; EDP: 32.23 + CAV 3.02, bill total 36.97); the keys hold one invoice each, and the reserve's R2/R3 are the other invoice of the same files. The contract returns one `header`, so one answer per file cannot match both keys, and no rule says which invoice is "the" answer or what `total_amount` is (32.23 vs the printed "Quanto tenho a pagar" 36.97). | Keys `I26070079.json`, `I26080035.json` `flagReviewNotes` ("carries TWO invoices with their own ATCUDs", "same PDF, own ATCUD, booked separately"); §2.1 shape. | Rule in §2.1/§5.3: the answer is the first-printed invoice (by ATCUD/number order) and `document_id` decides the key; R2/R3 leave the reserve (or the contract gains `additional_documents[]`, an owner question). |
| P-9 | Medium | §5.7 Plan vs Q-EX-9/§5.6 | Sitting 2 is "statements + reserve", but the bench is re-run while the package, `EXTRACT.md`, model or effort are tuned; the reserve would be opened on every sitting 2, against "opened once at R-EX-3 … never opened while … being tuned". | §5.7 "two sittings (fiscal documents; statements + reserve)"; §5.6 last sentence; Q-EX-9. | Sittings during tuning = fiscal / statements; the reserve is a third, single sitting under the frozen tuple at R-EX-3 (its 10 files ≈ 2 hours by the spec's own estimate). |
| P-10 | Medium | §4.7 `HeldForPerson` | Transitions are unnamed: what status a held row takes after `SubmitReviewAsync` (DeadLetter → Processed today; a held statement has "no completion path", so is submit refused for `bank_statement`?); `ChannelIntakeService.cs` 288 maps every non-Quarantined status to `CandidateStatus.Completed`, so a held statement reports Completed to the intake API the owner ruled unchanged; the duplicate branch of `AssignCompanyFromResultAsync` (status 6) can overwrite a hold in the same transaction. | `IngestionService.cs` 302–314; `ChannelIntakeService.cs` 288; `QueueWorker.cs` 322–325. | A transition table in §4.7 (held → Processed on a fiscal correction; held statement: submit refused with the page's message; held + duplicate → PossibleDuplicate with the hold reason kept); the API's candidate status for a held row named (Completed with a reason, or a new value, as an owner note under Q-EX-16). |
| P-11 | Low | §4.5 first bullet | "ignores user/project/local settings (`--strict-mcp-config`, `--disable-slash-commands`)" attributes `--restricted`'s effect to the wrong flags: `--strict-mcp-config` = "Only use MCP servers from --mcp-config"; `--disable-slash-commands` = "Disable all skills". | `claude.exe --help`. | Reword; keep all three flags. |
| P-12 | Low | §4.2 rule (2); §4.5 | The build refuses `CLAUDE.md` in the package, but the CLI discovers `CLAUDE.md` from the cwd's ancestors (`D:\ApolloData\work\<owner>`, `D:\ApolloData\work`, `D:\ApolloData`, `D:\`) and from `CLAUDE_CONFIG_DIR`; `--restricted` ignores settings files, not memory files. Today's worker has the same exposure. | `--help`: `--bare` and `--safe-mode` are the only flags naming CLAUDE.md discovery; `--safe-mode` "disables CLAUDE.md, skills, plugins, hooks, MCP … Auth, model selection, built-in tools, and permissions work normally". | R-EX-2 probe: a planted `CLAUDE.md` above a temporary sandbox root must not appear in the `stream-json` trace; evaluate `--safe-mode` in the flag set. |
| P-13 | Low | §5.5; §5.3 S-11 | "17 Revolut rows whose key description … carr[ies] the USD amount and the exchange-rate sub-line": 15 key descriptions match `Taxa|\$|USD`; the other two of the 18 differ for another reason and must be found by page reference at RED. The BPI fragment reads "-2 500,00 USD", not "-2 5". | Regex over `REV-DO-GOT-EUR_202512.json` (15/51); `BPI-DO-GOT-EUR_202507.json` description text. | "18 rows (15 with the FX sub-line, 2 otherwise column-sliced, 1 BPI amount fragment)", quotes corrected. |
| P-14 | Low | §4.3, §4.6 vs deployment health check 7 | The module's worker health is "same pid for 20 s; ExecutablePath = the activated exe". `VersionAsync` caps `--version` at 20 s and turns a slow answer into "unavailable (…)" — the mismatch would then surface at the edge of the health window; and a throw inside `BackgroundService.ExecuteAsync` stops the host with exit code 0 (a clean SCM stop, no recovery action). | `ClaudeCli.cs` 47–55; `local/deploy/tests/Sibyla.Deployment.Tests/oracles/health.v2.json` line 12; `apollo-deployment-slice2-spec.md` 338. | Run the version and hash checks in `IHostedService.StartAsync` before the slots start and rethrow so `Host.Run()` fails with a non-zero exit; cap the check at 10 s. |
| P-15 | Low | §7 `SkillPackageTests` | "the committed package equals a fresh build from the pinned commit" needs `D:\fileStorage\repos\invoice-skill-build` at test time; `local\test.ps1` runs every test project unfiltered, and the CI policy is local-first, so name the path's source and the behaviour when it is absent (fail with the reason, never skip silently). | `local/test.ps1`; `Build-SkillPackage.ps1` source path. | An env var or `local/skill-build.path` read by the test; absent → a single explicit failure. |
| P-16 | Low | §4.6 csproj | Sdk.Worker's default `None` glob already includes `skill\**`; the `Content` item must be paired with `<None Remove="skill\**" />` or the publish carries duplicate items. | `Sibyla.Worker.Documents.csproj` (no item groups today). | State both lines. |

#### Owner questions Q-EX-0..23

All 24 recommendations are defensible as written. Q-EX-21's flag set is verified flag-by-flag on the pinned CLI; Q-EX-23's expected value is verified on key #29; Q-EX-15's `--effort` and full-model-id mechanics exist on 2.1.259. Three notes and two additions:

- **Q-EX-18** — the recommendation's guarantee ("the previous answer surviving as `attempt-N-result.json` on disk") does not hold until P-2 is fixed; the owner should rule on the corrected wording.
- **Q-EX-15** — name the bench's binary as well as its login (P-7): `C:\Apps\Sibyla\tools\claude\claude.exe`.
- **Q-EX-9** — the reserve's single opening is incompatible with the two-sitting plan as written (P-9); the recommendation should say "a third sitting under the frozen tuple".
- **Add Q-EX-24 — two-invoice bills.** When one PDF prints two invoices with their own ATCUDs (#21, #27, and their reserve twins R2/R3), which is the row the contract answers, and is the twin dropped from the reserve? Recommendation: the first-printed invoice; the twin leaves the reserve. (P-8; it decides how gateway documents of this shape are booked.)
- **Add Q-EX-25 — parafiscal surcharges (INEM/FAT).** The FDR folded #25's 9.57 into the item line's net (392.39 = 382.82 + 9.57). Does the server do the same from `other_taxes_printed`, or does the charge stay a header note and #25's lines leave the score? Recommendation: exclude #25's line cells by flag for v1 and rule the fold with the persistence slice (P-1).

#### Readiness for RED oracles

Not yet. Typeable now with expected values: `ExtractionContractV2Tests` (accept/reject corpus), `SkillPackageTests` (once P-15 names the path source), `ProcessingEvidenceTests`/`WorkerStartupTests`, `ClaudeCliPermissionTests` (the exact argument list), `EvidenceSecretScanTests`, `CompanyMatcherTests`, `AccountPeriodTests`, `ExtractionResultIdentityTests`, `QueueWorkerGateTests` (#22/#23/#17/#29 both branches; DB-backed like `ProcessingEvidenceTests`, which already runs the processor against `TestDatabase.Migrator` with a fake CLI), and every `ExtractionScoringTests` fixture except one. Cannot be typed from the spec as it stands: the scoring fixture "the split function's four branches, #25 exercising branch (0)" (P-1 — the document has no 392.39 line); the review-page assertion for a re-extract's surviving evidence (P-2/P-3 — file naming and the second-run key); `ClaudeCliTimeoutTests` "a second timeout of the same job retries once at 1,350 s and a third lands the row in `HeldForPerson`" (P-4 — no seam carries the timeout, the prior-timeout fact or the hold outcome). Each is a few sentences; nothing structural.

#### Verified myself

- Spec and review record at `ae9883f`; worktree clean. Main at `2c8e5f2`: `WorkerOptions` (240/300/2/10, `ClaudeCliPath = "claude"`), `ClaudeCli.cs` (argument list, 20 s version cap, timeout kill, npm fallback resolution), `ClaudeDocumentProcessor` (evidence dir beside the source, `attempt-N` names, overwrite copies, prompt hash), `QueueWorker` (claim RETURNING columns, renewal every `max(10, Lease/3)` s, lane pause on the "Claude unavailable/limited" substring, `AssignCompanyFromResultAsync` top-level `recipient_tax_id` against `commst`, `CompleteAsync` overwriting `result_json` and appending `evidence_json`, backoff 30/120/600/1800/3600, `JobOutcomeKind` Success/Retry/Dead), `Program.cs` (secrets file, dp-keys), `IntakeProcessingStatus` 0–8, `IngestionService` (`SubmitReviewAsync` accepts Processed/DeadLetter, literal `apollo.extract.v0`, `ReleaseHeldProcessing`, idempotency keys `docint:<id>:process`), `Documents.razor` flat v1 form and `StatusLabel` default arm, `ChannelIntakeService.cs` 288, `ProcessingEvidence` fields, `ExtractionContract` v1, `CompanyMatcher.Match(string?, …)`, `QueueJob.MaxAttempts = 5`, `ix_jobque_idempotency_key` UNIQUE, no CHECK on `processing_status`; the worker csproj (Sdk.Worker, no items); `publish-release.ps1` (`dotnet publish` only); `provision-production.ps1` 158–170 (Set-ExactAcl: SYSTEM + Administrators FullControl, inherit flags), 176–177 (worker ReadAndExecute on `worker` and `tools`), the service environment (`SIBYLA_SECRETS_FILE`, `CLAUDE_CONFIG_DIR`), `sc failure` actions; `local/test.ps1` (no filter); test projects Platform and Argus reference the worker; `ProcessingEvidenceTests` is DB-backed via `Sibyla.Tests.Shared.TestDatabase.Migrator`; no `golden/` under Platform yet (Api has one via `None Update`).
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: `--version` = 2.1.259; `--help` for every flag in §4.3/§4.5 and their stated semantics; `--safe-mode`, `--bare`, `--fallback-model` text.
- Skill build: HEAD `a558523f…`, 2026-09-03 02:20:39 +0100, "Advance Stage 16 through Round 30", clean; heading anchors SKILL.md 16 (§1), 95 (§5), 410 (§8), 581 (§11), 595 (§12); lines > 1,900 chars: SKILL.md 16–409 = 4 (three > 2,000 plus line 408 at 1,993), 581–594 = 0, selected schema.md lines 17/78/87/93/95 (3,206 / 3,368 / 8,014 / 3,326 / 5,681), Financial Document Entry Policy = 1; the seven rule `.md` files exist; `vat_rates.json` 24,105 B; the cut without `EXTRACT.md` = 214,504 B (SKILL.md excerpt 66,391 B); zip 26 entries, 1,433,312 B uncompressed, 559,999 B file.
- Answer keys (40): 35 fiscal / 5 bank; 109 lines, 197 movements; docType 24/6/5; rules R1 12 / R3 12 / blank 10 / R2 1; six company codes with `code/name/taxId`; `(movDate, amount, currency)` unique on all five; 15 Revolut FX descriptions; the BPI fragment text; #6/#25/#31 lines and header figures; the 5 + 2 DateDue notes; the 6 receipts (dateDoc = dateDue = datePay); 11 `doclog.docDate ≠ dateDoc`; Revolut `bnkchk` from 202501 (30.06) with 13 periods; #29's company/flow/origin/ids; #21/#27 two-invoice notes and archive paths.
- PDFs (`pdftotext`): Revolut 202512 (40.06 / 164.94, newest-first), BPI-CC-ROA 202603 (270,03 / 619,22, "DATA DA TRANSACÇÃO"), Tranquilidade #6 (footnote "− 0,03", Total(*) 767,02), Tranquilidade #25 (the full amounts block quoted in P-1).
- Deployment: run record §7o (module deployment, bare-id pointers, `-Execute -Resume`, `rollback-release.ps1 -ToPath`), the module's health check 7 (`health.v2.json`: same pid 20 s, activated exe), spec line 338; `Worker:ClaudeCliPath` lives in `worker.json` (run record §4).
- `05-doclog-all.txt`: LG000015 / LG000914 (Duplicate, not archived), LG000795 / LG001770 (unarchived).

#### Taken from the spec without verification

- CLI runtime behaviour I could not exercise: the `Read` 2,000-character truncation; the `json` envelope's `num_turns`/`usage`/`modelUsage`; that `--restricted` still reads the login under `CLAUDE_CONFIG_DIR`; deny-over-allow precedence and the Windows `Read(...)` rule syntax (P-5 relies on documented precedence); the usage-limit message format the bench would parse.
- The token and context estimates (§4.2, §5.7) and the subscription's 5-hour window; the two-sitting arithmetic is the spec's.
- That the release gate's "clean-export fingerprint" is what binds the committed package (it is a review-evidence practice over the source tree, which includes committed files).
- `git archive` yielding CRLF on this machine; the personal-name and NIF occurrences in the cut (round-1/2 evidence, not re-read); `build_bnkmov.py` 77–84 and `SyncEngineWave3.ToCashDelta`; `DocumentCaptureService` C6; page counts of the sample PDFs; the content of `EXTRACT.md` (not yet written); the existence in `gott_sibyla` of the eight reserve entry codes beyond the doclog listing.

### Author's dispositions — revision 4

Revision 4 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 3. Ids: `C-P-n` = the contract reviewer's P-1…P-10, `F-P-n` = the feasibility reviewer's P-1…P-16. Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 4. Decisions the coordinator took for this revision are written in and not re-opened.

**Contract reviewer (C-P-1 … C-P-10)**

| id | Sev | Disposition | Where in revision 4 |
|---|---|---|---|
| C-P-1 | High | fixed + owner question — `other_taxes_amount` (d?) added beside `other_taxes_printed`; `Split` gains step C, the parafiscal fold per SKILL.md §6 (one item line → `Net := Total − duty`; several → the largest, flagged), inside the server transform the FDR runs; #25 traced: printed 382.82 / 0.00 / 0.00 / 19.15 / 9.57 / 28.72 / 411.54 → lines 392.39 / 19.15; #31's key (stamp −17.75 = 12.38 + 5.37) contradicts SKILL.md §6 and is pre-declared as a correction (stamp −12.38, item −143.02); Q-EX-4 says the transform includes the fold; Q-EX-25 added with the fold as the recommendation and the feasibility reviewer's exclusion-by-flag as the alternative | §2.2 `other_taxes_amount`; §2.6 Split; §5.5; §1.2 Q-EX-4, Q-EX-25; §7 |
| C-P-2 | High | fixed — branch (0) is terminal (no net printed → the Total already contains the duty → `Net := Total − (VAT ?? 0)`, done); branches (1)–(3) apply only when a net is printed; the stamp line is appended in every branch including (3) (step A); #6 traced: net null, vat null, total 767.02, duty −0.03 → (0) → Net 767.02, lines 634.72 / 132.33 / −0.03; only #6/#25/#31 carry stamp lines, so (1)/(2) are synthetic fixtures with named figures | §2.6 Split; §7 scoring row |
| C-P-3 | Medium | fixed — §2.5b classifies all 24 DOCLOG columns as from the extraction (`DocumentType`, `OriginClass`, `Date`), derived later (`Company`, `Entity`, `ItemCode`/`ItemDesc`, `Flag`/`FlagCategory`/`RiskFactor`, `CaptureQuality` — not computed by Sibyla, null in all 40 keys) or system (the rest, plus `CapturedAt`) | §2.5b |
| C-P-4 | Medium | fixed — doctrine clause (f): a POS receipt with a VAT summary is itemised at its VAT-rate groups (net, VAT, total from the summary block), product rows and discounts summarised in a note; golden #2 typed to that; Q-EX-20 amended | §2.3 doctrine; §1.2 Q-EX-20; §7 golden row |
| C-P-5 | Medium | fixed — `EXTRACT.md` states document kind by fiscal function: an itemised receipt detail (#6) and a payment-portal charge (#12) are `invoice`; their `document_type` cells stay scored | §2.3; §4.2 `EXTRACT.md` row |
| C-P-6 | Low | fixed — "18 rows (15 with the FX sub-line, 2 otherwise column-sliced, 1 BPI amount fragment)"; the BPI quote corrected to "… -2 500,00 USD"; the 15 Revolut dates listed; rows keyed by `(movDate, amount)` | §5.5 |
| C-P-7 | Low | fixed — an amount in the debit/outflow column is negative and in the credit/inflow column positive on column-formatted statements; Revolut sub-lines (exchange rate, "$127.51") are not the description | §2.4 `amount`, `description_printed`; §4.2 `EXTRACT.md` row |
| C-P-8 | Low | fixed — occurrence order = the print order of the answer (`seq`) against the `bm_code` order of the key | §5.3 S-2 |
| C-P-9 | Low | fixed — `fiscal_no` compared under `CompanyMatcher.SameTaxId` semantics (prefix tolerated on either side) | §5.3 S-9 |
| C-P-10 | Low | fixed — "4 X 11,99" | §2.3 `unit_price_basis` |

**Feasibility reviewer (F-P-1 … F-P-16)**

| id | Sev | Disposition | Where in revision 4 |
|---|---|---|---|
| F-P-1 | High | fixed + owner question — as C-P-1: the fold is defined on the server from `other_taxes_amount` per SKILL.md §6 (the pinned skill's rule), #25's expected line 1 = `Total − duty` = 392.39; the reviewer's exclusion-by-flag is Q-EX-25's alternative; #31 corrected | §2.2; §2.6; §5.5; Q-EX-25 |
| F-P-2 | High | fixed — evidence files per job: `<staging dir>/evidence/<jobId>/attempt-N-{stdout.txt,result.json}`, nothing overwritten; the review-page test asserts both jobs' files exist after a re-extract and that the old `evidence_json` element's `RawStdoutSha256` still matches its file; Q-EX-18's wording corrected | §4.3 evidence bullet; §4.7 Re-processing; §1.2 Q-EX-18; §7 |
| F-P-3 | Medium | fixed — idempotency key `docint:<id>:process:<contract>:<n>`, n = count of existing jobs for the row + 1; the second re-extract gets `…:3` in the test | §4.7; Q-EX-18; §7 |
| F-P-4 | Medium | fixed — the seams named: `IClaudeCli.RunAsync(prompt, jobDir, timeoutSeconds, ct)`; the prior-timeout fact read from `docint.evidence_json` (an element for this job with `ExitCode = −1`); `JobOutcome.Hold(resultJson?, evidence)` writing status 9 with `result_json` kept and the gate run; the lane-pause counter per process across both slots, separate from the "Claude unavailable/limited" pause; `ClaudeCliTimeoutTests` typed to those calls | §4.3 Timeouts; §4.7; §7 |
| F-P-5 | Medium | fixed — the deny list enumerated (`tools\**`, `web\**`, `api\**`, `worker\<other releases>\**`, `worker\<release>\*.json`, `**\*.dll`, plus the secrets/config/keys/staging roots and `local\**`); `--restricted`'s working-directory confinement named as the primary control; the R-EX-2 probe reads the package and a sibling `appsettings.json` and records both outcomes | §4.5; §8 R-EX-2; Q-EX-21 |
| F-P-6 | Medium | fixed — `ExtractionRunner(filePath, sandboxRoot, evidenceRoot, …)` copies each file into its own sandbox and writes evidence under `evidenceRoot`; the bench writes under `tests/Sibyla.Tests.Argus/evidence/extract-v2/<run>/`; `git -C D:\fileStorage\repos\invoice-skill-build status --porcelain` empty is a bench post-condition | §4.3; §5.7 |
| F-P-7 | Medium | fixed — the bench sets `ClaudeCliPath = C:\Apps\Sibyla\tools\claude\claude.exe` and refuses on a `CliVersion` mismatch like the worker; Q-EX-15 names it | §5.7; §1.2 Q-EX-15 |
| F-P-8 | Medium | owner question — Q-EX-24: the answer is the first-printed invoice (by ATCUD/number order), `document_id` decides the key, `total_amount` is that invoice's total (#27: 32.23); R2/R3 leave the reserve, replaced by I26080031 (CONF, LG001801) and I26070074 (ITOO, LG001795); alternative `additional_documents[]` in a later contract | §1.2 Q-EX-24; §5.1 #27; §5.6; §4.2 `EXTRACT.md` row |
| F-P-9 | Medium | fixed — tuning sittings = fiscal / statements; the reserve is a third, single sitting under the frozen tuple at R-EX-3; Q-EX-9 amended | §5.7 Plan; §5.6; §1.2 Q-EX-9 |
| F-P-10 | Medium | fixed — the `HeldForPerson` transition table: held fiscal document → `Processed` on `SubmitReviewAsync`; held statement → submit refused with the page's message; held + duplicate → `PossibleDuplicate` with the hold reason kept in the audit detail; re-extract → `Queued`; the channel-intake API keeps its vocabulary and reports `completed` for a held row, noted under Q-EX-16 with "a new candidate status in a later API revision" as the alternative | §4.7 transitions; §1.2 Q-EX-16; §7 holds row |
| F-P-11 | Low | fixed — the `--restricted` bullet reworded: `--restricted` confines the file tools and ignores settings files; `--strict-mcp-config` admits only command-line MCP servers; `--disable-slash-commands` disables skills; all three kept | §4.5 |
| F-P-12 | Low | fixed — `--safe-mode` added to the flag set (disables `CLAUDE.md` discovery, skills, plugins, hooks, MCP); R-EX-2 probe (b): a planted `CLAUDE.md` above a temporary sandbox root must not appear in the `stream-json` trace while package reads still work; if `--safe-mode` breaks the run, R-EX-2 records it and the flag leaves the set by amendment | §4.3; §4.5; §8 R-EX-2; Q-EX-21; §7 permissions row |
| F-P-13 | Low | fixed — as C-P-6 (15 + 2 + 1; the corrected quote) | §5.5 |
| F-P-14 | Low | fixed — the version and package-hash checks run in `IHostedService.StartAsync` before the slots start, capped at 10 s, rethrown so `Host.Run()` exits non-zero (not the clean exit of a throw in `ExecuteAsync`); `WorkerStartupTests` typed to it | §4.3; §4.6 Deployment; §7 |
| F-P-15 | Low | fixed — `SkillPackageTests` reads the skill-build path from `SIBYLA_SKILL_BUILD_PATH` or `local/skill-build.path`; absent → one explicit failure, never a silent skip | §7 |
| F-P-16 | Low | fixed — both csproj lines: `<Content Include="skill\**" CopyToPublishDirectory="PreserveNewest" />` and `<None Remove="skill\**" />` | §4.6; §7 |

Owner questions after revision 4: Q-EX-0…23 keep their numbers (Q-EX-4, Q-EX-9, Q-EX-15, Q-EX-16, Q-EX-18, Q-EX-20, Q-EX-21 reworded as above), plus **Q-EX-24** (two-invoice bills: the first-printed invoice is the answer; the twins leave the reserve) and **Q-EX-25** (parafiscal surcharges: the server folds `other_taxes_amount` into the item line per SKILL.md §6, #31's key corrected; alternative: exclude by flag for v1).

## Round 4 — revision 4, head `7f86af2` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-3 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 2 / 3 / 6 | 24 of 26 (C-P-1 = F-P-1 partially) |
| feasibility | REVISE | 0 / 1 / 6 / 6 | 25 of 26 (F-P-10 partially, F-P-16 open) |

Shared finding (contract R-1 = feasibility R-6): the insurer documents #25 and #31 print a net ("Prémio antes de impostos"), so a printed-net answer takes Split branches (1)–(3), which have no `other` term and never fold into the header. Contract R-2: the Continente receipt prints no header net/VAT while the key holds the VAT-summary column sums. Feasibility R-1: the prescribed csproj `Content Include="skill\**"` double-includes the package JSON under the Worker SDK (NETSDK1022). Ids are per reviewer (C-R-n, F-R-n).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** 7f86af2 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main `2c8e5f2`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 2 / Medium 3 / Low 6

Revision 4 closes 24 of the 26 round-3 findings by text I checked against the keys, the PDFs at `a558523`, `05-doclog-all.txt` and the code; C-P-1/F-P-1 are partially closed. Both Highs sit where the header meets `Split`: the Tranquilidade documents #25 and #31 print a net ("Prémio antes de impostos"), which the spec asserts they do not, and with that figure returned the function lands #31 in its "no split" branch with a header net of 137.65 against a scored key of −155.40; and Continente #2 prints no header net/VAT at all, so §2.2's `null` rule contradicts the key's 60.55 / 13.56 on two scored cells and the golden #2's header cannot be typed.

#### Round-3 revisit

| id | State | Revision-4 text relied on and what I verified |
|---|---|---|
| C-P-1 | PARTIALLY | §2.2 `other_taxes_amount`; §2.6 step C; §5.5 #31 correction; Q-EX-4/25. Verified PDF #25 (`pdftotext -raw`, lines 40–48): 382,82 / 0,00 / 0,00 / "Prémio antes de impostos 382,82" / 19,15 / 9,57 / 28,72 / 411,54; key lines 392.39 + 19.15, net 411.54. PDF #31 lines 32–40: "Prémio antes de impostos (1) 137,65" / 12,38 / 5,37 / 17,75 / 155,40; corrected −143.02 + −12.38 = −155.40 = key net. SKILL.md line 160 fold rule quoted correctly. The line side is now derivable; the header side is not — step B has no `other` term and step C never adjusts the header (R-1). |
| C-P-2 | CLOSED | §2.6: branch (0) terminal, step A "in every branch", (1)–(3) only with a printed net. Traced #6 from the PDF (`Total Movimentos … 634,72`, `132,33`, footnote "− 0,03", "Total(*) 767,02", no net printed): net null, vat null → line 3 = −0.03, Net = 767.02 → 634.72 / 132.33 / −0.03 = key. Fixtures computed: (1) 100+23+4 = 127 → 104/23; (2) 100+27 = 127, 27 ≥ 4 → 104/23; (3) neither → finding, line appended. |
| C-P-3 | CLOSED | §2.5b: 3 (DocumentType, OriginClass, Date) + 8 derived + 13 system = 24; every name matches schema.md line 17's list (parsed: 24). |
| C-P-4 | CLOSED (lines) | §2.3 clause (f); §7 golden #2. PDF #2 prints "(B) 13,00% 3,62 0,47 4,09 / (C) 23,00% 56,93 13,09 70,02"; key lines 3.62/0.47/4.09 and 56.93/13.09/70.02. The header on #2 is a new finding (R-2). |
| C-P-5 | CLOSED | §2.3 last paragraph and §4.2 `EXTRACT.md` row; keys #6 and #12 `docType = Invoice`; `DocumentTypeRouter.DocumentTypeFor("invoice") = "Invoice"`. |
| C-P-6 | CLOSED | §5.5 "15 + 2 + 1". Regex `\$|Taxa de c` over the Revolut key: exactly 15 rows on the 15 dates listed; BPI 2025-07-30 −2186.06 text is "…FAT-20250422904233 [spaces] -2 500,00 USD" (currency EUR) — the quote now matches. |
| C-P-7 | CLOSED | §2.4 `amount` (debit/outflow column negative) and `description_printed` (Revolut sub-lines excluded); §4.2 `EXTRACT.md` row. |
| C-P-8 | CLOSED | S-2 "occurrence order = the print order of the answer (`seq`) against the `bm_code` order of the key" — computable. Verified 0 ties on `(movDate, amount, currency)` on all five keys (46/10/3/87/51). Direction note in R-9. |
| C-P-9 | CLOSED | S-9 `SameTaxId` semantics. `CompanyMatcher.cs` 35–50: `Normalize` = letters+digits upper (≥ 5 chars); `SameTaxId` = equal, or `Bare` equal with a two-letter prefix on either side → "500 940 231" ↔ "PT500940231" and "19.628.811/0001-60" ↔ "BR19628811000160" match. The method is private (R-11). |
| C-P-10 | CLOSED | §2.3 "4 X 11,99"; PDF #2 "BUONDI NESPRESSO INTENSO 36CAP / 4 X 11,99 47,96". |
| F-P-1 | PARTIALLY | As C-P-1 (R-1). |
| F-P-2 | CLOSED | §4.3 "evidence files are per job: `<staging dir>/evidence/<jobId>/attempt-N-…`"; §4.7; §7 review-page assertion; Q-EX-18 reworded. Code today: `ClaudeDocumentProcessor.cs` 109–111 (`<source dir>/evidence`, `attempt-{AttemptCount}`). |
| F-P-3 | CLOSED | §4.7/Q-EX-18/§7 key `docint:<id>:process:<contract>:<n>`, second re-extract `…:3`. Today `docint:{id}:process` (`IngestionService.cs` 208/455/575); `ix_jobque_idempotency_key` unique (migration line 89). |
| F-P-4 | CLOSED | §4.3 Timeouts: `RunAsync(prompt, jobDir, timeoutSeconds, ct)` (today 3-arg, `ClaudeCli.cs` 14), prior timeout from `evidence_json` `ExitCode = −1`, `JobOutcome.Hold` (today `Success/Retry/Dead`, `QueueWorker.cs` 506), per-process lane counter; §7 timeouts row typed to those calls. |
| F-P-5 | CLOSED | §4.5 enumerated denies, `--restricted` primary, R-EX-2 probe (a) in §4.5/§8. Rule syntax not runnable here (R-EX-2 item). |
| F-P-6 | CLOSED | §4.3 `ExtractionRunner(filePath, sandboxRoot, evidenceRoot, …)`; §5.7 `status --porcelain` empty as a post-condition. |
| F-P-7 | CLOSED | §5.7 and Q-EX-15: `ClaudeCliPath = C:\Apps\Sibyla\tools\claude\claude.exe`, `CliVersion` refusal. |
| F-P-8 | CLOSED | Q-EX-24, §5.1 #27, §5.6 R2/R3. Verified: EDP PDF prints FT2026 K3426/340027209607 (32,23) first (raw line 96), CAV FT2026 K1726/170016185242 (3,02) at line 153 → key `FT2026K3426340027209607` (N-2) and total 32.23; Águas do Porto prints 96340048 (66,27) before 1185418181 (27,12) → key 96340048 / 66.27. `05-doclog-all.txt` line 1587 `LG001801\|CONF\|I26080031` (EMPCO), line 1583 `LG001795\|ITOO\|I26070074` (`Itoorer_Invoice_Gott_202607_01.pdf`); neither in `sample-set.csv` (40 rows); the old twins I26080036 (LG001808) / I26070080 (LG001810) exist and share the sample files. Count slip in R-8. |
| F-P-9 | CLOSED | §5.7 "two tuning sittings … reserve in a third, single sitting under the frozen tuple"; §5.6; Q-EX-9. |
| F-P-10 | CLOSED | §4.7 transition table (5 rows); Q-EX-16 intake note. `ChannelIntakeService.cs` 288 maps every non-Quarantined status to `Completed`; `IngestionService.cs` 302 accepts Processed/DeadLetter today. |
| F-P-11 | CLOSED | §4.5 first bullet attributes each effect to its own flag (help text taken from the round-3 record). |
| F-P-12 | CLOSED | `--safe-mode` in §4.3, §4.5, Q-EX-21, §7 permissions row, §8 probe (b) with the amendment path. |
| F-P-13 | CLOSED | As C-P-6. |
| F-P-14 | CLOSED | §4.3 `IHostedService.StartAsync`, 10 s cap, rethrow → non-zero exit; §4.6 Deployment; §7 `WorkerStartupTests`. |
| F-P-15 | CLOSED | §7 `SkillPackageTests`: `SIBYLA_SKILL_BUILD_PATH` or `local/skill-build.path`; absent → explicit failure. |
| F-P-16 | CLOSED | §4.6 both csproj lines; §7. The worker csproj today is the Sdk line only. |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| R-1 | High | §2.2 `net_amount`; §2.6 Split steps B/C; §5.2 `netVatNotPrinted`; S-9 | #25 and #31 print a net: "Prémio antes de impostos 382,82" and "Prémio antes de impostos (1) 137,65"; §2.2's "(Tranquilidade's breakdown prints only a gross)" is true of #6 only. With the printed net returned, step B has no `other` term: #31 → (1) 137.65+0+12.38 = 150.03 ≠ 155.40; (2) 137.65 ≠ 155.40; → (3) "no header split, `reconciliation_note` required, DPRCHK finding" on a document that reconciles exactly (137.65+12.38+5.37 = 155.40). Step C sets the item line to 143.02 but never touches the header, so header Net stays 137.65 against the key's −155.40 — a **scored** cell (#31 is not flagged `netVatNotPrinted`) that misses by design, and Σ lines 155.40 ≠ header Net 137.65 after the transform. #25 takes the same path (header unscored by flag, same spurious finding and inconsistency). The #25 trace and the un-traced #31 rest on the model returning `null` for a figure printed under the words "before taxes", with no rule saying so. | PDF #25 raw line 40; PDF #31 raw line 32; key `I25040004.json` net −155.4, vat 0; §5.2 flags name #6 and #25 only; §2.6 step C text. | (a) Step B tests include `other`: (1) `net + vat + duty + (other ?? 0) = total`, (2) likewise; and when a net is printed the header gains `other` with the fold (`Net += other`), so header = Σ lines in every branch. (b) Trace #31 in §2.6 both ways (net null → (0); net 137.65 → (1) Net 150.03 → C → 155.40; lines −143.02 / −12.38). (c) One rule for what an insurer's "Prémio antes de impostos" is — `net_amount` (then (a) carries it) or the item line only (then `netVatNotPrinted` must also cover #31, as the FDR stored gross-as-net on all three) — written into `EXTRACT.md`. |
| R-2 | High | §2.2 `net_amount`/`vat_amount`; §2.3 clause (f); §7 golden #2; S-9 | Continente #2 prints no header net or VAT figure: "SUBTOTAL 76,21 / Desconto Cartao Utilizado 2,10 / TOTAL A PAGAR 74,11" and a per-rate block with no totals row; the key's 60.55 / 13.56 are the block's column sums. Under "as printed … `null` when no net is printed" both are `null`, `Split` is the identity (duty and other null), and S-9 scores both cells (#2 is not in `netVatNotPrinted`) → two designed misses. Golden #2 is "hand-authored to the key" (60.55/13.56) and to §2.2 (`null`) — it cannot be typed. | PDF #2 raw lines 40–48; key `I26010026.json` net 60.55 / vat 13.56 / total 74.11; §5.2 flag list. | Extend clause (f) to the header: on a document printing a per-rate VAT summary and no single net/VAT figure, `net_amount`/`vat_amount` are the summary's column sums (Continente 60.55 / 13.56), in `EXTRACT.md` and golden #2; or flag #2 `netVatNotPrinted`. |
| R-3 | Medium | §2.3 doctrine (d) vs §2.6 #25 trace | #25's "Detalhe do Valor a Pagar" prints "Custos de fracionamento 0,00" and "Custos de gestão do seguro (1) 0,00". Doctrine (d) ("zero-amount printed lines are lines, AWS: 9") makes them item lines: 3 items + stamp = 4 vs the key's 2; step C's "largest item line" still gives 392.39 on line 1, but `line_count`, lines 2–3 and the stamp at line 4 (key: line 2) miss. The trace's "the single item line" has no clause producing it. | PDF #25 raw lines 41–42; key `lineCount = 2`. | Clause (g): a premium/charges breakdown block is not a line table — the item line is the premium; the block's zero rows and the "Total outras entidades" subtotal are not lines; #25/#31 typed to one item line + stamp. |
| R-4 | Medium | §2.6 branch (3) "reconciliation_note required (V-12)" | `Split` runs after validation; V-12 is defined on V-8/V-9 of the raw answer. When (3) fires on an answer that passed validation (e.g. `vat` null, so V-8 never applied — #31 above), whether a null note fails the document (retry → dead-letter) or only records a finding is unstated. | §2.6 vs §2.8 V-12 wording. | "Branch (3) records a DPRCHK finding and never fails the document; V-12 applies to the raw answer only." |
| R-5 | Medium | §5.6 R3; S-9 `company`, `fiscal_no` | R3 = I26070074 (Itoorer's payable leg of Gott → Itoorer). Under Q-EX-23's recommendation the gate answers GOTT / R / Internal for every Gott → Itoorer document, while R3's key is Company ITOO, flow I: the reserve `company`/`origin_class` cells miss by design. `fiscal_no` = "issuer on I / recipient on R" does not say whose flow — the key's (I → issuer PT513362061) or the derived (R → recipient IE3559150RH). | `05-doclog-all.txt` 1583 (`LG001795\|ITOO\|I26070074`); key #29 GOTT/R/Internal, fiscalNo IE3559150RH; §2.6 both-match rule. | S-9: the flow selecting `fiscal_no` is the key's; R3's gate cells are reported, not scored, under the recommendation (the FDR books both legs, one per book; Apollo answers one). |
| R-6 | Low | §2.6 step C | "`Net := Total − duty` (i.e. printed item + other)" — the two are equal only when VAT = 0 and there is one item line. | Arithmetic. | Rule = `item.Net += other`; `Total − duty` as the check. |
| R-7 | Low | §2.6 branch (2) | Claims SKILL.md §6's verification is "this branch's arithmetic"; SKILL.md line 160 zeroes the line's VAT and moves the whole old VAT to Net (`NetAmount += old VATAmount`, the remainder folded into the item), whereas (2) keeps `VAT − duty` as VAT. The fixture figures (104 / 23) are computable, so wording only. | SKILL.md a558523 line 160. | Drop the attribution or say (2) deliberately keeps the remainder as VAT. |
| R-8 | Low | Q-EX-24; §5.1 #27; §4.2 | The EDP bill prints four invoices with their own ATCUDs (…209607 32,23; K1726/170016185242 3,02; …208063 0,21; …208062 0,01) plus a 1,50 item "não serve de fatura"; 32.23+3.02+0.21+0.01+1.50 = 36.97. The first-printed rule still yields the key. | PDF #27 raw lines 96, 153, 185, 187, 222. | "two or more invoices"; `evidence.notes` names the further invoices. |
| R-9 | Low | S-2 occurrence order | Revolut `bm_code` ascends with `docDate` (BM001883 2024-12-10 … BM001933 2025-12-15) while the answer prints newest-first, so positional pairing runs in opposite directions. No ties on the 40; fixture-only. | Key `REV-DO-GOT-EUR_202512.json`; §5.1 #40. | Pair against the key's print order (`bm_code` reversed when `printOrder = newest_first`), or state the direction in the §7 fixture. |
| R-10 | Low | S-4 last sentence | "scores only the 5 + 2 flagged rows plus the receipts" — `dateDuePrinted` is hand-set from all 40 PDFs, so other rows with no printed due date (I25040004, I26080018 credit notes with `dateDue = dateDoc` and no note; I25120001 +30 with no note; #1/#19/#24 with `dateDue = dateDoc`) will also be false. A count claim, not a rule. | Key deltas computed over the 35. | Reword as "the rows flagged false plus the receipts". |
| R-11 | Low | S-9 `SameTaxId` | The method is `private` (`CompanyMatcher.cs` 42); the scorer needs it exposed or restated. | Code. | Make it internal/public or restate the rule in S-9. |

#### Owner questions Q-EX-0..25

All 26 recommendations are defensible as written. Verified against the artefacts: **Q-EX-24** — the first-printed rule reproduces both keys (#21 96340048 / 66.27; #27 FT2026K3426340027209607 / 32.23) and the replacements LG001801 / LG001795 exist and are outside the sample (count slip, R-8). **Q-EX-25** — the fold is SKILL.md §6's own rule and reproduces #25's 392.39 and #31's corrected −143.02 / −12.38; the recommendation should carry R-1(a), the header adjustment, or the stored header will not reconcile with the lines on a printed-net insurer document. **Q-EX-20** — clause (f) verified on the Continente PDF; R-2 adds the header half. **Q-EX-4**, **Q-EX-9**, **Q-EX-15**, **Q-EX-16**, **Q-EX-18**, **Q-EX-21** read as the round-3 reviewers asked. **Q-EX-23** — R-5 is a consequence the owner should see: the recommendation cannot reproduce the FDR's two-book practice on R3 (it is not a reason to change the ruling; it is a scoring note). No new question: R-1(c) is an `EXTRACT.md` rule, not a ruling.

#### Readiness for RED oracles

Not yet, on two goldens/fixtures: golden #2's header (`null` per §2.2, 60.55 / 13.56 per the key — R-2) and the `Split` case for #31 with its header (R-1; the line half −143.02 / −12.38 is typeable). Typeable now from the spec alone: the rest of the accept/reject corpus (#6, #10, #35, #37, #40 — #6 verified against the PDF and key), `SkillPackageTests`, `ProcessingEvidenceTests`/`WorkerStartupTests`, `ClaudeCliTimeoutTests`, `ClaudeCliPermissionTests`, `EvidenceSecretScanTests`, `CompanyMatcherTests`, `AccountPeriodTests`, `ExtractionResultIdentityTests`, `QueueWorkerGateTests` (#22/#23/#17/#29 both branches), `DocumentsReviewV2Tests`/`IngestionServiceHoldTests`, and the scoring fixtures: pairing on the shuffled Revolut key (unique natural key verified), the S-11 tie-break then occurrence order (computable; direction per R-9), `Split` on #6 and #25 and the three synthetic branches (figures recomputed above), the statement cells (40.06 → 164.94; 270.03 → 619.22, taken from round 3), S-4, S-6, S-11 thresholds, the floor rule, the 18 corrections (15 dates verified). R-1 and R-2 are each a few sentences plus one flag row; R-3 to R-5 should ride the same revision.

#### Verified myself

Worktree head `7f86af2`, clean; the spec and the review record in full. Skill build HEAD `a558523f…`, clean: schema.md line 17 (24 DOCLOG columns, parsed), 78 (32), 87 (18), 93 (11), 95 (26); SKILL.md §6 lines 156–162 (stamp and parafiscal rules) and the §1/§5/§8/§11/§12 anchors at 16/95/410/581/595. PDFs via `pdftotext -raw`/`-layout`: Tranquilidade #6 (option totals, footnote −0,03, "Total(*) 767,02", no net), #25 (the full breakdown incl. "Prémio antes de impostos 382,82", the two 0,00 rows, "Recebemos o valor de 411,54", ATCUD), #31 ("Prémio antes de impostos (1) 137,65", 12,38 / 5,37 / 17,75 / 155,40, ATCUD); Continente #2 (10 product rows, "4 X 11,99", subtotal/discount/total, the per-rate block, ATCUD); EDP #27 (four invoices + the 1,50 item, order, 36,97); Águas do Porto #21 (two invoices, order, 66,27 / 27,12). Keys (40): #6/#25/#31/#2/#21/#27 headers and lines in full; 35 fiscal, 109 lines, 6 receipts, rules R1 12 / R3 12 / blank 10 / R2 1; 11 `doclog.docDate ≠ dateDoc`; DateDue deltas and notes on all 35 (15 rows +30; the 5 + 2 notes); #29 GOTT/R/Internal/IE3559150RH, #23 ITOO; the 15 Revolut FX rows by date and `bm_code`, `bm_code` ascending with `docDate`; the BPI −2186.06 description; 0 pairing ties on all five statements; INDEX.md. `05-doclog-all.txt` lines 1583/1587/1594/1596; `sample-set.csv` (40 rows, no I26080031/I26070074). Code on main `2c8e5f2`: `CompanyMatcher` (Normalize/SameTaxId/HasCountryPrefix/Bare, private), `DocumentTypeRouter.DocumentTypeFor`, `ExtractionContract` vocabulary, `IntakeProcessingStatus` ending at `Quarantined = 8`, `ChannelIntakeService.cs` 288, `ClaudeCli.cs` 12–14/78/86, `ClaudeDocumentProcessor.cs` 83/109–111, `IngestionService.cs` 208/302–313/455/575, `QueueWorker.cs` 276/391/506–512, the unique index migration, the worker csproj (Sdk line only), `publish-release.ps1` 33.

#### Taken from the spec without verification

The pinned CLI's flag semantics, `Read` truncation, `CLAUDE.md` discovery and the `--help` texts (round-3 feasibility evidence); the Windows path-rule syntax and deny-over-allow precedence; token/context estimates and the sitting arithmetic; module deployment/rollback mechanics and `provision-production.ps1` ACLs; `git archive` CRLF; `build_bnkmov.py` 77–84; `SyncEngineWave3.ToCashDelta`; `DocumentCaptureService` C6; the statement balances and V-14 chains (40.06/164.94, 270.03/619.22 and the other three — round-3 verified, not re-read); MEO/EDP-period/Avis/VFX/Locarent/AWS/Stripe wording in §3 and §5.1; page counts; the content of `EXTRACT.md` (unwritten); the existence of the reserve entries R1, R4–R10 beyond their doclog rows; the "2 otherwise column-sliced" Revolut rows (to be found at RED).

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** 7f86af2 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main at `2c8e5f2`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 6 / Low 6

Revision 4 closes 25 of the 26 round-3 findings by text I checked against the code, the pinned CLI's `--help`, the .NET 10.0.301 SDK targets, the skill build at `a558523`, the 40 keys and the two Tranquilidade PDFs. One disposition introduces a defect: the two csproj lines prescribed for F-P-16 fail the worker build under `Microsoft.NET.Sdk.Worker`, which already includes every `**\*.json` as `Content` and runs the SDK's duplicate-`Content` check (NETSDK1022) — `skill\references\vat_rates.json` would be included twice. The remaining findings are seams a builder would otherwise pick alone.

#### Round-3 revisit

| id | State | Revision-4 text relied on; what I verified |
|---|---|---|
| C-P-1 | CLOSED | §2.2 `other_taxes_amount`; §2.6 step C; §5.5 #31 corrected to −12.38 / −143.02; Q-EX-4, Q-EX-25. Verified `pdftotext -raw` #25: "Prémio comercial (*) 382,82 / Custos de fracionamento 0,00 / Custos de gestão 0,00 / Prémio antes de impostos 382,82 / Imposto de selo 19,15 / Outros encargos e taxas 9,57 / Total outras entidades 28,72 / Total 411,54"; key lines 392.39 / 19.15, net 411.54, vat 0. #31 PDF: 137,65 / 12,38 / 5,37 / 17,75 / 155,40; key lines −137.65 / −17.75, net −155.40; the correction sums to −155.40. SKILL.md line 160 at `a558523` carries the fold rule verbatim. Residual on the printed-net branches: R-6. |
| C-P-2 | CLOSED | §2.6 branch (0) terminal, step A in every branch; trace #6 (`net null, vat null, total 767.02, duty −0.03`) → Net 767.02, lines 634.72 / 132.33 / −0.03 = key `I26030044.json`. |
| C-P-3 | CLOSED | §2.5b: 3 from the extraction + 8 derived + 13 system = 24 = schema.md line 17. |
| C-P-4 | CLOSED | §2.3 clause (f); Q-EX-20; §7 golden #2 "2 VAT-rate lines". (PDF #2 not re-read; figures match round 3's.) |
| C-P-5 | CLOSED | §2.3 "Document kind by fiscal function"; §4.2 `EXTRACT.md` row names it. |
| C-P-6 | CLOSED | §5.5 "15 + 2 + 1", the 15 dates. Verified by regex over `REV-DO-GOT-EUR_202512.json`: exactly 15 rows match `Taxa|\$|USD` on the listed dates; BPI row 2025-07-30 −2186.06 reads "… FAT-20250422904233 -2 500,00 USD". |
| C-P-7 | CLOSED | §2.4 `amount` column rule; `description_printed` excludes Revolut sub-lines; §4.2 `EXTRACT.md` row. |
| C-P-8 | CLOSED | S-2 "occurrence order = the print order of the answer (`seq`) against the `bm_code` order of the key". |
| C-P-9 | CLOSED | S-9 "under `CompanyMatcher.SameTaxId` semantics"; code `CompanyMatcher.cs` 42–50 (`Bare` + `HasCountryPrefix`). |
| C-P-10 | CLOSED | §2.3 "4 X 11,99". |
| F-P-1 | CLOSED | As C-P-1. |
| F-P-2 | CLOSED | §4.3 "`<staging dir>/evidence/<jobId>/attempt-N-{stdout.txt,result.json}` … nothing is ever overwritten"; §4.7; §7 asserts both jobs' files and the old `RawStdoutSha256`. Code today: `ClaudeDocumentProcessor.cs` 109–112, 145 (`<source dir>/evidence/attempt-{AttemptCount}-*`, `File.Copy(…, true)`) — the change is named. |
| F-P-3 | CLOSED | §4.7 key `docint:<id>:process:<contract>:<n>`; `ix_jobque_idempotency_key` `unique: true` (migration `20260827172608_IntakeAndQueue.cs` 88–92); §7 "`…:3`". |
| F-P-4 | CLOSED | §4.3: `RunAsync(prompt, jobDir, timeoutSeconds, ct)` (today 3 args, `ClaudeCli.cs` 14, 59); `ExitCode = −1` on timeout (`ClaudeCli.cs` 100 → processor 115); `JobOutcome.Hold` beside Success/Retry/Dead (`QueueWorker.cs` 506–514); per-process counter distinct from the "Claude unavailable/limited" pause (111–115). Residuals: R-2 (no job id on an evidence element), R-3 (jobque state for Hold). |
| F-P-5 | CLOSED | §4.5 deny list enumerated; `--restricted` primary; `--help` 2.1.259: "confines the file tools to the working directories (--add-dir included) … ignores user, project and local settings files (managed settings and --settings still apply)". Residual: R-4 (the probe cannot show a deny rule binds). |
| F-P-6 | CLOSED | §4.3 `ExtractionRunner(filePath, sandboxRoot, evidenceRoot, …)`; §5.7 sandbox copy, evidence under `tests/…/evidence/extract-v2/<run>/`, `git status --porcelain` empty as post-condition. Skill build clean at `a558523` today. |
| F-P-7 | CLOSED | §5.7 / Q-EX-15 `ClaudeCliPath = C:\Apps\Sibyla\tools\claude\claude.exe`; `ResolveCliExecutable` returns the configured path when it exists and is not `.cmd` (`ClaudeCli.cs` 107–109); the binary prints `2.1.259 (Claude Code)`. |
| F-P-8 | CLOSED | Q-EX-24; §5.1 #27 `total_amount` 32.23; §5.6 R2 = I26080031 `LG001801` (CONF), R3 = I26070074 `LG001795` (ITOO) — both present in `05-doclog-all.txt` lines 1587 / 1583 with archive paths. |
| F-P-9 | CLOSED | §5.7 "two tuning sittings (fiscal documents; statements) … the reserve in a third, single sitting under the frozen tuple at R-EX-3"; Q-EX-9; §5.6. |
| F-P-10 | PARTIALLY | §4.7 transition table; `SubmitReviewAsync` accepts Processed/DeadLetter and moves DeadLetter → Processed (`IngestionService.cs` 302–314); `ChannelIntakeService.cs` 288; duplicate branch `QueueWorker.cs` 311–325 (status 6 in the same transaction). Missing: the return path from `PossibleDuplicate` — `RuleOnDuplicateAsync(false)` → `ReleaseHeldProcessing` → `Processed` whenever `ResultJson` is non-null (`IngestionService.cs` 438–443), which drops a V-19 hold; §4.7 says `ReleaseHeldProcessing` is untouched — R-5. |
| F-P-11 | CLOSED | §4.5 reworded; matches `--help` for `--restricted`, `--strict-mcp-config` ("Only use MCP servers from --mcp-config"), `--disable-slash-commands` ("Disable all skills"). |
| F-P-12 | CLOSED | `--safe-mode` in §4.3/§4.5/Q-EX-21; `--help`: "all customizations (CLAUDE.md, skills, plugins, hooks, MCP servers …) disabled … Auth, model selection, built-in tools, and permissions work normally"; probe (b) in §4.5/§8. Whether `--settings` still applies under `--safe-mode` is not in the help text — folded into R-4. |
| F-P-13 | CLOSED | As C-P-6. |
| F-P-14 | CLOSED | §4.3 checks in `IHostedService.StartAsync`, 10 s cap, rethrow; §7 `WorkerStartupTests`. `Program.cs` 34–35 (`host.Run()`, no catch): an exception from `StartAsync` is unhandled and ends the process non-zero; `QueueWorker` is a `BackgroundService`, so `StartAsync` must be overridden. Health check 7 = "same pid for 20 s" (`health.v2.json` line 12). SCM behaviour not exercised; R-8 on the two 10 s caps. |
| F-P-15 | CLOSED | §7 `SkillPackageTests`: `SIBYLA_SKILL_BUILD_PATH` or `local/skill-build.path`; absent → one explicit failure. |
| F-P-16 | OPEN | §4.6 / §7 now prescribe `<Content Include="skill\**" CopyToPublishDirectory="PreserveNewest" />` + `<None Remove="skill\**" />`. Under `Microsoft.NET.Sdk.Worker` this fails the build — R-1. |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| R-1 | High | §4.6 csproj; §7 `SkillPackageTests` | The Worker SDK already declares `<Content Include="**\*.json" … CopyToPublishDirectory="PreserveNewest" />` and the SDK's duplicate check runs on `Content` when `EnableDefaultContentItems` is true (the Worker SDK sets it). `<Content Include="skill\**" />` therefore includes `skill\references\vat_rates.json` (and any `manifest.json` shipped inside the package) a second time → error NETSDK1022, and `dotnet publish` in `publish-release.ps1` fails. The `None Remove` line does not touch it. | SDK 10.0.301: `Sdks\Microsoft.NET.Sdk.Worker\targets\Microsoft.NET.Sdk.Worker.targets` 15 (`EnableDefaultContentItems = true`); `…Worker.props` 25 (`**\*.json` as Content), 35 (`None Remove="**\*.json;**\*.config"`); `Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.DefaultItems.Shared.targets` 212–222 (`CheckForDuplicateItems Items="@(Content)" DefaultItemsOfThisTypeEnabled="$(EnableDefaultContentItems)"`); §4.2 table (`references/vat_rates.json` in the package). | `<Content Include="skill\**" Exclude="skill\**\*.json" CopyToPublishDirectory="PreserveNewest" />` + `<None Remove="skill\**" />` — the `.json` files already publish through the SDK default. `SkillPackageTests` asserts the published tree (every manifest file under `<publish>\skill\`) rather than the csproj text, or both. |
| R-2 | Medium | §4.3 Timeouts; §7 `ClaudeCliTimeoutTests` | "an element for this job with `ExitCode = −1`" — `ProcessingEvidence` carries `Attempt` and no job id; after a re-extract, elements of two jobs share `docint.evidence_json`, so a prior job's timeout would select 1,350 s for a fresh job unless the builder parses the job id out of `RawStdoutPath`. The fixture cannot name "for the job" without a field. | `ProcessingEvidence.cs` 17–35; `QueueWorker.cs` 428–433 (elements appended per attempt, never keyed by job); §4.3 list of new evidence fields (no `JobId`). | Add `JobId` to `ProcessingEvidence` (in the §4.3 field list) and read prior timeouts as `JobId == job.Id && ExitCode == −1`; the test seeds two jobs' elements. |
| R-3 | Medium | §4.3 / §4.7 `JobOutcome.Hold`; §7 `ClaudeCliTimeoutTests` | `CompleteAsync` writes a `jobque.state` per outcome (2 Succeeded / 3 DeadLetter; 0 or 1 is re-claimed after the lease expires). The state for a Hold is unnamed, so the test's expected row on `jobque` cannot be typed, and a wrong choice re-runs a held job. | `QueueJob.cs` 5–8 (`Queued 0, Succeeded 2, DeadLetter 3`); `QueueWorker.cs` 376–424; `ClaimAsync` 197–198, 206–207 (state 0/1 re-claimed). | State the row: `state = 2`, `last_error` = the hold reason, audit action `job.hold`; the test asserts it. |
| R-4 | Medium | §4.5 R-EX-2 probe (a); §8 | The sibling `<release>\appsettings.json` lies outside every working directory, so `--restricted` refuses it whatever the `Read(...)` deny syntax does — the probe cannot show that the `--settings` deny rules bind (the spec's own "a rule that does not bind is a rule that does not exist"). Whether `--settings` still applies under `--safe-mode` is unstated in `--help` ("Admin-managed (policy) settings still apply"). | `claude.exe --help` 2.1.259 (`--restricted` "confines the file tools to the working directories (--add-dir included)"; `--safe-mode` text). | Probe (a′): plant `<sandbox>\deny-probe.txt` inside the sandbox, deny it by rule in `permissions.json`, run under the full flag set (with `--safe-mode`): `document.pdf` reads, the probe file is refused, the trace records both. Keep probe (a) for the confinement. |
| R-5 | Medium | §4.7 transitions | Return path missing: a row held (V-19, `result_json` kept) then marked `PossibleDuplicate` by the gate, ruled "not a duplicate" by a person, goes through `ReleaseHeldProcessing`, which sets `Processed` whenever `ResultJson` is non-null — the hold is lost and a truncated statement shows as processed. §4.7 says `ReleaseHeldProcessing` is untouched. | `IngestionService.cs` 438–443, 474–486 (`RuleOnDuplicateAsync`); §4.7 row "`HeldForPerson` → duplicate → `PossibleDuplicate`, the hold reason kept in the audit detail". | Add the row "`PossibleDuplicate` carrying a hold → not-duplicate ruling → `HeldForPerson`"; keep the hold reason on the row (`last_error`) so `ReleaseHeldProcessing` can restore status 9; drop "untouched". |
| R-6 | Medium | §2.6 Split branches (1)–(3); §5.2 `netVatNotPrinted`; §7 Split fixtures | Branch (1) tests `net + vat + duty = total` and ignores `other`. Both Tranquilidade documents print a net ("Prémio antes de impostos" 382,82 on #25, 137,65 on #31), so an honest `net_amount` is possible; then #31 reconciles only as `137.65 + 0 + 12.38 + 5.37 = 155.40`, falls to branch (3) (no header split, `reconciliation_note` required, V-12), and header Net 137.65 misses the key's −155.40 — #31's net cell is scored (`netVatNotPrinted` names #6 and #25 only). Step C also leaves the header untouched in (1)/(2), so `Σ lines ≠ Net` after the fold. The §7 fixture "#31 against its corrected key" states no inputs. §5.2's justification for #25 ("prints a gross and no net/VAT split") is inexact. | `pdftotext -raw` #25 lines 42–48, #31 lines 32–40; keys `I25040004.json` (net −155.40), `I26080026.json`; §2.6 text; §5.2. | Branches (1)/(2) test with `duty + (other ?? 0)` and set `Net += duty + other`; state the #31 fixture inputs (`net 137.65 or null, vat null, total 155.40, duty 12.38, other 5.37` → Net 155.40, lines 143.02 / 12.38, signed by kind); set `netVatNotPrinted` for all three Tranquilidade rows or for none. |
| R-7 | Medium | §7 `QueueWorkerGateTests` "through the worker path" | `AssignCompanyFromResultAsync` and `CompleteAsync` are private in `QueueWorker`; no test constructs `QueueWorker` today; `ProcessingEvidenceTests` drives only the processor. §7 names no seam, so the builder chooses between running the `BackgroundService` loop against the test database and extracting the gate. | `QueueWorker.cs` 268, 340 (`private`); `grep "QueueWorker(" tests` → none; `ProcessingEvidenceTests.cs` 114–120. | Name the seam: an internal `CompanyGate.ApplyAsync(conn, tx, ownerId, intakeId, resultJson)` with `InternalsVisibleTo("Sibyla.Tests.Platform")`, called from `CompleteAsync`; the test drives it with `ExtractionResult.ReadIdentities` on canned v1/v2 answers. |
| R-8 | Low | §4.3 start-up checks | `claude --version` prints `2.1.259 (Claude Code)`; `CliVersion` "e.g. `2.1.259`" — the comparison (first token) is unstated. Two checks "each capped at 10 s" can sum to 20 s, the whole of health check 7's window. | Binary output; `health.v2.json` line 12. | Compare the first whitespace-separated token; one shared 10 s budget. |
| R-9 | Low | §4.3 Timeouts | The attempt table is prose: whether attempt 3 runs at 1,350 s and holds only if it times out, or the hold is immediate after two timeouts, and whether a non-timeout failure between two timeouts resets the count. | §4.3 sentence "'a second timeout' … selects 1,350; a third timeout returns Hold". | A three-row table: attempt / prior timeouts for this job / `timeoutSeconds` / outcome on timeout. |
| R-10 | Low | §4.7 last transition row; Q-EX-16 note | "the channel-intake API reads the row → candidate `completed`": the candidate's status is set once at upload (`outcome.Intake`) and never re-read from `docint`; the hold is invisible to the API by construction. | `ChannelIntakeService.cs` 285–291. | Reword: "candidate status is fixed at upload as `completed`; a later hold does not reach the API". |
| R-11 | Low | §4.2 build rule (1) counts | Under the 1,900-character threshold: SKILL.md 16–409 has 4 long lines (89, 164, 228, 408), the five selected schema.md lines are all long (17, 78, 87, 93, 95: 3,206 / 3,368 / 8,014 / 3,326 / 5,681), Financial Document Entry Policy 1 (line 53). The spec says "SKILL.md §1–7 three". | `awk 'length > 1900'` over the files at `a558523`. | 4 / 5 / 1. |
| R-12 | Low | §7 `ClaudeCliPermissionTests`; §5.7 | "the argument list is exactly the §4.3 flag set" — the order is not stated. Bank keys carry the file hash under `statementDoclog.fileHash` (fiscal keys at top level `fileHash`); §5.7's "verifies each file's SHA-256 against its key" should name both. All 40 hashes match the files on disk. | §4.3 prose; keys' structure; my hash check. | State the argument order (or assert set-equality with `-p` first); name `statementDoclog.fileHash`. |

#### Owner questions Q-EX-0..25

All 26 recommendations are buildable as written, with these notes. **Q-EX-15**: the pinned binary answers `2.1.259 (Claude Code)` and `ResolveCliExecutable` keeps an explicit existing path — verified. **Q-EX-16**: the intake API's candidate status is fixed at upload (R-10), so "with the hold visible on the Uploads page only" is right for a stronger reason than stated. **Q-EX-18**: the wording now holds once evidence is per job; R-2 (job id on the element) is what makes "its element of `evidence_json`" identifiable. **Q-EX-21**: every flag verified on 2.1.259; the deny list's binding needs probe (a′) (R-4). **Q-EX-24**: both replacement reserve rows exist in the doclog listing with archive paths. **Q-EX-25**: the recommendation is buildable; either ruling meets R-6 — under the alternative (exclude by flag) #31's header-net and line cells must be excluded as well as #25's, since #31 prints the same shape and its net cell is scored.

#### Readiness for RED oracles

Not yet, on one blocker and three seams. **Blocker:** the first commit of the package with the §4.6 csproj lines fails `dotnet build` of the worker (R-1) — one `Exclude` fixes it. **Seams without an expected call:** `ClaudeCliTimeoutTests` (R-2 job id on the evidence element; R-3 jobque state of a Hold), `QueueWorkerGateTests` "through the worker path" (R-7). **Typeable now:** `ExtractionContractV2Tests` (accept/reject corpus — V-7/V-8/V-9 hold on the keys per round 3), `SkillPackageTests` (path source, heading anchors, ≤ 1,900 rule, CRLF/LF — `git -c core.autocrlf=false archive` yields LF and plain `git archive` CRLF on this machine, verified), `ProcessingEvidenceTests`/`WorkerStartupTests`, `ClaudeCliPermissionTests` (order to be fixed, R-12), `EvidenceSecretScanTests`, `CompanyMatcherTests`, `AccountPeriodTests`, `ExtractionResultIdentityTests`, `DocumentsReviewV2Tests` (`Sibyla.Tests.Browser` prerenders pages through `WebApplicationFactory` against the local database — a seeded v2 row fits), `IngestionServiceHoldTests` (plus R-5's row), and every `ExtractionScoringTests` fixture — pairing (0 collisions on `(movDate, amount, currency)`, 46/10/3/87/51), the 15 + 2 + 1 corrections, statement cells, S-4/S-6/S-11, the floor rule, Split #6 and #25 as traced; Split #31 once R-6 states its inputs.

#### Verified myself

- Worktree head `7f86af2`, clean; the spec and the review record in full. Main `2c8e5f2`: `WorkerOptions.cs` (240/300/2/10, `ClaudeCliPath = "claude"`, no version/model/effort), `ClaudeCli.cs` (3-arg `RunAsync`, `--allowedTools Read` only, timeout → `(-1, stdout, "…timed out…")`, 20 s `--version` cap swallowing failures, npm fallback), `ClaudeDocumentProcessor.cs` (evidence beside the source, `attempt-N` names, overwrite copies, `ExitCode` into the element), `ClaimedJob.cs` (no `last_error`), `QueueWorker.cs` (claim RETURNING, renewal `max(10, Lease/3)`, lane pause on substring, private gate reading top-level `recipient_tax_id` against `commst`, duplicate → status 6 in-transaction, `CompleteAsync` overwriting `result_json` and appending `evidence_json`, `JobOutcomeKind` Success/Retry/Dead), `Program.cs` (`AddWindowsService`, `host.Run()` uncaught), `QueueJob.cs` (`MaxAttempts = 5`, states 0/2/3), `DocumentIntake.cs` (enum 0–8, `ResultJson`/`EvidenceJson`/`CorrectedJson`), `IngestionService.cs` (`SubmitReviewAsync` 293–330, `ReleaseHeldProcessing` 438–458, `RuleOnDuplicateAsync`, keys `docint:<id>:process`/`:transfer`, literal `apollo.extract.v0`), `ChannelIntakeService.cs` 285–291, `ProcessingEvidence.cs`, `CompanyMatcher.cs`, `ExtractionContract.cs` (v1, 11 fields), `Documents.razor` (flat v1 form 310–330, `StatusLabel` default arm, form only on `Processed`), migration `IntakeAndQueue.cs` 88–92 (unique index; no CHECK on `processing_status` anywhere in Platform migrations), worker csproj (Sdk.Worker, no item groups), `publish-release.ps1` (`dotnet publish` only), `provision-production.ps1` 158–177 (Set-ExactAcl; worker ReadAndExecute on `worker` and `tools`), `test.ps1` (`dotnet test Sibyla.slnx`, no filter), `Sibyla.slnx` (six test projects; no `tools/` folder yet), test projects (Platform and Argus reference the worker; Browser references Web; `ProcessingEvidenceTests` DB-backed with `CannedCli`; no test constructs `QueueWorker`; Api's `golden\*.json` via `None Update`), `Invoke-SibylaDeployment` cmdlet (`-Execute` switch, `-Resume` string; `Sibyla.Deployment.psd1` exports it), `health.v2.json` check 7, run record §7o and §8 (bare-id pointers, `-Execute -Resume`, `rollback-release.ps1 -ToPath`).
- .NET SDK 10.0.301 targets: `Microsoft.NET.Sdk.Worker.props` 22–35, `Microsoft.NET.Sdk.Worker.targets` 15, `Microsoft.NET.Sdk.DefaultItems.Shared.targets` 179–222, `Microsoft.NET.Publish.targets` 838–931 (`ContentWithTargetPath` with `CopyToPublishDirectory` is what publish copies); the current production worker folder holds `appsettings.json` and no `skill\`.
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: `--version` = `2.1.259 (Claude Code)`; `--help` text for `-p`, `--output-format`, `--add-dir`, `--tools`, `--restricted`, `--safe-mode`, `--strict-mcp-config`, `--disable-slash-commands`, `--permission-prompts`, `--settings`, `--no-session-persistence`, `--model`, `--effort`, `--fallback-model`, `--bare`.
- Skill build: HEAD `a558523`, clean; headings §1 line 16, §5 95, §6 156, §7 162, §8 410, §11 581, §12 595; SKILL.md §6 text (stamp and parafiscal rules); long-line counts under 1,900; `core.autocrlf = true`, `git archive` CRLF vs `-c core.autocrlf=false` LF; all 35 fiscal PDFs and 5 statement PDFs present at their key archive paths with matching SHA-256 (40/40).
- Keys: #6/#25/#31 header and line figures and flag texts; 0 pairing collisions on the five statements; 15 Revolut FX rows by date; the BPI fragment; six `company.{code,name,taxId}`; bank keys' `statementDoclog`/`bankAccount` shape. PDFs via `pdftotext -raw`/`-layout`: #25 and #31 amounts blocks. Reserve doclog codes LG000015, LG000795, LG001795, LG001801 in `05-doclog-all.txt`.

#### Taken from the spec without verification

- CLI runtime behaviour not exercised: the `Read` 2,000-character truncation, the `json` envelope's `num_turns`/`usage`/`modelUsage`, the `stream-json` file-read trace, deny-over-allow precedence and the Windows `Read(...)` rule syntax, whether `--settings` applies under `--safe-mode`, the usage-limit message the bench would parse.
- The SCM's treatment of a process that dies after `OnStart` returned (unhandled exception from `Host.Run()`), and `sc failure` actions on the production service; the token/context estimates and the sitting arithmetic of §4.2/§5.7.
- Content of `EXTRACT.md` (unwritten); PDFs #2, #6 and the statements (round-3 evidence, not re-read); MEO/EDP/Avis/VFX wording cited in §3/§5.1; page counts; that the release gate's clean-export fingerprint binds a committed package.

### Author's dispositions — revision 5

Revision 5 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 4. Ids: `C-R-n` = the contract reviewer's R-1…R-11, `F-R-n` = the feasibility reviewer's R-1…R-12. Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 5. Decisions the coordinator took for this revision are written in and not re-opened; no new owner question.

**Contract reviewer (C-R-1 … C-R-11)**

| id | Sev | Disposition | Where in revision 5 |
|---|---|---|---|
| C-R-1 | High | fixed — (a) branches (1)/(2) test with `extra = duty + (other ?? 0)` and set `Net += extra` ((2) also `VAT −= duty`), so the header equals Σ lines after step C in every branch; (b) #25 and #31 traced both ways (printed net → branch (1): 382.82 + 0 + 19.15 + 9.57 = 411.54 → 392.39 / 19.15; 137.65 + 0 + 12.38 + 5.37 = 155.40 → −155.40, −143.02 / −12.38; net null → branch (0), same results); (c) `EXTRACT.md` rule: an insurer's "Prémio antes de impostos" IS `net_amount`; the `netVatNotPrinted` flag dropped entirely — net/VAT scored on #6/#25/#31 after the identical transform; Q-EX-25's recommendation carries the header adjustment | §2.2 `net_amount`; §2.6 Split; §4.2 `EXTRACT.md` row; §5.2; §5.3 S-9; §1.2 Q-EX-25; §7 |
| C-R-2 | High | fixed — clause (f) extended to the header: on a document printing a per-rate VAT summary and no single net/VAT figure, `net_amount`/`vat_amount` are the summary's column sums (60.55 / 13.56); `EXTRACT.md` and golden #2 typed to that | §2.2; §2.3 clause (f); §4.2; §7 golden row |
| C-R-3 | Medium | fixed — clause (g): an insurer's premium/charges breakdown block is not a line table; the item line is the premium, the zero rows and the "Total outras entidades" subtotal are not lines; #25/#31 typed to one item line + the stamp line; clause (d) does not apply inside such a block | §2.3 clause (g); §2.6 traces; §7 |
| C-R-4 | Medium | fixed — branch (3) records a DPRCHK finding and never fails the document; V-12 applies to the raw answer only, before `Split` | §2.6 branch (3); §7 fixture (3) |
| C-R-5 | Medium | fixed — S-9: the flow selecting `fiscal_no` is the key's flow; under Q-EX-23's recommendation R3's `company`/`origin_class` cells are reported, not scored (the FDR books both legs, one per book; Apollo answers one); noted under Q-EX-23 as a consequence the owner sees | §5.3 S-9; §1.2 Q-EX-23 |
| C-R-6 | Low | fixed — step C is `item.Net += other`; `Total − duty` is only the check (with one item line and no VAT) | §2.6 step C |
| C-R-7 | Low | fixed — the SKILL.md §6 attribution of branch (2) dropped; (2) deliberately keeps the remainder as VAT | §2.6 branch (2) |
| C-R-8 | Low | fixed — Q-EX-24 "two or more invoices" (#27 prints four plus a 1.50 item); `evidence.notes` names the further invoices | §1.2 Q-EX-24; §4.2 |
| C-R-9 | Low | fixed — occurrence order compares the answer's print order against the key's print order: `bm_code` as-is for `oldest_first`, reversed for `newest_first`, by section for `sectioned` | §5.3 S-2 |
| C-R-10 | Low | fixed — S-4 "the rows flagged `false` plus the receipts" (the notes seed 5 + 2 of the flags) | §5.3 S-4 |
| C-R-11 | Low | fixed — `CompanyMatcher.SameTaxId` made internal under `InternalsVisibleTo("Sibyla.Tests.Platform")` (and the bench), one implementation for gate, scorer and tests | §4.3 gate seam; §5.3 S-9; §7 |

**Feasibility reviewer (F-R-1 … F-R-12)**

| id | Sev | Disposition | Where in revision 5 |
|---|---|---|---|
| F-R-1 | High | fixed — `<Content Include="skill\**" Exclude="skill\**\*.json" CopyToPublishDirectory="PreserveNewest" />` + `<None Remove="skill\**" />`; the `.json` files publish through the Worker SDK's default `**\*.json` Content item (`Microsoft.NET.Sdk.Worker.props` line 25) and the `Exclude` avoids the NETSDK1022 duplicate-`Content` check; `SkillPackageTests` asserts the published tree (every manifest file under `<publish>\skill\`), not the csproj text | §4.6; §7 `SkillPackageTests` |
| F-R-2 | Medium | fixed — `JobId` added to `ProcessingEvidence` (§4.3 field list); prior timeouts read as `JobId == job.Id && ExitCode == −1`; the test seeds two jobs' elements | §4.3 evidence and timeouts; §7 timeouts row |
| F-R-3 | Medium | fixed — a Hold writes `jobque.state = 2`, `last_error` = the hold reason, audit action `job.hold`; the test asserts the row | §4.3 timeouts; §7 |
| F-R-4 | Medium | fixed — probe (a′): a file planted inside the sandbox and denied by rule must be refused while `document.pdf` reads, under the full flag set with `--safe-mode`; probe (a) kept for confinement; R-EX-2 records whether `--settings` applies under `--safe-mode` (if not, `--safe-mode` leaves the set by amendment and probe (b) stands on `--restricted` alone) | §4.5; §8 R-EX-2; §1.2 Q-EX-21; §7 permissions row |
| F-R-5 | Medium | fixed — transition row "`PossibleDuplicate` carrying a hold → not-duplicate ruling → `HeldForPerson`"; the hold reason kept on the row in the new nullable column `docint.hold_reason` so `ReleaseHeldProcessing` restores status 9; "`ReleaseHeldProcessing` untouched" dropped | §4.7 transitions; §7 holds row |
| F-R-6 | Medium | fixed — as C-R-1: `extra = duty + (other ?? 0)` in branches (1)/(2) with the header adjusted; the #31 fixture inputs stated both ways (net 137.65 / null, vat null, total 155.40, duty 12.38, other 5.37 → Net −155.40, lines −143.02 / −12.38); `netVatNotPrinted` dropped for all three Tranquilidade rows; §5.2's "#25 prints no net" justification removed | §2.6; §5.2; §7 |
| F-R-7 | Medium | fixed — the seam: internal `CompanyGate.ApplyAsync(conn, tx, ownerId, intakeId, resultJson)` with `InternalsVisibleTo("Sibyla.Tests.Platform")`, called from `CompleteAsync` for Success and Hold; `QueueWorkerGateTests` drives it with `ExtractionResult.ReadIdentities` on canned v1/v2 answers | §4.3 gate seam; §7 gate row |
| F-R-8 | Low | fixed — version compare on the first whitespace-separated token (`2.1.259 (Claude Code)`); one shared 10 s budget for both start-up checks | §4.3 start-up; §7 |
| F-R-9 | Low | fixed — the three-column attempt table (attempt / prior timeouts / `timeoutSeconds` / outcome): 1 / 0 / 900 / Retry; 2 / 1 / 1,350 / Hold (no third run); a non-timeout failure between does not reset the count | §4.3 timeouts; §7 |
| F-R-10 | Low | fixed — the API row reworded: the candidate status is fixed at upload as `completed`; a later hold does not reach the API by construction; Q-EX-16 note likewise | §4.7 transitions; §1.2 Q-EX-16 |
| F-R-11 | Low | fixed — long-line counts 4 (SKILL.md 89, 164, 228, 408) / 5 (schema.md 17, 78, 87, 93, 95) / 1 (Financial Document Entry Policy 53) | §4.2 build rule (1) |
| F-R-12 | Low | fixed — the argument order stated (`-p` first, then the flag set in §4.3 order) and asserted; `statementDoclog.fileHash` named beside the fiscal keys' top-level `fileHash` | §4.3; §5.7; §7 permissions row |

Owner questions after revision 5: Q-EX-0…25 unchanged in number; Q-EX-16, Q-EX-21, Q-EX-23, Q-EX-24 and Q-EX-25 reworded as above. No new question.

## Round 5 — revision 5, head `fef45af` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-4 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 1 / 6 / 3 | 22 of 23 (C-R-9 partially) |
| feasibility | REVISE | 0 / 1 / 4 / 5 | 23 of 23 (the §4.6 csproj lines built and published in a throwaway Worker-SDK project) |

Contract T-1: the scorer has no rule for a `null` extraction amount against a numeric key (unprinted VAT; gross-only net without duty). Feasibility T-1: the fixed prompt names `document.pdf` while the intake accepts PNG/JPEG/TIFF and the sandbox copy keeps the source extension. Ids are per reviewer (C-T-n, F-T-n).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** fef45af (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main `2c8e5f2`; skill build HEAD `a558523`, clean)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 6 / Low 3

Revision 5 closes 22 of the 23 round-4 findings by text I checked against the four PDFs, the 40 keys, the skill build and the code; C-R-9 is partially closed (the adopted direction is wrong for the only case it serves). `Split` now reproduces #6, #25 and #31 on net, total and lines both ways — I traced every figure. The High is one level up from `Split`: the scorer has no rule for a `null` extraction amount against a numeric key, and the function is declared the identity when no duty is printed, so honest `null`s on gross-only or no-VAT documents (Lari, eSIMGo, VFX; eight zero-VAT rows) miss by design — `vat_amount` alone falls under the 85 % floor.

#### Round-4 revisit

| id | State | Revision-5 text relied on and what I verified |
|---|---|---|
| C-R-1 | CLOSED | §2.6 step B `extra = duty + (other ?? 0)`, (1)/(2) `Net += extra`; step C `item.Net += other`; §2.2 "Prémio antes de impostos is the printed net"; §4.2 `EXTRACT.md` row; §5.2 flag dropped; Q-EX-25. Traced from the PDFs: #25 (`pdftotext -raw` lines 42–48: 382,82 / 0,00 / 0,00 / 382,82 / 19,15 / 9,57 / 28,72; line 33 Total 411,54): net 382.82 → (1) 382.82+0+19.15+9.57 = 411.54 ✓ → Net 411.54 → C: 392.39; lines 392.39 / 19.15 = key `I26080026.json`; net null → (0) 411.54 − 0, same. #31 (lines 32–35, 40: 137,65 / 12,38 / 5,37 / 17,75 / Total 155,40): (1) 137.65+0+12.38+5.37 = 155.40 ✓ → C: 143.02 → signed −155.40 / −143.02 / −12.38 = the §5.5 correction; net null → (0) same. Header = Σ lines both ways. The header VAT after the transform is `null` on all three against key 0.00 — new T-1. |
| C-R-2 | CLOSED | §2.2 `net_amount`/`vat_amount` rows (60.55 / 13.56), §2.3 clause (f) "the header follows the summary", §7 golden #2. PDF #2 lines 46–47: "(B) 13,00% 3,62 0,47 4,09 / (C) 23,00% 56,93 13,09 70,02"; sums 60.55 / 13.56 / 74.11 = key; V-8/V-9 hold. |
| C-R-3 | CLOSED | §2.3 clause (g); #25/#31 "one item line plus the stamp line"; (d) excluded inside the block. PDF #25 lines 43–44 are the two 0,00 rows, 48 the subtotal; key `lineCount = 2` on both. |
| C-R-4 | CLOSED | §2.6 "(3) … never fails the document (V-12 applies to the raw answer only, before `Split`), and the stamp line of step A is still appended"; §7 fixture (3) "a finding and no failure". |
| C-R-5 | CLOSED | S-9 "`fiscal_no` … by the key's flow"; R3's `company`/`origin_class` "reported, not scored"; Q-EX-23 consequence paragraph. `05-doclog-all.txt` 1583: `LG001795\|ITOO\|I26070074 … Itoorer/Payables/202607/Itoorer_Invoice_Gott_202607_01.pdf` — the payable leg on Itoorer's book, as stated. |
| C-R-6 | CLOSED | §2.6 step C `item.Net += other`; "`Total − duty`" only as the check with one item line and no VAT (392.39 = 411.54 − 19.15; 143.02 = 155.40 − 12.38). |
| C-R-7 | CLOSED | §2.6 (2) "(the remainder is deliberately kept as VAT)"; SKILL.md §6 attribution gone (line 160 at `a558523` re-read: `NetAmount += old VATAmount`). |
| C-R-8 | CLOSED | Q-EX-24 "two or more invoices … #27 EDP: four … plus a 1.50 item"; §4.2 `EXTRACT.md` "further invoices in `evidence.notes`". EDP figures not re-read. |
| C-R-9 | PARTIALLY | S-2 "reversed where `printOrder = newest_first`". Verified on `REV-DO-GOT-EUR_202512.json` + PDF: `bm_code` ascends with `docDate` across days, but **within a day it follows print order** — 15 Dec: A VER NAV (raw line 37) then Isadora (40) = BM001932 → BM001933; 19 Aug: Stripe (120) → Gott (126) → Factor Matriz (127) = BM001910 → 1911 → 1912; 10 Mar and 31 Jan likewise. Occurrence order only breaks same-day ties, where reversing pairs A with B. T-4. |
| C-R-10 | CLOSED | S-4 "scores only the rows flagged `false` plus the receipts (the FDR's notes seed 5 + 2 of those flags; the hand check … sets the rest)". |
| C-R-11 | CLOSED | §4.3 "`CompanyMatcher.SameTaxId` becomes internal under the same `InternalsVisibleTo`"; S-9. `CompanyMatcher.cs` line 42 is `private static bool SameTaxId` today; `Bare`/`HasCountryPrefix` (48–50) give "500 940 231" ↔ "PT500940231". |
| F-R-1 | CLOSED | §4.6 `<Content Include="skill\**" Exclude="skill\**\*.json" CopyToPublishDirectory="PreserveNewest" />` + `<None Remove="skill\**" />`, `Microsoft.NET.Sdk.Worker.props` line 25 named; §7 `SkillPackageTests` on the published tree. SDK facts taken from the round-4 record (not rebuilt). |
| F-R-2 | CLOSED | §4.3 `ProcessingEvidence` gains `JobId`; prior timeouts = "`JobId == job.Id && ExitCode == −1`"; §7 timeouts row "seeds elements of two jobs". |
| F-R-3 | CLOSED | §4.3 "`jobque.state = 2` …, `jobque.last_error` = the hold reason, audit action `job.hold`"; §7 "the test asserts the row". |
| F-R-4 | CLOSED | §4.5 probe (a′) (planted `<sandbox>\deny-probe.txt`, `document.pdf` reads); §8 R-EX-2; Q-EX-21; the `--settings`-under-`--safe-mode` record with the amendment path. |
| F-R-5 | CLOSED | §4.7 row "`PossibleDuplicate` carrying a `hold_reason` → `RuleOnDuplicateAsync(false)` → **`HeldForPerson`**"; `docint.hold_reason` column; "`ReleaseHeldProcessing` changes". |
| F-R-6 | CLOSED | As C-R-1; §7 #31 inputs stated both ways; §5.2 no `netVatNotPrinted`, the "#25 prints no net" justification gone. |
| F-R-7 | CLOSED | §4.3 "`CompanyGate.ApplyAsync(conn, tx, ownerId, intakeId, resultJson)` (`InternalsVisibleTo("Sibyla.Tests.Platform")`), called from `CompleteAsync` for Success and Hold"; §7 gate row drives it. |
| F-R-8 | CLOSED | §4.3 "first whitespace-separated token", "one shared 10 s budget". |
| F-R-9 | CLOSED | §4.3 attempt table 1/0/900/Retry, 2/1/1,350/Hold; "a non-timeout failure … does not reset the count". |
| F-R-10 | CLOSED | §4.7 last row and Q-EX-16: "fixed at upload as `completed` … never re-read from `docint`". |
| F-R-11 | CLOSED | §4.2 "four such lines — 89, 164, 228, 408 —, … 17, 78, 87, 93, 95: 3,206 / 3,368 / 8,014 / 3,326 / 5,681 …, and … one, line 53". Recomputed with `awk length>1900` at `a558523`: exactly those. |
| F-R-12 | CLOSED | §4.3 "in this order: `-p` …"; §7 "in the §4.3 order, `-p` first"; §5.7 "`statementDoclog.fileHash` on a bank key". All five bank keys carry `statementDoclog.fileHash` and no top-level `fileHash` (python over the keys). |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| T-1 | High | §2.6 `Split` opening clause; §5.3 S-3, S-9; §2.2 `net_amount`/`vat_amount` | Two gaps make honest `null`s miss numeric keys by design. (a) `vat_amount`: the contract returns `null` when no VAT figure is printed; every key holds a number; S-3 compares `sign × |extraction|` and no rule says what `null` compares to. At least 8 of the 17 zero-VAT rows print no VAT figure at all (#6, #25 "Isento de IVA" text only, #31 likewise, Viajando, eSIMGo, VFX, Alibaba, Lari; Awin is scanned, unverifiable) — after `Split`, #6/#25/#31 still carry VAT `null` (branch (0) never assigns VAT, (1) leaves it "unchanged"), so S-9's "whose headers the function reproduces" is only true if `null ≡ 0.00`, which is nowhere written. Under the literal reading `vat_amount` scores ≤ 27/35 = 77 % < the 85 % floor (35 cells ≥ 20) → the header gate is blocked by design. (b) `net_amount`: "both `null` → the function is the identity" precedes branch (0), so a gross-only document **without** stamp duty keeps `net_amount = null` against a key net = total: Lari ("Valor da Comissão em Dólares: $ 372,30", the only figure; key net 372.30), eSIMGo ("Total 3 $79.1100"; key 79.11), VFX ("Valor total R$ 2.031,78 / Valor original 1.990,00"; key 2031.78). | PDFs via `pdftotext -raw`: #6 (no net/VAT line), #25 line 35, #31 line 41, Lari line 11, eSIMGo lines 9/19, VFX lines 12–15; python over the keys: 17 rows with `vatAmount = 0`, none with a null header amount; §2.6 wording "(both `null` → the function is the identity)"; S-3 text. | State the comparison: after `Split`, `VAT := VAT ?? 0` and, when `net_amount` is `null` and a total is printed, `Net := Total − VAT` **irrespective of `duty`/`other`** (make branch (0) run before the identity clause); or, in S-3, "a `null` amount compares equal to a key `0.00`; a `null` net on a gross-only document compares to the key after `Net := Total − (VAT ?? 0)`". One sentence either way; the §7 #6 fixture should then assert VAT 0.00. |
| T-2 | Medium | §2.3 `vat_rate`; §2.6 step A; Q-EX-6 | All 8 scored `vat_rate` cells sit on lines that print no per-line rate: Avis R26080004 (6 lines, key 0.00 each) prints "VAT @ 0% 0,00 12.676,92" only in its VAT SUMMARY, the line rows carry an "EUS" code; #25 (2 lines, key 0.00) prints "Isento de IVA (N.º 28 do Art.º 9º CIVA)" once at document level, and line 2 is the **server-appended** stamp line, for which step A sets `Net` and `VAT` but no `vat_rate`. Under "`null` when the line prints no rate" all 8 miss (≈ 2.4 % of the lines budget); the appended line's rate is undefined under any reading. | `pdftotext -layout` Avis lines 21–26, 39–40; PDF #25 line 35; keys `R26080004.json`, `I26080026.json` (`vatRate: 0.0`); §2.3 row; §2.6 step A. | Add to §2.3: a single document-level rate or exemption ("VAT @ 0%", "Isento de IVA", reverse charge) is the `vat_rate` of every line; step A: the appended stamp line takes `vat_rate = 0`. Note under Q-EX-6 that the 8 cells rest on that rule. |
| T-3 | Medium | §5.3 S-6 vs §3 #1 | S-6 says `account_period` = "the ladder applied to the extraction (R1 from `service_period` when present, else R3)" on R1/R3 rows; §3 #1 says "under the key's own rule stamp … MEO … reported, not penalised". `AccountPeriodService.Decide` (main, lines 34–37) returns R1 whenever a stated period exists, so on two R3-stamped rows an honest read misses by design: MEO #18 prints "PERÍODO CONTAGEM: 01/06/2026 A 30/06/2026" → R1 202606 vs key R3 202607; Regus #17 prints per-line "1 Aug 2026 – 31 Aug 2026" → R1 202608 vs key R3 202606 (DateDoc 2026-07-01). The R1 rows are fine: `advance = Start > DateDoc` reproduces all 12 (Hydra 20-02…19-03 with DateDoc 12-03 → last month 202603; #6 → first 202605). | `AccountPeriodService.cs` 26–39; keys (12 R1 / 12 R3 listed by python); MEO PDF line 8; Regus PDF line 44. | S-6: "the **key's** rule applied to the extraction's inputs — R1 rows: `Decide` from `service_period`; R3 rows: the day-of-month rule from `date_doc`; a printed period on an R3 row is reported (S-8)". |
| T-4 | Medium | §5.3 S-2 occurrence order; §7 pairing fixture | Occurrence order matters only among equal `(date, amount, currency)`, i.e. same-day rows; on the Revolut key `bm_code` within a day is in **print** order, not reversed (four days checked). "Reversed where `newest_first`" therefore pairs a same-day tie the wrong way round, and the §7 fixture "same-day same-amount pair … then by occurrence order" typed to that rule encodes the inversion. No ties on the 40; the reserve statements are `oldest_first`/`sectioned`. | Key `REV-DO-GOT-EUR_202512.json` vs `pdftotext -raw` lines 37/40, 120/126/127, 218/221/222, 241/247. | "Occurrence order = the answer's `seq` among the tied rows against the key's `bm_code` ascending among the tied rows, in every `printOrder`" (the FDR assigned `bm_code` top-to-bottom within a day). |
| T-5 | Medium | §2.6 "The check, in every branch"; §7 fixture (3) | Fixture (3) (net 100, vat 23, duty 4, total 130) asserts "no header adjustment, stamp line still appended" **and** "header = Σ lines asserted in every case": after the append Σ lines = 104 ≠ header 100 — the two expectations contradict by construction; §2.6's "in every branch" has the same defect. | §7 scoring row text; §2.6 step C last sentence. | "header = Σ lines in branches (0)–(2); in (3) the difference is the DPRCHK finding" in both places. |
| T-6 | Medium | §2.3 doctrine (a)–(g); §5.1 #12 | VFX #12: the key has 2 lines — 1,990.00 (service) and 41.78 = "R$ 39,80 de multa + R$ 1,98 de juros" merged; the document prints one description and two penalty figures. No clause says whether late-payment penalty/interest are lines, or one line or two; `line_count` and both line cells are not typeable from the spec. | PDF VFX lines 12–24; key `I26050010.json` lines. | Clause (h): printed interest/penalty amounts on an overdue bill form one `item` line ("Juros e multa") — or exclude #12's line cells by flag. |
| T-7 | Medium | §2.2 `net_amount`, `total_amount`; §5.1 #13 | Alibaba #13 prints "Subtotal (excl. tax)", "Order total USD 1,278.00", "Payment processing fee USD 38.23", "Amount paid USD 1,316.23"; the key is net = total = 1,316.23 with the fee as line 4. "The document's own current-period total" does not choose between the two totals, and "as printed" net is the subtotal (≠ key) — three header cells and `line_count` depend on a rule that is not written. | PDF Alibaba lines 33–41; key `I26050030.json` (lines 750 / 478 / 50 / 38.23). | Rule: on a receipt, `total_amount` = the amount paid; fees charged on the document are lines; `net_amount` = total − VAT when the printed subtotal excludes printed fees. |
| T-8 | Low | §2.6 branches (1)/(2) | (1) tests `net + vat + extra = total` and (2) `vat ≥ \|duty\|` with `vat` possibly `null` (#25/#31); the traces write "+ 0 +" but the formulas do not say `(vat ?? 0)`. | §2.6 text vs the #25 trace. | Write `(vat ?? 0)` in (1)/(2). |
| T-9 | Low | §2.3 clause (g) | "the item line is the premium ("Prémio comercial", … #31: 137.65)" — #31 prints no "Prémio comercial" row; its footnote (1) names "prémio comercial de 127,45"; the block's row is "Prémio antes de impostos (1) 137,65". The value is given, the wording points at the wrong figure. | PDF #31 lines 32, 42. | "the block's premium row — 'Prémio comercial' or, absent that, 'Prémio antes de impostos'". |
| T-10 | Low | §5.3 S-9 `line_count` | S-2 pairs the appended stamp line as n + 1 "on both sides", but `line_count` is not said to be compared after `Split`; a literal builder compares the answer's 2 with the key's 3 on #6, #25, #31. | S-2 / S-9 text; keys `lineCount` 3 / 2 / 2. | "`line_count` is compared after `Split`". |

#### Owner questions Q-EX-0..25

All 26 recommendations are defensible as written; no new question. Verified this round: **Q-EX-25** — the header adjustment gives 411.54 and −155.40 = Σ lines on both insurer documents from the printed figures (traced above), and the corrected #31 key follows SKILL.md §6 line 160; **Q-EX-20** (f) — the Continente summary block reproduces the key's two lines and header; **Q-EX-23** — LG001795 is the ITOO payable leg, so the "reported, not scored" consequence is correctly stated; **Q-EX-4** — the fold is the pinned skill's own rule. Notes: **Q-EX-6** should say that the 8 `vat_rate` cells rest on a document-level rate (T-2) rather than "blank because uncaptured"; **Q-EX-24** figures were taken from round 4, not re-read. T-1 is a scoring rule, not a ruling.

#### Readiness for RED oracles

Not yet, on four items. Cannot be typed with an expected value from the spec: the `vat_amount` cell on #6/#25/#31 and every no-VAT row, and `net_amount` on Lari/eSIMGo/VFX (T-1); the §7 fixture (3) (T-5, self-contradictory); the same-day occurrence-order fixture (T-4); `account_period` on MEO/Regus (T-3). Typeable now: the accept/reject corpus (golden #2 header and lines, #6 lines and net; #10, #35, #37, #40 taken from earlier rounds), `Split` on #6/#25/#31 both ways and fixtures (1), (1′), (2) (figures recomputed: 104/23, 106/23 + item 102, 104/23), `SkillPackageTests` (counts 4/5/1 recomputed), `WorkerStartupTests`, `ClaudeCliTimeoutTests` (JobId rule, attempt table, jobque row), `ClaudeCliPermissionTests` (order stated), `QueueWorkerGateTests` (#22/#23/#17/#29 both branches; `SameTaxId` on "500 940 231"), `IngestionServiceHoldTests` (all six transitions), `EvidenceSecretScanTests`, `CompanyMatcherTests`, `AccountPeriodTests` (the R1 rule verified against `Decide`), the natural-key pairing fixture (unique key from earlier rounds), statement cells, S-4, S-11, the floor rule, the 18 corrections. T-1 and T-5 are one sentence each; T-2/T-3/T-4 a sentence or two.

#### Verified myself

Worktree head `fef45af` (clean); the spec and the review record in full (rounds 1–4 and all dispositions). Skill build HEAD `a558523`, clean: SKILL.md §6 lines 156–162 (stamp and parafiscal rules, line 160 quoted); schema.md lines 17 (24 DOCLOG columns), 78 (32), 87 (18), 93, 95 (26) — the §2.2/§2.3/§2.4 lists match column by column; `awk` long-line counts 4 / 5 (3,206 / 3,368 / 8,014 / 3,326 / 5,681) / 1. PDFs via `pdftotext -raw`/`-layout`: Tranquilidade #6 (634,72 / 132,33 / footnote − 0,03 / Total(*) 767,02, no net, no recipient NIF), #25 (every row of the block, "Recebemos o valor de 411,54", ATCUD, "Isento de IVA"), #31 (137,65 / 12,38 / 5,37 / 17,75 / 155,40, footnote 127,45), Continente #2 (10 product rows, "4 X 11,99", SUBTOTAL/Desconto/TOTAL, the per-rate block), Avis (line rows and VAT SUMMARY), Alibaba, Lari, VFX, eSIMGo, Hydra (Período 20-02-2026 a 19-03-2026, Data Documento 12-03-2026), MEO (PERÍODO CONTAGEM), Regus (per-line From/To dates, TAX $ 0.00), BICS #22 ("VAT On 0.00"), Google Cloud ("VAT (0%)"), Itoorer #29 ("VAT - Self-settlement 0% … 0,00"), Stripe ("Total VAT 0.00"), Revolut (opening/closing lines, four multi-movement days). Keys (40, python): 35/5; 17 zero-VAT headers, none null; the 8 `vatRate` and 6 quantity/unit-price lines; 12 R1 rows with DateDoc/AccountPeriod/evidence, 12 R3 rows; 6 receipts (dateDoc = dateDue = datePay); `statementDoclog.fileHash` on all five bank keys; Revolut `bm_code` order = file order, non-decreasing `docDate`, `docDate = movDate` on all 51, the seven multi-movement days. `05-doclog-all.txt` lines 1583/1587. Code on main `2c8e5f2`: `CompanyMatcher.cs` (private `SameTaxId`, `Bare`, `HasCountryPrefix`), `DocumentTypeRouter.DocumentTypeFor` (invoice/receipt/credit_note/bank_statement/other), `AccountPeriod.cs` (R1–R4), `AccountPeriodService.Decide` (advance = `Start > DateDoc` → first month, else last).

#### Taken from the spec without verification

The pinned CLI's flag semantics, `Read` truncation, `CLAUDE.md` discovery, Windows path-rule syntax and `--settings`/`--safe-mode` interaction; the .NET SDK item facts behind F-R-1 (round-4 feasibility evidence, not rebuilt); token/context estimates and sitting arithmetic; module deployment/rollback and ACLs; `git archive` CRLF; `build_bnkmov.py` 77–84; `SyncEngineWave3.ToCashDelta`; `DocumentCaptureService` C6; the statement balances and V-14 chains (round-3 verified, not re-read); EDP/Águas do Porto order and figures (Q-EX-24, round 4); the 15 Revolut FX rows and the BPI fragment (round 4); `(movDate, amount, currency)` uniqueness (rounds 3–4); AWS/Stripe/Locarent/Via Verde line counts; page counts; `EXTRACT.md` (unwritten); the Awin PDF's figures (no text layer); the worker/queue/ingestion code cited in §4.3/§4.7 (round-4 evidence, not re-read this round).

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** fef45af (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main at `2c8e5f2`)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 4 / Low 5

Revision 5 closes all 23 round-4 findings by text I checked against the code, the .NET 10.0.301 Worker SDK (the §4.6 csproj lines built and published in a throwaway project), the pinned CLI's `--version`/`--help`, the skill build at `a558523`, the 40 keys and the three PDFs the `Split` traces rest on. One High remains, outside the round-4 items: the fixed prompt line names `<sandbox>/document.pdf` while the intake accepts PNG/JPEG/TIFF and the sandbox copy keeps the source extension — under v2 every image intake would name a file that does not exist. The Mediums are a missing migration step in the deployment order, an untyped correction outcome on a `timeouts:2` hold, and two §7 seams (`CompleteAsync`'s Hold branch, the lane-pause counter) that are private and unnamed.

#### Round-4 revisit

| id | State | Revision-5 text relied on; what I verified |
|---|---|---|
| C-R-1 | CLOSED | §2.2 `net_amount` ("Prémio antes de impostos" is the printed net); §2.6 step B `extra = duty + (other ?? 0)`, (1)/(2) `Net += extra`, step C `item.Net += other`, #25/#31 traced both ways; §5.2 flag dropped; S-9. Re-read `pdftotext -raw` #25 lines 43–48 (382,82 / 19,15 / 9,57 / 28,72 / 411,54) and #31 lines 32–40 (137,65 / 12,38 / 5,37 / 17,75 / 155,40); keys: #25 net 411.54, vat 0, lines 392.39 / 19.15; #31 net −155.40, lines −137.65 / −17.75 (corrected per §5.5). Arithmetic: 382.82+0+19.15+9.57 = 411.54 → item 392.39; 137.65+0+12.38+5.37 = 155.40 → item 143.02; header = Σ lines in both. |
| C-R-2 | CLOSED | §2.2 (column sums 60.55 / 13.56), §2.3 clause (f) "the header follows the summary", §7 golden #2. PDF #2 lines 45–47 "(B) 13,00% 3,62 0,47 4,09 / (C) 23,00% 56,93 13,09 70,02"; key net 60.55 / vat 13.56 / total 74.11, lines 3.62/0.47/4.09 and 56.93/13.09/70.02. |
| C-R-3 | CLOSED | §2.3 clause (g); §2.6 traces "one item line". Keys #25 and #31 `lineCount = 2`; PDF #25 lines 43–44 are the two 0,00 rows the clause excludes. |
| C-R-4 | CLOSED | §2.6 branch (3) "never fails the document (V-12 applies to the raw answer only, before `Split`)"; §7 fixture (3) "a finding and no failure". |
| C-R-5 | CLOSED | S-9 "by the key's flow"; R3 "reported, not scored"; Q-EX-23 consequence paragraph. Key #29 GOTT / R / Internal / IE3559150RH. |
| C-R-6 | CLOSED | §2.6 step C "`item.Net += other`"; the `Total − duty` equality is now only the one-item-no-VAT check. |
| C-R-7 | CLOSED | §2.6 branch (2) "(the remainder is deliberately kept as VAT)"; no SKILL.md §6 attribution on (2). |
| C-R-8 | CLOSED | Q-EX-24 "two or more invoices", #27's four ATCUDs plus the 1.50 item; §4.2 "the further invoices in `evidence.notes`". EDP PDF not re-read (round 4 did). |
| C-R-9 | CLOSED | S-2 "reversed where `printOrder = newest_first`". Revolut key's first movement BM001883 at 2024-12-10, `bm_code` ascending with `docDate`; §5.1 #40 "printed newest-first". |
| C-R-10 | CLOSED | S-4 "scores only the rows flagged `false` plus the receipts". |
| C-R-11 | CLOSED | §4.3 gate seam "`CompanyMatcher.SameTaxId` becomes internal under the same `InternalsVisibleTo`"; S-9. `CompanyMatcher.cs` 42 is private today; `src/Sibyla.Platform.Infrastructure/Properties/AssemblyInfo.cs` 3–4 already names `Sibyla.Tests.Platform` and `Sibyla.Tests.Browser`. Residual on the bench/Argus assemblies: T-5. |
| F-R-1 | CLOSED | §4.6 `<Content Include="skill\**" Exclude="skill\**\*.json" CopyToPublishDirectory="PreserveNewest" />` + `<None Remove="skill\**" />`; §7 published-tree assertion. **Built it:** a scratchpad `Microsoft.NET.Sdk.Worker` project with `skill\{EXTRACT.md,SKILL.md,manifest.json,references\vat_rates.json}` and exactly those two lines — `dotnet publish -c Release -f net10.0 --self-contained false -o out` exits 0 and `out\skill\` holds all four files; the same project without the `Exclude` fails with NETSDK1022 ("Duplicate 'Content' items … skill\…"). SDK 10.0.301: `Microsoft.NET.Sdk.Worker.props` 25 (`**\*.json` Content, `CopyToPublishDirectory`), 35 (`None Remove="**\*.json;**\*.config"`); `Microsoft.NET.Sdk.Worker.targets` 15 (`EnableDefaultContentItems` = true); `Microsoft.NET.Sdk.DefaultItems.Shared.targets` 213–222 (`CheckForDuplicateItems Items="@(Content)"`). The module copies the staged tree whole (`FileSystemWindowsAdapterV2.cs` 281, `EnumerateFiles(source, "*", AllDirectories)`), so `skill\` reaches `C:\Apps\Sibyla\worker\<release>\`. |
| F-R-2 | CLOSED | §4.3 `ProcessingEvidence` gains `JobId`; prior timeouts = elements with `JobId == job.Id && ExitCode == −1`; §7 seeds two jobs' elements. `ProcessingEvidence.cs` 17–35 has no job id today; `ReadAll` (45–46) deserialises with camelCase + null-ignore — nullability note T-4. |
| F-R-3 | CLOSED | §4.3 "`jobque.state = 2` … `last_error` = the hold reason, audit action `job.hold`"; §7 "the test asserts the row". `QueueWorker.cs` 376–424 (2 / 0 / 3 per kind), `ClaimAsync` 197–198, 206–207 (states 0/1 re-claimed) — state 2 is never re-claimed. Seam residual: T-3b. |
| F-R-4 | CLOSED | §4.5 probe (a′) (planted `<sandbox>\deny-probe.txt`, `document.pdf` reads), the `--settings`-under-`--safe-mode` record with the amendment path; §8 R-EX-2; Q-EX-21; §7 permissions row. `claude.exe --help` 2.1.259: `--restricted` "confines the file tools to the working directories (--add-dir included) … (managed settings and --settings still apply …)"; `--safe-mode` "Admin-managed (policy) settings still apply. Auth, model selection, built-in tools, and permissions work normally" — silent on `--settings`, as the spec says. |
| F-R-5 | CLOSED | §4.7 `docint.hold_reason` (new nullable text); transition row "`PossibleDuplicate` carrying a `hold_reason` → not-duplicate → `HeldForPerson`"; "`ReleaseHeldProcessing` changes: a row whose `hold_reason` is non-null returns to `HeldForPerson` (9)". `IngestionService.cs` 438–443 (`ResultJson != null → Processed`), 463–486 (`RuleOnDuplicateAsync(false)` → `ReleaseHeldProcessing`), 377–399 (the triage-assignment path also calls it — the rule is on the method, so that caller is covered too). Residual: T-3a (the `timeouts:2` correction). |
| F-R-6 | CLOSED | As C-R-1; §7 "#31 (inputs net 137.65 **and** net null … → Net 155.40 signed −155.40, lines −143.02 / −12.38 … both ways)"; §5.2 "there is **no `netVatNotPrinted` flag**". |
| F-R-7 | CLOSED | §4.3 "internal `CompanyGate.ApplyAsync(conn, tx, ownerId, intakeId, resultJson)` (`InternalsVisibleTo("Sibyla.Tests.Platform")`), called from `CompleteAsync` for Success and Hold"; §7 gate row drives it with `ExtractionResult.ReadIdentities`. `QueueWorker.cs` 268 (`AssignCompanyFromResultAsync` private static, reads top-level `recipient_tax_id`), 340 (`CompleteAsync` private); no test constructs `QueueWorker` (grep over `tests/`: none). Assembly placement: T-5. |
| F-R-8 | CLOSED | §4.3 "the comparison is on the **first whitespace-separated token**", "under **one shared 10 s budget** for both checks". `C:\Apps\Sibyla\tools\claude\claude.exe --version` → `2.1.259 (Claude Code)`, exit 0. Today's `VersionAsync` (`ClaudeCli.cs` 45–74) swallows failures into "unavailable (…)" — the new check's rethrow is the change. |
| F-R-9 | CLOSED | §4.3 two-row attempt table (1 / 0 / 900 / Retry; 2 / 1 / 1,350 / Hold) + "A non-timeout failure between two timeouts does not reset the count". |
| F-R-10 | CLOSED | §4.7 last row and Q-EX-16 note: "fixed at upload as `completed` … never re-reads `docint`". `ChannelIntakeService.cs` 288 (`candidate.Status = … Quarantined ? Quarantined : Completed` from `outcome.Intake`). |
| F-R-11 | CLOSED | §4.2 "four such lines — 89, 164, 228, 408 —, all five selected schema.md lines … 3,206 / 3,368 / 8,014 / 3,326 / 5,681 …, Financial Document Entry Policy one, line 53". `awk 'length > 1900'` at `a558523`: SKILL.md 16–409 → 89 (2251), 164 (2986), 228 (3244), 408 (1993); 581–594 → none; schema.md 17/78/87/93/95 → 3206/3368/8014/3326/5681 chars; Entry Policy → 53 (2116); the two other long-lined rule files (Bank Movement Reconciliation 176, Reconciliation Review 3) are not in the §4.2 selection. Headings: 16 "## 1.", 95 "## 5.", 156 "## 6.", 162 "## 7.", 410 "## 8.", 581 "## 11.", 595 "## 12." |
| F-R-12 | CLOSED | §4.3 "in this order: `-p <prompt>`, `--output-format json` …"; §5.7 "`fileHash` at the top level of a fiscal key; `statementDoclog.fileHash` on a bank key"; §7 "in the §4.3 order, `-p` first". Keys: fiscal keys carry top-level `fileHash`; `REV-DO-GOT-EUR_202512.json` has no top-level `fileHash` and `statementDoclog.fileHash = 554cff19…`; the three PDFs I opened match their key hashes at `D:\fileStorage\repos\invoice-skill-build\<archivePath>`. |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| T-1 | High | §4.3 prompt; §4.5 probe (a′); §7 `ClaudeCliPermissionTests` | The fixed prompt line names "the document at `<sandbox>/document.pdf`" and probe (a′) asserts "`document.pdf` reads". The intake accepts PDF, PNG, JPEG and TIFF, and the sandbox copy is `document` + the source's extension — under v2 every image intake would hand the CLI a path that does not exist (`Read` fails → no valid answer → five retries → dead-letter). Today's prompt carries the real path with its extension and the token replaces the whole path before hashing. | `DocumentContentInspector.cs` 37–40, 55 (`application/pdf`, `image/png`, `image/jpeg`, `image/tiff`); `ClaudeDocumentProcessor.cs` 82 (`"document" + Path.GetExtension(sourceFile)`), 26–38 (`DocumentToken`, `PromptHash`); §4.3 "The document is still copied into the per-job sandbox." | Prompt line "…the document at `<sandbox>/document<ext>`", the whole path replaced by `<document>` before hashing (as today), so `PromptSha256` is extension-independent; probe (a′) and the hostile run name `document.pdf` only because their inputs are PDFs; `ExtractionRunner` keeps the extension. One sentence. |
| T-2 | Medium | §4.6 Deployment; §4.7 `hold_reason` | `docint.hold_reason` is a Platform migration. Production migrations run by hand through `local\migrate.ps1` (Main + Preview) before a release; `publish-release.ps1` lists the release's required migrations and the module's health check 9 (`ApiSchemaMatchesRelease`) fails — and rolls back — a release whose migrations are not applied. §4.6's order is "web → worker" with no migration step, and the web reads `docint` through EF with the new column. | `local\migrate.ps1` 1–25 (`-Target All`, Main + Preview, `PreviewParityTests`); `publish-release.ps1` 39–44; `health.v2.json` check 9; run record §7o item 1 ("none new, so no migration ran"). | State the order: `migrate.ps1` (Main + Preview) → web release → worker release; note the column is nullable, so a worker rollback to a pre-v2 release needs no schema rollback. |
| T-3a | Medium | §4.7 transitions; §7 `IngestionServiceHoldTests` | The `HeldForPerson` rows are split "fiscal document → `Processed` on correction" / "statement → refused", decided by the document kind — but a `timeouts:2` hold has `result_json = null` (§7 timeouts row asserts it), so there is no `doc_type` to decide by, and the outcome of a correction submitted on such a row is not typed (hand-keyed like DeadLetter's "Enter manually" today, or refused). | §4.7 rows 1–2; §7 "`result_json` null and `hold_reason = timeouts:2`"; `Documents.razor` 176–179 (DeadLetter offers "Enter manually"); `IngestionService.cs` 302–313. | "A hold with `result_json` null takes the fiscal branch (hand-keyed, like DeadLetter today); the statement refusal is decided by `result_json.doc_type = bank_statement`" — and one §7 case for it. |
| T-3b | Medium | §7 `ClaudeCliTimeoutTests` | Two expected effects have no seam: (i) "`CompleteAsync` writes `docint` status 9 … `jobque.state = 2`, `last_error`, audit `job.hold`" — `CompleteAsync` is private in `QueueWorker`, which no test constructs (F-R-7 named a seam for the gate only); (ii) "two consecutive timeouts across the two slots pause the lane" — the per-process counter and `PauseLaneAsync` are private to the same class. The expected rows are typeable; how the test reaches them is not. | `QueueWorker.cs` 164 (`PauseLaneAsync` private), 340 (`CompleteAsync` private); `grep "QueueWorker(" tests` → none. | Name the seams: an internal `JobCompletion.CompleteAsync(conn, tx, leaseOwner, job, outcome)` (or `CompleteAsync` made internal) under the worker's `InternalsVisibleTo`, and an internal `TimeoutLaneMonitor.Record(timedOut) → bool pause` the slot loop consults; the test drives both. |
| T-4 | Low | §4.3 evidence fields | `JobId` must be nullable: `ProcessingEvidence.ReadAll` deserialises the existing rows' elements, which carry no `jobId`; a `required`/non-nullable member would make every pre-v2 row's evidence unreadable (the page's evidence panel, the prior-timeout read). | `ProcessingEvidence.cs` 15–46 (`required` members, `ReadAll`). | `public Guid? JobId { get; init; }`; old elements compare unequal to any job id. |
| T-5 | Low | §4.3 gate seam; S-9; §5.7 | The assembly hosting `CompanyGate` is unstated: `CompleteAsync` lives in `Sibyla.Worker.Documents`, which has no `InternalsVisibleTo` today; `SameTaxId` lives in `Sibyla.Platform.Infrastructure`, whose `InternalsVisibleTo` names neither `Sibyla.Tests.Argus` (where `ExtractionScoringTests` sits) nor the bench assembly. | `AssemblyInfo.cs` in Platform.Infrastructure (Platform, Browser) and Web (Browser); no `Properties/AssemblyInfo.cs` in the worker; §7 places the scorer in `tests/Sibyla.Tests.Argus`. | Name them: `CompanyGate` in the worker with `[InternalsVisibleTo("Sibyla.Tests.Platform")]`; Platform.Infrastructure adds `Sibyla.Tools.ExtractionBench` (and `Sibyla.Tests.Argus` if the scorer calls `SameTaxId` directly). |
| T-6 | Low | §4.5 deny file | `permissions.json` is written inside the sandbox, which is an `--add-dir` root with `Read(<sandbox>/**)` allowed — the model can read the deny file and learn the enumerated secret locations. No content is exposed (the paths stay denied); it is just an unnecessary disclosure. | §4.3 "`--settings <jobDir>/permissions.json`"; §4.5 allow list. | Deny `Read(<sandbox>/permissions.json)` too, or write the file beside the sandbox (outside every working directory). |
| T-7 | Low | §7 `SkillPackageTests` published tree | A `dotnet publish` of the worker inside a test that runs under `dotnet test Sibyla.slnx` (no filter, all six projects) rebuilds the worker into the same `obj\` while sibling test assemblies run; feasible (my probe published in ~20 s) but the flags are unstated and the suite time grows. | `test.ps1` 4; scratchpad probe. | State `-c Release --no-restore -o <temp>` for the in-test publish, or move the published-tree assertion to `publish-release.ps1` as a post-publish check (both keep "asserted on the output"). |
| T-8 | Low | §7 `WorkerStartupTests` | "a fake that never answers exhausts the shared 10 s budget" is a 10 s wall-clock wait in the suite unless the budget is injectable. | §4.3 "one shared 10 s budget". | `WorkerOptions.StartupCheckSeconds` (default 10); the test passes 1. |

#### Owner questions Q-EX-0..25

All 26 recommendations are buildable as written on this host. Verified where an artefact decides: **Q-EX-15** — the pinned binary answers `2.1.259 (Claude Code)`; `--model`, `--effort` (choices `low, medium, high, xhigh, max` — the value the bench passes must be one of these) and `--fallback-model` exist on 2.1.259; `ResolveCliExecutable` keeps an explicit existing path (`ClaudeCli.cs` 122–127). **Q-EX-21** — every flag of the §4.3 set is in `--help`; `--permission-prompts none` and `--no-session-persistence` "only work with --print", satisfied by `-p`; the deny rules' binding remains the R-EX-2 probe (a′), as the spec says. **Q-EX-16** — the web-first release needs the migration step first (T-2). **Q-EX-18** — per-job evidence folders and `JobId` (nullable, T-4) make "its element of `evidence_json`" identifiable; the re-extract key's uniqueness holds (`ix_jobque_idempotency_key` unique, migration `20260827172608_IntakeAndQueue.cs` 88–92). **Q-EX-25** — the fold with the header adjustment reproduces #25 and the corrected #31 from the printed figures both ways (arithmetic re-done against the PDFs and keys). **Q-EX-22** — nothing in this review read `D:\ApolloData\worker-claude`. **Q-EX-24** — replacement rows taken from round 4. No new owner question: T-1…T-8 are spec sentences, not rulings.

#### Readiness for RED oracles

Not yet, on one blocker and three seams. **Blocker:** the fixed prompt line and its golden hash (T-1) — the `ClaudeCliPermissionTests` argument list and the `ProcessingEvidenceTests` prompt hash would be typed to `document.pdf` and then changed. **Seams:** `ClaudeCliTimeoutTests`' Hold write and lane pause (T-3b); the `timeouts:2` correction case (T-3a). **Typeable now from the spec alone:** `ExtractionContractV2Tests` (accept/reject corpus; goldens #2/#6/#25/#31 figures verified against PDFs and keys), `SkillPackageTests` (path source, heading anchors at 16/95/156/162/410/581, long-line list, CRLF/LF — `git archive` at `a558523` yields 3,043 CRs in SKILL.md and `-c core.autocrlf=false` yields 0, the blob being `i/lf`; the published-tree assertion proven by the scratchpad build), `WorkerStartupTests` (first-token compare against `2.1.259 (Claude Code)`), `ExtractionResultIdentityTests`, `QueueWorkerGateTests` through `CompanyGate.ApplyAsync` (#22 → GOTT/PT513362061, #23 → ITOO/IE3559150RH, #29 both branches, `SameTaxId` on "500 940 231" ↔ "PT500940231" per `CompanyMatcher.cs` 42–50), `DocumentsReviewV2Tests` (`Sibyla.Tests.Browser` already prerenders `/documents` through `RenderSmokeTests.SibylaFactory`, line 277), `IngestionServiceHoldTests` (all rows but T-3a), `ClaudeCliPermissionTests` (order and flag set verified against `--help`; the file name per T-1), `EvidenceSecretScanTests`, `CompanyMatcherTests`/`AccountPeriodTests` (both exist: `tests/Sibyla.Tests.Platform/CompanyMatcherTests.cs`, `tests/Sibyla.Tests.Argus/AccountPeriodTests.cs`), and every `ExtractionScoringTests` fixture — `Split` on #6/#25/#31 both ways and the four synthetic branches (figures recomputed), pairing on the Revolut key (51 movements, `bm_code` ascending), the statement cells, S-4/S-6/S-11, the floor rule, the 18 corrections. T-1 is one sentence; T-2, T-3a, T-3b are a sentence each plus a named seam; the Lows can ride the same revision.

#### Verified myself

- Worktree head `fef45af`, clean; the spec (504 lines) and the review record (893 lines, rounds 1–4 and the revision-5 dispositions) in full. Main `2c8e5f2`: `WorkerOptions.cs` (240/300/2/10, `ClaudeCliPath = "claude"`, no version/model/effort), `ClaudeCli.cs` (3-arg `RunAsync`, `-p` then `--output-format json --add-dir <jobDir> --allowedTools Read`, timeout → `(-1, …)`, 20 s `--version` cap swallowing failures, npm fallback), `ClaudeDocumentProcessor.cs` (sandbox copy `document<ext>` line 82, prompt with the real path, evidence beside the source `attempt-N-*` with overwrite, `ExitCode` into the element), `QueueWorker.cs` (claim states 0/1, renewal `max(10, Lease/3)`, lane pause on the substring, private gate on top-level `recipient_tax_id` against `commst`, duplicate → status 6 in-transaction, `CompleteAsync` per-kind rows and `evidence_json` append, `JobOutcomeKind` Success/Retry/Dead), `ClaimedJob.cs`, `Program.cs` (`AddWindowsService`, `host.Run()` uncaught), `QueueJob.cs` (states 0–4, `MaxAttempts = 5`), `DocumentIntake.cs` (enum ends at `Quarantined = 8`), `IngestionService.cs` 208/230/455/575/592 (keys), 293–330, 377–399, 438–458, 463–486, 513–580, `ChannelIntakeService.cs` 285–291, `ProcessingEvidence.cs`, `CompanyMatcher.cs`, `ExtractionContract.cs` 26 (`sibyla.extract.v1`), `Documents.razor` (route `/documents`, form on `Processed`, DeadLetter "Enter manually", `StatusLabel` default arm, v1 field names), `DocumentContentInspector.cs` 37–55, migration `20260827172608_IntakeAndQueue.cs` (unique index 88–92; no CHECK on `processing_status` in Platform migrations), the worker csproj (Sdk line only), `publish-release.ps1`, `provision-production.ps1` 158–177 and 224 (worker ReadAndExecute on `worker` and `tools` — line 176 as cited; `SIBYLA_SECRETS_FILE`, `CLAUDE_CONFIG_DIR`), `migrate.ps1`, `test.ps1`, `Sibyla.slnx`, the six test projects' references and fixtures, `ProcessingEvidenceTests` (DB-backed, `CannedCli`), `RenderSmokeTests` 277, `AssemblyInfo.cs` (both), `local/deploy/Invoke-SibylaDeployment.ps1` params (`-Execute`, `-Resume`, `-WhatIf`), `FileSystemWindowsAdapterV2.cs` 281 (whole-tree copy), `health.v2.json` checks 7 and 9, run record §7o.
- .NET SDK 10.0.301: `Microsoft.NET.Sdk.Worker.props` 22–38, `Microsoft.NET.Sdk.Worker.targets` 15, `Microsoft.NET.Sdk.DefaultItems.Shared.targets` 179–222, `Microsoft.NET.Sdk.DefaultItems.props` 42–43; **the two §4.6 csproj lines built and published in a scratchpad Worker-SDK project (all four package files present in `out\skill\`), and the same project without `Exclude` failing NETSDK1022**.
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe` (219,715,232 bytes): `--version` → `2.1.259 (Claude Code)`; `--help` for `-p`, `--output-format`, `--add-dir`, `--tools`, `--restricted`, `--safe-mode`, `--strict-mcp-config`, `--disable-slash-commands`, `--permission-prompts`, `--settings`, `--no-session-persistence`, `--model`, `--effort`, `--fallback-model`, `--allowedTools`, `--bare`. Never run on any input.
- Skill build: HEAD `a558523`, `status --porcelain` empty, `core.autocrlf = true`, `ls-files --eol` = `i/lf w/crlf`; headings, long-line counts and lengths as in F-R-11; SKILL.md 160 (the stamp/fold rule); `skill_currency.json` (Stage 16 Round 30, `PackageMatchesSource`, 20/20); `invoice-registry.skill` 559,999 bytes, 26 entries, 1,433,312 uncompressed; `Specs/vat_rates.json` present; archive CR counts (3,043 vs 0).
- Keys (read-only): 42 entries (40 keys + `INDEX.md` + `_sql`); the fiscal key's top-level shape (`fileHash`, `lineCount`, `accountPeriodRule`, …) and the bank key's (`statementDoclog.fileHash`, `bankAccount`, `bnkchk`, `movements`); #6/#25/#31/#2/#29/#23 headers and lines; six companies `{CONF PT502947721, FMAT PT509428428, GOTT PT513362061, ITOO IE3559150RH, SILA PT518198499, VIGA PT519031563}`; Revolut `bnkchk` starting at 202501/30.06; the card account type "Cartão de Crédito". PDFs #25, #31, #2 located at their key archive paths, SHA-256 matching, amounts via `pdftotext -raw`.

#### Taken from the spec without verification

CLI runtime behaviour not exercised: the `Read` 2,000-character truncation, the `json` envelope's `num_turns`/`usage`/`modelUsage`, the `stream-json` file-read trace, the Windows `Read(...)` rule syntax and deny-over-allow precedence, whether `--settings` applies under `--safe-mode`, the usage-limit message the bench would parse; the SCM's treatment of a service whose `StartAsync` throws after `OnStart`; the token/context estimates and the sitting arithmetic; that the release gate's clean-export fingerprint binds a committed `skill/` (no fingerprint code found under `local/deploy/*.ps1` — the claim rests on the Slice 2 record); the EDP/Águas do Porto PDFs, #6, the statement PDFs and their printed balances (round-3/4 evidence, not re-read); MEO/EDP/Avis/VFX/AWS/Stripe wording in §3/§5.1; page counts; the content of `EXTRACT.md` (unwritten); the reserve entries beyond their doclog rows; the "2 otherwise column-sliced" Revolut rows.

### Author's dispositions — revision 6

Revision 6 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 5. Ids: `C-T-n` = the contract reviewer's T-1…T-10, `F-T-n` = the feasibility reviewer's T-1…T-8 (T-3a/T-3b). Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 6. Decisions the coordinator took for this revision are written in and not re-opened; no new owner question.

**Contract reviewer (C-T-1 … C-T-10)**

| id | Sev | Disposition | Where in revision 6 |
|---|---|---|---|
| C-T-1 | High | fixed — branch (0) runs whenever `net_amount` is null and a total is printed, irrespective of duty/other, and comes before the identity clause (`Net := Total − (VAT ?? 0)`); in every branch, after the split, `VAT := VAT ?? 0`; S-3 states that no header amount is null after the transform, so a raw null never meets a key number; Lari/eSIMGo/VFX named as the gross-only-without-duty cases (Net = Total); the §7 #6 fixture asserts VAT 0.00 and Lari is traced | §2.6 Split (steps A/B, traces); §5.3 S-3; §7 scoring row |
| C-T-2 | Medium | fixed — §2.3: a single document-level rate or exemption ("VAT @ 0%", "Isento de IVA", reverse charge, "VAT - Self-settlement 0%") is the `vat_rate` of every line; step A gives the appended stamp line `vat_rate = 0`; Q-EX-6's note says the 8 scored cells rest on that rule | §2.3 `vat_rate`; §2.6 step A; §1.2 Q-EX-6 |
| C-T-3 | Medium | fixed — S-6: the key's rule applied to the extraction's inputs — R1 rows via `AccountPeriodService.Decide` from `service_period`, R3 rows via the day-of-month rule from `date_doc`; a printed period on an R3 row (MEO, Regus) is reported under S-8, never penalised; §3 #1 aligned | §5.3 S-6; §3 #1 |
| C-T-4 | Medium | fixed — occurrence order = the answer's `seq` among the tied rows against the key's `bm_code` ascending among the tied rows, in every `printOrder` (the FDR assigned `bm_code` top-to-bottom within a day; Revolut 15 Dec, 19 Aug, 10 Mar, 31 Jan); the "reversed for `newest_first`" sentence dropped; the §7 fixture retyped (A pairs with A) | §5.3 S-2; §7 scoring row |
| C-T-5 | Medium | fixed — "header = Σ lines in branches (0)–(2); in (3) the difference is the DPRCHK finding", in §2.6 and in fixture (3) (Net 100.00, Σ lines 104.00, the 4.00 difference as the finding) | §2.6 check; §7 fixture (3) |
| C-T-6 | Medium | fixed — clause (h): printed interest/penalty amounts on an overdue bill form one item line ("Juros e multa"); VFX #12 typed to 2 lines (1,990.00 / 41.78) | §2.3 clause (h); §7 |
| C-T-7 | Medium | fixed — clause (i) and §2.2 `total_amount`: on a receipt `total_amount` = the amount paid; fees charged on the document are lines; `net_amount = total − VAT` when the printed subtotal excludes printed fees; Alibaba #13 typed (750 / 478 / 50 / 38.23, net = total = 1,316.23) | §2.2 `total_amount`; §2.3 clause (i); §7 |
| C-T-8 | Low | fixed — `(vat ?? 0)` written in branches (1)/(2); `extra = (duty ?? 0) + (other ?? 0)` | §2.6 step B |
| C-T-9 | Low | fixed — clause (g): "the block's premium row — 'Prémio comercial' or, absent that, 'Prémio antes de impostos'" | §2.3 clause (g) |
| C-T-10 | Low | fixed — `line_count` is compared after `Split` (#6 3, #25 2, #31 2) | §5.3 S-9; §7 |

**Feasibility reviewer (F-T-1 … F-T-8)**

| id | Sev | Disposition | Where in revision 6 |
|---|---|---|---|
| F-T-1 | High | fixed — the fixed prompt line names `<sandbox>/document<ext>`; the whole path is replaced by the `<document>` token before hashing (as today's `DocumentToken`), so `PromptSha256` is extension-independent; `ExtractionRunner` keeps the source extension; probe (a′) and the hostile run say `document.pdf` only because their inputs are PDFs; a §7 `ClaudeCliPermissionTests` case with a `.png` input and an equal-hash assertion in `ProcessingEvidenceTests` | §4.3 flag list and prompt; §4.5 probe (a′); §7 permissions and evidence rows |
| F-T-2 | Medium | fixed — deployment order `local\migrate.ps1 -Target All` (Main + Preview) → web release → worker release, with health check 9 (`ApiSchemaMatchesRelease`) named as the guard; `docint.hold_reason` is nullable, so a worker rollback to a pre-v2 release needs no schema rollback | §4.6 Deployment order, Rollback |
| F-T-3a | Medium | fixed — a hold with `result_json` null takes the fiscal branch (hand-keyed, like DeadLetter's "Enter manually" today); the statement refusal is decided by `result_json.doc_type = bank_statement`; one `IngestionServiceHoldTests` case | §4.7 transitions; §7 holds row |
| F-T-3b | Medium | fixed — internal `JobCompletion.CompleteAsync(conn, tx, leaseOwner, job, outcome)` and internal `TimeoutLaneMonitor.Record(timedOut) → bool pause` consulted by the slot loop, both in `Sibyla.Worker.Documents` under a new `[InternalsVisibleTo("Sibyla.Tests.Platform")]`; `ClaudeCliTimeoutTests` drives both | §4.3 seams; §7 timeouts row |
| F-T-4 | Low | fixed — `public Guid? JobId { get; init; }` nullable; old elements read back and compare unequal to any job id | §4.3 evidence fields; §7 |
| F-T-5 | Low | fixed — `CompanyGate` lives in the worker under the same `InternalsVisibleTo`; `Sibyla.Platform.Infrastructure`'s `InternalsVisibleTo` adds `Sibyla.Tests.Argus` and `Sibyla.Tools.ExtractionBench` for `SameTaxId` | §4.3 seams |
| F-T-6 | Low | fixed — `permissions.json` is written under `<staging dir>/evidence/<jobId>/permissions.json`, outside every working directory, and `--settings` points there | §4.3 flag list; §4.5; §7 |
| F-T-7 | Low | fixed — the published-tree assertion moves to `publish-release.ps1` as a post-publish check that fails the publish when any manifest file is missing under `<target>\skill\`; `SkillPackageTests` keeps only the committed-package-equals-fresh-build assertion | §4.6; §7 `SkillPackageTests` |
| F-T-8 | Low | fixed — `WorkerOptions.StartupCheckSeconds` (default 10) is the shared budget; the test passes 1 | §4.3 start-up; §7 |

Owner questions after revision 6: Q-EX-0…25 unchanged in number; Q-EX-6's note added. No new question.

## Round 6 — revision 6, head `8ffd66d` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-5 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 4 / 3 / 5 | 18 of 18 |
| feasibility | ACCEPT | 0 / 0 / 2 / 5 | 18 of 18 ("nothing in §4–§8 is unbuildable on this host"; RED oracles ready) |

The contract reviewer's full sweep of the 35 fiscal keys against their PDFs found four document-specific gaps (Lari `document_id` is an FDR note outside N-7; the two utility bills have one key line each; Águas do Porto's key `dateDoc` is not the printed issue date; AWS's lines are EUR conversions of USD-only rows). Feasibility ACCEPT stands on revision 6; revision 7 must be re-reviewed by both. Ids are per reviewer (C-U-n, F-U-n).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** 8ffd66d (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main `2c8e5f2`; skill build HEAD `a558523`, clean; all 40 PDFs located at their key `archivePath`, SHA-256 = key `fileHash` on 40/40)
**Verdict:** REVISE
**Counts:** Critical 0 / High 4 / Medium 3 / Low 5

Revision 6 closes all 18 round-5 findings by text I checked against the PDFs, the keys and the code — `Split` reproduces #6, #25, #31, Lari, eSIMGo, VFX and Alibaba from the printed figures exactly as §2.6 writes it, the `vat_rate` rule types all 8 scored cells, S-6 reproduces all 12 R1 and all 12 R3 `account_period` cells, and the Revolut same-day `bm_code` order is print order on the three days I re-read. The full sweep of the 35 fiscal keys against their PDFs found four cells or cell groups the document + spec still cannot produce: Lari's key `document_id` `202605` is the FDR's own note and is outside N-7's closed list; the two utility bills (#21, #27) have one key line each where doctrine (a) yields three or four; #21's key `dateDoc` 2026-07-14 is not the printed issue date (2026-07-22); and AWS's nine key lines are EUR conversions of USD-only detail rows, which the contract's "verbatim" wording forbids and V-9 then rejects. Each is one or two sentences or one corrections row.

#### Round-5 revisit

| id | State | Revision-6 text relied on and what I verified |
|---|---|---|
| C-T-1 | CLOSED | §2.6 step B "(0) no-net branch, terminal, and it comes first … irrespective of `duty` and `other` … `Net := Total − (VAT ?? 0)`", "In every branch, after the split, `VAT := VAT ?? 0`"; S-3 "so no header amount is null after the transform"; §7 "#6 … Net 767.02, **VAT 0.00**", Lari traced. Traced from `pdftotext -raw`: #6 (lines 56/67/72/74: 634,72 / 132,33 / − 0,03 / Total(*) 767,02, no net or VAT printed) → step A line 3 = −0.03 → (0) Net 767.02, VAT 0 → Σ lines 767.02 = key `I26030044.json`. Lari (line 11 "Valor da Comissão em Dólares: $ 372,30", the only figure) → (0) Net 372.30, VAT 0 = key. eSIMGo (line 19 "Total 3 $79.1100") → 79.11 = key; VFX (lines 13/15/20–21) → 2,031.78 = key. |
| C-T-2 | CLOSED | §2.3 `vat_rate` "a single document-level rate or exemption … is the `vat_rate` of every line"; §2.6 step A "`vat_rate = 0`"; Q-EX-6 note. Avis `-layout` line 40 "VAT @ 0% 0,00 12.676,92" with "EUS" on every line row (21–28) → 6 cells at 0 = key `R26080004.json`; #25 line 35 "Isento de IVA (N.º 28 do Art.º 9º CIVA)" → item line 0 and the appended stamp line 0 = key `I26080026.json` (`vatRate: 0.0` on both). No other key line carries a `vatRate` (python over the 109 lines). |
| C-T-3 | CLOSED | S-6 "the key's own rule applied to the extraction's inputs — on the 12 R1 rows `AccountPeriodService.Decide` … on the 12 R3 rows the day-of-month rule from `date_doc` alone"; §3 #1 aligned. `AccountPeriodService.cs` 26–58 on main: `advance = stated.Start > dateDoc` → first month else last; R3 `Day <= 10` → prior month. MEO 2026-07-17 → 202607 = key (its "PERÍODO CONTAGEM: 01/06/2026 A 30/06/2026", raw line 8, now reported only); Regus 2026-07-01 → 202606 = key (per-line "1 Aug 2026 – 31 Aug 2026", line 43). All 12 R3 rows recomputed = key; all 12 R1 rows through `Decide` = key (Hydra "Período: 20-02-2026 a 19-03-2026" with DateDoc 12-03 → 202603; #6 → 202605; Google "Fee for April 2026" → 202604; AWS "billing period April 1 - April 30, 2026" → 202604; eSIMGo → 202604; Stripe "Service Month May 2026" → 202605; Lari → 202605; BICS "202607" traffic period with DateDoc 08-15 → 202607; #25 → 202609; Itoorer "from 03/2026" → 202603). Locarent's header period: U-5. |
| C-T-4 | CLOSED | S-2 "occurrence order = the answer's `seq` among the tied rows against the key's `bm_code` ascending among the tied rows, in every `printOrder`"; §7 fixture "the tie pairs A with A". Re-read `REV-DO-GOT-EUR_202512.json` vs raw: 15 Dec A VER NAV (raw 37) → Isadora (40) = BM001932 → 1933; 19 Aug Stripe (120) → Gott −25,000 (126) → Factor Matriz +25,000 (127) = BM001910 → 1911 → 1912; 10 Mar FEE (218) → Reembolso (221) → MOA 500 (222) = BM001889 → 1890 → 1891. No `(movDate, amount, currency)` tie inside any of the five keys (python). |
| C-T-5 | CLOSED | §2.6 "header `Net` equals Σ lines' `Net` after step C in branches (0)–(2); in branch (3) the difference is the DPRCHK finding"; §7 fixture (3) "Net 100.00 … Σ lines 104.00 … the 4.00 difference". |
| C-T-6 | CLOSED | §2.3 clause (h); §5.1 #12; §7 "VFX #12 (clause (h): lines 1,990.00 / 41.78)". Raw lines 12–21: 2.031,78 / 1.990,00 / "R$ 39,80 de multa + R$ 1,98 de juros" → 41.78 = key `I26050010.json` line 2. |
| C-T-7 | CLOSED | §2.2 `total_amount` "on a receipt, the amount paid"; clause (i); §7 Alibaba fixture. Raw lines 32–41: 750.00 / Shipping / Insurance (USD 478.00, 50.00 at lines 5–6) / "Order total USD 1,278.00" / "Payment processing fee USD 38.23" / "Amount paid USD 1,316.23" → 4 lines, net = total = 1,316.23 = key `I26050030.json`. |
| C-T-8 | CLOSED | §2.6 "`extra = (duty ?? 0) + (other ?? 0)`", "(1) `net + (vat ?? 0) + extra`", "(2) `net + (vat ?? 0) = total` and `(vat ?? 0) ≥ \|duty\|`". #25: 382.82 + 0 + 19.15 + 9.57 = 411.54 (raw 45–48, 33) → Net 411.54, item 392.39, stamp 19.15 = key; #31: 137.65 + 0 + 12.38 + 5.37 = 155.40 (raw 32–35, 40) → −155.40 / −143.02 / −12.38 = the §5.5 correction. |
| C-T-9 | CLOSED | Clause (g) "the block's premium row — 'Prémio comercial' (#25: 382.82) or, absent that, 'Prémio antes de impostos' (#31: 137.65)". #31 raw 32 "Prémio antes de impostos (1) 137,65", footnote 42 "prémio comercial de 127,45" — the wording now points at the right row. |
| C-T-10 | CLOSED | S-9 "`line_count` (**compared after `Split`** … #6 3, #25 2, #31 2)"; §7 "`line_count` 3 after `Split`". Keys `lineCount` 3 / 2 / 2. |
| F-T-1 | CLOSED | §4.3 prompt "`<sandbox>/document<ext>`", "the **whole document path** is replaced by the `<document>` token"; §4.5 (a′) "the probe's input is a PDF"; §7 "one case with a `.png` input … the same `PromptSha256`". Text only; code not re-read. |
| F-T-2 | CLOSED | §4.6 "(1) **`local\migrate.ps1 -Target All`** … health check 9 (`ApiSchemaMatchesRelease`) … (2) web … (3) worker"; Rollback "needs no schema rollback — `hold_reason` is nullable". Text only. |
| F-T-3a | CLOSED | §4.7 row 1 "**or any hold with `result_json = null`** (`timeouts:2`) … hand-keyed like DeadLetter"; row 2 "decided by **`result_json.doc_type = bank_statement`**"; §7 holds row. Text only. |
| F-T-3b | CLOSED | §4.3 "**`JobCompletion.CompleteAsync(conn, tx, leaseOwner, job, outcome)`** … **`TimeoutLaneMonitor.Record(timedOut) → bool pause`**" in `Sibyla.Worker.Documents` under `InternalsVisibleTo("Sibyla.Tests.Platform")`; §7 timeouts row drives both. Text only. |
| F-T-4 | CLOSED | §4.3 "`public Guid? JobId { get; init; }` — nullable"; §7 "an old element without one reads back and compares unequal". |
| F-T-5 | CLOSED | §4.3 "`CompanyGate` … in the worker"; "`Sibyla.Platform.Infrastructure`, whose `InternalsVisibleTo` … adds `Sibyla.Tests.Argus` … and `Sibyla.Tools.ExtractionBench`". `CompanyMatcher.cs` 42 on main is still `private static bool SameTaxId` (the change is this slice's), `Bare`/`HasCountryPrefix` 48–50 give "500 940 231" ↔ "PT500940231". |
| F-T-6 | CLOSED | §4.3 "`--settings <staging dir>/evidence/<jobId>/permissions.json` … written **outside every working directory**"; §4.5 same; §7 "outside the sandbox". |
| F-T-7 | CLOSED | §4.6 "**the published tree is checked by `publish-release.ps1`** as a post-publish step"; §7 `SkillPackageTests` "The published tree is **not** this test's". Text only. |
| F-T-8 | CLOSED | §4.3 "**one shared budget of `WorkerOptions.StartupCheckSeconds` (default 10)** … (the test passes 1)"; §7 "`StartupCheckSeconds = 1`". |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| U-1 | High | §2.7 N-7; §5.2 `placeholders`; §5.5 | Lari #16's key `documentId` is `202605` — the FDR's own note (the reference period), printed nowhere; the PDF has no invoice number at all. N-7 names the class ("a key `document_id` that is the FDR's own note rather than a printed number") but lists only eSIMGo, and says the list "is closed: a new placeholder is a key correction (§5.5)" — §5.5 does not pre-declare it either. The honest answer (`null`) misses by design on a cell the spec claims is derivable. | `I26060014.json` `documentId: "202605"`; raw lines 1–14 (only "INVOICE ORBISTER", "Data da Invoice: 3 de Junho de 2026", "Mês de Referência: Janeiro … Maio"); N-7 text; §5.5 list. | Add `I26060014 document_id 202605` to N-7's list (and `answer-key.flags.json` `placeholders`) → expected `null`; §5.1 #16 "no document number printed". |
| U-2 | High | §2.3 doctrine (a)–(i); §5.1 #21, #27; §2.2 `net_amount` | The two utility bills have **one key line each**, and no clause produces that. Águas do Porto #21 (invoice 96340048, p.2) prints category subtotals ÁGUA 32,83 / ÁGUAS RESIDUAIS 28,92 / TAXA DE RECURSOS HÍDRICOS 0,76 over six tariff rows, then "Total sem IVA 62,51 / IVA 3,76 / TOTAL 66,27" — clause (a) (summary page + detail → category subtotals) gives 3 lines; the key has 1 (62.51). EDP #27's first invoice prints Consumo 13,52 / Potência 14,41 ("A Total 27,93 sem IVA") and DGEG 0,07 / IEC 0,07 with a per-rate VAT block (6 % on 13,52 → 0,81; 23 % on 14,55 → 3,35) — (a) gives 2–4 lines, (f) would give 2; the key has 1 (28.07 / 4.16 / 32.23). Neither `line_count` nor any line cell on the two rows is typeable; pairing by `line_no` then misses line 1 too. Also EDP's header: "A Total 27,93 sem IVA" reads as a single printed net, while the key's 28.07 is the VAT block's base sum (13.52 + 14.55). | `I26070079.json` (1 line 62.51/3.76/66.27); raw 132–158. `I26080035.json` (1 line 28.07/4.16/32.23); raw 116–140. Clause (a) wording; §2.2 "per-rate VAT summary and no single net figure". | Clause (j): **a utility bill (water, electricity, gas) is one `item` line per invoice** — the invoice's "Total sem IVA" / VAT / total — its consumption, tariff, availability and tax rows (TRH, DGEG, IEC) summarised in `evidence.notes`; its header `net_amount` is the VAT block's base sum where the sectional subtotal excludes VAT-bearing tax rows (#27: 28.07, not 27.93). Type #21 and #27 in §7. (Alternative: exclude the two rows' line cells by flag.) |
| U-3 | High | §5.5 pre-declared corrections; §2.2 `date_doc` | #21's key `dateDoc` is **2026-07-14**; the document prints "Emissão: 2026-07-22" (p.1) and "Data de Fatura 2026-07-22" on the invoice page; 2026-07-14 is the billing-period end and the date beside the invoice in the conta-corrente block ("Fatura 96340048 - Instalação 743801 2026-07-14 66,27"). Under "the issue date as printed" the extraction says 07-22 and misses. Not pre-declared. | `I26070079.json` `dateDoc: 2026-07-14`, `dateDue: 2026-08-11`; raw lines 7–8, 30, 106–107, 109. | Pre-declare `I26070079 dateDoc 2026-07-14 → 2026-07-22, p.2 "Data de Fatura"` in §5.5 (blank rule row, so `account_period` is unaffected). |
| U-4 | High | §2.3 `net_amount` "printed sign, verbatim"; §2.1 "amounts are in the document's currency"; §5.1 #10; §7 golden #10 | AWS #10's detail prints every service **in USD only** ("Amazon Elastic Compute Cloud USD 626.08 / Charges USD 509.01 / VAT USD 117.07"); EUR appears only in the summary (448.88 / 103.24 / 552.12 at 1 USD = 0.86340951809 EUR). The key's nine lines are EUR conversions at that rate, rounded per line (509.01 × 0.8634… = 439.48, 117.07 → 101.08, 10.80 → 9.32, 2.48 → 2.14, 0.08 → 0.07, 0.02 → 0.02; Σ net 448.87 vs header 448.88). No rule says lines printed in a secondary currency are converted; "verbatim" says the opposite, and a USD-line answer fails V-9 (Σ 519.89 ≠ 448.88) with an honest null `reconciliation_note` → dead-letter. 27 line cells (≈ 8 % of the lines budget) and golden #10 rest on the unwritten rule. | Raw lines 22–25, 27–36, 46–48; `I26050001.json` lines. | §2.3: "when the line table is printed only in a secondary currency and the document states the conversion rate, line amounts are that rate applied per line, rounded to 2 dp, in the document's currency, flagged 'converted at printed rate'; the header stays as printed; V-9's ± 0.02 absorbs the rounding". Type golden #10 to it. |
| U-5 | Medium | §2.2 `service_period`; §2.3 per-line `service_period` "(Locarent)"; S-6 | Locarent #24 prints its period only on the lines ("Prestação nº 74 (01/08/2026 - 31/08/2026)" on all seven); the header block has none. §2.3 says the period is per line on Locarent; §2.2 says the header period is "`null` when none is stated". S-6 derives the R1 row from the **header** `service_period`: null → `Decide` falls to R3 → 2026-08-01 (day ≤ 10) → 202607 ≠ key 202608 — `service_period` and `account_period` miss on an honest answer. | Raw lines 14–35; `I26080017.json` R1 202608; `AccountPeriodService.cs` 33–58. | "When the header prints no period and every line prints the same one, the header `service_period` is that period (Locarent)"; or S-6 takes the lines' common period when the header's is null. |
| U-6 | Medium | §2.3 `vat_amount`, `net_amount` (non-nullable, "as printed") | Per-line VAT is not printed on MEO (8 lines: category subtotals s/IVA, one 23 % summary), Hydra (3), Mobilize (3 of 7), Locarent #24/#35 (14), Anaptyxis (7: net, rate and total printed, VAT not); per-line net is not printed on Via Verde (5: "Total em Portagens 4,05 / IVA incluído 0,76"). The keys hold `round(net × rate, 2)` and `total − vat` (MEO Σ line VAT 700.69 vs header 700.68, inside V-9's ± 0.02). SKILL.md §6 (packaged) says "split proportionally", but §2.3 says "as printed" and only `total_amount` is allowed to be computed ("else `net + vat` with a note"). ≈ 40 line cells hang on a derivation the spec does not state; Hydra's "0%" column (a discount) is a trap for `vat_rate`. | MEO raw 47, 135, 167, 173, 180; Hydra raw 3–8; Mobilize raw 20–34, 42–43; Via Verde raw 30–31; keys. | §2.3: "a line's `vat_amount` not printed per line = `net × the document's single rate` (or the rate printed on the line), rounded per line; `net = total − vat` when only a VAT-inclusive total and its VAT are printed; each with a note"; EXTRACT.md: a per-line percentage in a discount column is not the VAT rate. |
| U-7 | Medium | §2.8 V-8, V-9, V-12; §2.6 branch (2) | V-8 (`total = net + vat`) fails by design on any document printing net, VAT **and** stamp duty — fixture (1)'s own figures (100 + 23 ≠ 127) — and V-9 fails when a `stamp_tax` line is printed and the net excludes it; `reconciliation_note` is defined "non-null only when the document's own figures do not reconcile", which they do, so V-12 dead-letters an honest answer. Branch (2) also misfires when a printed stamp line already sits inside the net (net 104 incl. stamp 4, VAT 23, total 127 → (2) moves 4 from VAT to Net: 108 / 19 ≠ Σ lines 104). None of the 40 has these shapes (#6/#25/#31 have null VAT, no printed stamp lines), so the bench is unaffected; production is. | V-8/V-9/V-12 text; §7 fixture (1); §2.6 (2). | V-8: `total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02`; V-9 sums `item` lines only; (2) guarded by "and no printed `stamp_tax` line is inside `net` (Σ all lines ≠ net)". |
| U-8 | Low | S-2; §5.2 statistics | "unique across all 197 key movements" is false across statements: `(2025-09-26, 70,000.00, EUR)` is Revolut BM001918 (De FACTOR MATRIZ) and BCP #36's "TRF. P/O GOTT … 70 000.00" (raw 91). Pairing is per statement, so harmless. | python over the five keys; BCP raw 91. | "unique within each of the five statements". |
| U-9 | Low | S-6 R3 rule | "the day-of-month rule from `date_doc` alone" — on VFX `date_doc` is null; the rule runs on the derived `DateDoc` (§2.6 fallback = due date 2026-05-02 → 202604 = key). | `I26050010.json`; §2.6. | "from `DateDoc` (after the §2.6 fallback)". |
| U-10 | Low | §5.1 #7; §2.2 `date_doc` | Viajando prints "Data da fatura : 30 de mar. de 2026" and "Data de vencimento : 30 de mar. de 2026" in its invoice block and "Data da Invoice: 06/03/2026" in the observations; §5.1 names the conflict, no rule picks. Key 03-30. | Raw lines 23, 25, 27. | EXTRACT.md: the document's own invoice-date field wins over a date in free text; §5.1 #7 says which. |
| U-11 | Low | §2.1 `doc_type`; §4.2 EXTRACT.md row | `receipt` ↔ SKILL.md §5 "Invoice-Receipt" (a document that "already proves payment happened") is never stated as the mapping the model applies; `DocumentTypeRouter` maps it, and 6 `document_type` cells plus S-4/S-5 receipt handling rest on it (Stripe "Amount Due 0.00 / Debited from your Balance", eSIMGo's prepaid wallet, #25 "Recebemos o valor"). The packaged SKILL.md 95–155 carries the rule. | `DocumentTypeRouter.cs` 23–30; SKILL.md §5 at `a558523`; keys' `docType`. | One line in the EXTRACT.md row: "`receipt` = SKILL.md's Invoice-Receipt: the document proves its own payment (`payment_proof.kind ≠ none`)". |
| U-12 | Low | §2.6 "steps A–C change nothing" | The no-duty, printed-net case is not a numbered branch, and "change nothing" can be read as "return the input"; only the following sentence and S-3 make `VAT := VAT ?? 0` apply there (Viajando, BICS print no VAT figure or print 0). | §2.6 text. | "…change nothing except `VAT := VAT ?? 0`". |

#### Owner questions Q-EX-0..25

All 26 recommendations stand as written; no new question. Verified this round where an artefact decides: **Q-EX-6** — the 8 `vat_rate` cells are exactly Avis's six under "VAT @ 0%" and #25's two under "Isento de IVA", as the note says; **Q-EX-24** — #27's four invoices (32,23 / 3,02 / 0,21 / 0,01) and the 1,50 "Débitos/Créditos" item, "Devo pagar 36,97 até 25 agosto 2026"; #21's two (96340048 66,27; 1185418181 27,12) — the first-printed rule types the header, U-2 the lines; **Q-EX-25** — the fold reproduces #25 and the corrected #31 both ways from the printed figures; **Q-EX-20** — (f) reproduces Continente (raw 46–47), (a) reproduces MEO's 8 and Regus's 3 (page-2 detail 111 / 38 / 20 over the page-1 summary's 111 / 58); the utility clause of U-2 belongs under Q-EX-20's recommendation, not a new question; **Q-EX-23** — the intercompany consequence unchanged. U-1, U-3 are corrections rows; U-2, U-4 are spec sentences.

#### Readiness for RED oracles

Not yet, on four items, each one or two sentences or one CSV row. Cannot be typed from the spec alone: `document_id` on #16 (U-1); `line_count` and the line cells on #21 and #27, and #27's header net (U-2); `date_doc` on #21 (U-3); the nine AWS lines and golden #10 (U-4). Typeable now, figures recomputed by me: the accept corpus #2 (60.55 / 13.56 / 74.11; 3.62/0.47/4.09, 56.93/13.09/70.02), #6 (767.02 / 0.00; 634.72 / 132.33 / −0.03, `vat_rate` 0 on line 3), #25 and #31 both ways, Lari / eSIMGo / VFX / Alibaba, #35, #37 (270.03 → 619.22), #40 (40.06 → 164.94); the synthetic `Split` fixtures (1) 104/23, (1′) 106/23 + item 102, (2) 104/23, (3) 100 with Σ 104 and the 4.00 finding; the S-6 fixtures (R1 through `Decide`, R3 by day-of-month, a blank row) on every one of the 24 stamped rows; the occurrence-order fixture (A with A); S-4's three branches; the gate tests (#22 → GOTT/PT513362061, #23 → ITOO/IE3559150RH, #17 triage, #29 both branches; `SameTaxId` on "500 940 231"); the 18 description corrections and #31's. U-5…U-7 are one sentence each and can ride the same revision; U-8…U-12 are wording.

#### Verified myself

Worktree head `8ffd66d`, clean; the spec (506 lines) and the review record (1,076 lines: rounds 1–5 and every disposition block) in full. Skill build HEAD `a558523`, `status --porcelain` empty: SKILL.md §5 lines 95–155 (the Invoice-Receipt rule), §6 lines 156–162 (stamp and fold, line 160), schema.md lines 17 / 78 / 87 / 93 / 95 — the §2.2 (32), §2.3 (18), §2.4 (26), §2.5b (24) column lists match column by column. All 40 keys (python): shape, the 35 fiscal headers and 109 lines (17 zero-VAT headers, none null; 8 `vatRate` and 6 quantity/unit-price lines, all Avis/#25; 12 R1 / 12 R3 / 10 blank / 1 R2; 6 receipts with `dateDoc = dateDue = datePay`; `docType` per row); all 197 movements (no `(movDate, amount, currency)` tie within any statement; one across statements, U-8; the seven Revolut multi-movement days). All 40 PDFs located at `D:\fileStorage\repos\invoice-skill-build\<archivePath>`, SHA-256 = key hash on 40/40, dumped with `pdftotext -raw` and `-layout`; read in full or at the cited lines: #1 Telles (13 021,67 / 2 994,98 / 16 016,65, "Vencimento: P/Pagamento", no issuer NIF), #2 Continente, #3 Via Verde (totals and "IVA incluído" per sub-invoice), #5 Hydra, #6, #7 Viajando (three dates), #8 Mobilize, #9 Google ("Apr 30, 2026", "VAT (0%)"), #10 AWS (USD detail, EUR summary, the rate), #11 eSIMGo, #12 VFX, #13 Alibaba, #14 Anaptyxis, #15 Stripe, #16 Lari, #17 Regus, #18 MEO (period, VAT summary, the 8 category subtotals, "DATA DE EMISSÃO: 17-07-2026"), #19/#20/#26 (dates by label), #21 Águas do Porto (both invoices, all rows, both dates), #22 BICS ("15 AUG 2026", "before 14 SEP 2026", traffic period 202607, "VAT On 0.00"), #24/#35 Locarent, #25, #27 EDP (summary page, first invoice in full, the four ATCUDs), #28 eSIM Belgium ("Invoice-Receipt", "Document paid on this date: 31-01-2026"), #29 Itoorer, #30 Avis, #31, #32 BICS CN (date), #33 BastidorDistância (one printed row), #34 Farminvest, Revolut (balances, 15 Dec / 19 Aug / 10 Mar print order), BPI card (270,03 / 619,22), BCP (raw 91). #4 Awin has no text layer (1 raw line) — its cells rest on the key. Code on main `2c8e5f2`: `AccountPeriodService.Decide` and `R3` (lines 26–58), `CompanyMatcher` (private `SameTaxId`, `Bare`, `HasCountryPrefix`), `DocumentTypeRouter.DocumentTypeFor` (lines 23–30). Arithmetic: every `Split` trace and synthetic fixture; all 24 `account_period` cells; MEO/Hydra/Mobilize line VAT at 23 %; AWS conversions; EDP's 13.52 + 14.55 = 28.07 and 0.81 + 3.35 = 4.16.

#### Taken from the spec without verification

The CLI's flag semantics, `Read` truncation, `CLAUDE.md` discovery, Windows path-rule syntax, `--settings`/`--safe-mode` interaction; the .NET SDK csproj facts (round-4/5 feasibility evidence); token, context and sitting estimates; the module deployment/rollback steps, `migrate.ps1`, health checks 7 and 9, ACLs; `git archive` CRLF and the long-line counts (rounds 4–5); `build_bnkmov.py` 77–84, `bnk_statement_parsers.py`; `SyncEngineWave3.ToCashDelta`; `DocumentCaptureService` C6; the worker/queue/ingestion/web code cited in §4.3/§4.7 and the F-T seams (text only this round); the statement V-14 chains and the BPI-DO / BPI-USD / BCP printed balances (round 3); the 15 Revolut FX rows and the BPI fragment (round 4); the 31 Jan Revolut order (round 5); Stripe's and Via Verde's line-to-row correspondence beyond the totals; page counts; the Awin PDF's figures; the reserve entries and their doclog rows; `EXTRACT.md` (unwritten).

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** 8ffd66d (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main `2c8e5f2` at `D:\fileStorage\repos\GOTT.Apollo`; skill build HEAD `a558523`, clean)
**Verdict:** ACCEPT
**Counts:** Critical 0 / High 0 / Medium 2 / Low 5

Revision 6 closes all eighteen round-5 findings by text I checked against the worker, ingestion and web code on main, the pinned CLI's `--version`/`--help`, the deployment module's oracles and adapter source, the .NET 10.0.301 Worker SDK (the §4.6 csproj lines rebuilt and published in a throwaway project), the skill build at `a558523` and the 40 keys. Nothing in §4–§8 is unbuildable on this host. The two Mediums are places where a builder decides alone: §4.6/§4.7 describe a web release followed by a worker release, while the module verifies and activates web, api and worker as one release; and §7's `ClaudeCliPermissionTests` asserts an exact argument list and a generated `permissions.json` against a class that starts the real process, with no observing seam named.

#### Round-5 revisit

| id | State | Revision-6 text relied on; what I verified |
|---|---|---|
| C-T-1 | CLOSED | §2.6 "(0) … whenever `net_amount = null` and a total is printed — irrespective of `duty` and `other`", "In every branch, after the split, `VAT := VAT ?? 0`", Lari/eSIMGo/VFX named; S-3 "no header amount is null after the transform"; §7 #6 "VAT 0.00", Lari traced. Keys: I26030044 net 767.02 / vat 0.0 / lines 634.72, 132.33, −0.03; I26060014 net 372.3 / vat 0.0 / total 372.3. |
| C-T-2 | CLOSED | §2.3 `vat_rate` "a single document-level rate or exemption … is the `vat_rate` of every line"; §2.6 step A "`vat_rate = 0`"; Q-EX-6 note. Key I26080026: both lines `vatRate 0.0`, line 2 "Stamp Tax". (Key I26030044's stamp line has `vatRate null` — unscored, the §7 assertion is on the transform, not the score.) |
| C-T-3 | CLOSED | S-6 "the key's own rule applied to the extraction's inputs — R1 rows `Decide` from `service_period`; R3 rows the day-of-month rule from `date_doc`"; §3 #1 aligned. `AccountPeriodService.cs` 32–39: `Decide` returns `StatedPeriod` whenever `stated` is non-null, so the key's-rule wording is what makes MEO/Regus typeable. |
| C-T-4 | CLOSED | S-2 "occurrence order = the answer's `seq` among the tied rows against the key's `bm_code` ascending among the tied rows, in every `printOrder`"; §7 fixture "A pairs with A". Python over the five bank keys: 0 `(movDate, amount, currency)` collisions on any statement; Revolut `bmCode` ascending in file order. |
| C-T-5 | CLOSED | §2.6 "header `Net` equals Σ lines' `Net` after step C in branches (0)–(2); in branch (3) the difference is the DPRCHK finding"; §7 fixture (3) "Net 100.00 … Σ lines 104.00 … the 4.00 difference as the finding". |
| C-T-6 | CLOSED | §2.3 clause (h); §5.1 #12 "2"; §7 "VFX #12 (clause (h): lines 1,990.00 / 41.78)". Key I26050010: lines 1990.0 "Monthly digital marketing service", 41.78 "Late payment fee/interest"; net = total = 2031.78. |
| C-T-7 | CLOSED | §2.2 `total_amount` "on a receipt, the amount paid"; clause (i); §7 Alibaba. Key I26050030: 750 / 478 / 50 / 38.23, net = total = 1316.23, `lineCount 4`. |
| C-T-8 | CLOSED | §2.6 "`extra = (duty ?? 0) + (other ?? 0)`", "(1) `net + (vat ?? 0) + extra`", "(2) `net + (vat ?? 0) = total`". |
| C-T-9 | CLOSED | §2.3 clause (g) "the block's premium row — 'Prémio comercial' (#25: 382.82) or, absent that, 'Prémio antes de impostos' (#31: 137.65)". Text only. |
| C-T-10 | CLOSED | S-9 "`line_count` (**compared after `Split`** …: #6 3, #25 2, #31 2)"; §7 "`line_count` 3 after `Split`". Keys `lineCount` 3 / 2 / 2. |
| F-T-1 | CLOSED | §4.3 prompt "`<sandbox>/document<ext>`", "the **whole document path** is replaced by the `<document>` token … (as today's `DocumentToken`)"; §4.5 (a′) "the probe's input is a PDF"; §7 `.png` case with equal `PromptSha256`. `ClaudeDocumentProcessor.cs` 26 (`DocumentToken`), 38 (`PromptHash` hashes `PromptFor(DocumentToken)`, so the whole path is tokenised), 82 (`"document" + Path.GetExtension(sourceFile)`). Residual wording on the extension count: U-3. |
| F-T-2 | CLOSED | §4.6 "(1) `local\migrate.ps1 -Target All` … (2) web … (3) worker", "health check 9 (`ApiSchemaMatchesRelease`) fails and rolls back"; Rollback "needs no schema rollback". `migrate.ps1` 8 (`All` default, Main + Preview), 28–31 (both contexts); `publish-release.ps1` 39–44 (`requiredMigrations` = Platform migration files); `health.v2.json` check 9 "the body lists every required migration"; `execution-semantics.json` case `health-schema` → `RolledBack`, detail `SIBYLA-S2-HEALTH-9`, and `install-schema-refused` (Install-phase `Precondition.Schema` refuses before any tree is published). No `docint.hold_reason` exists in the Platform migrations today (grep; the `hold_reason` hits are Argus tables). Residual on "web release then worker release": U-1. |
| F-T-3a | CLOSED | §4.7 row 1 "or any hold with `result_json = null` (`timeouts:2`), which is hand-keyed like DeadLetter's 'Enter manually' today"; row 2 "decided by `result_json.doc_type = bank_statement`"; §7 "a `timeouts:2` hold with `result_json` null accepts a hand-keyed correction". `IngestionService.cs` 302 (accepted statuses today), 307 (`apollo.extract.v0` literal), 311–314 (DeadLetter → Processed); `Documents.razor` 176–178. |
| F-T-3b | CLOSED | §4.3 "`JobCompletion.CompleteAsync(conn, tx, leaseOwner, job, outcome)`", "`TimeoutLaneMonitor.Record(timedOut) → bool pause`", "`[InternalsVisibleTo("Sibyla.Tests.Platform")]`" on the worker; §7 timeouts row drives both. Worker has no `Properties/AssemblyInfo.cs` today (only `launchSettings.json`) — one is added; `tests/Sibyla.Tests.Platform/Sibyla.Tests.Platform.csproj` 25 already references the worker project. `QueueWorker.cs` 340 (`CompleteAsync` private), 164 (`PauseLaneAsync` private), 371–373 (`Hold` passes the Dead promotion untouched), 440 (audit action `job.{kind}` → `job.hold` for free). |
| F-T-4 | CLOSED | §4.3 "`public Guid? JobId { get; init; }` — nullable … an old element compares unequal to every job id"; §7. `ProcessingEvidence.cs` 37–41 (`ReadAll`, camelCase + null-ignore): a missing `jobId` deserialises to `null`. |
| F-T-5 | CLOSED | §4.3 "`CompanyGate` … in the worker with its `InternalsVisibleTo`"; "`Sibyla.Platform.Infrastructure`, whose `InternalsVisibleTo` (today `Sibyla.Tests.Platform`, `Sibyla.Tests.Browser`) adds `Sibyla.Tests.Argus` and `Sibyla.Tools.ExtractionBench`". `src/Sibyla.Platform.Infrastructure/Properties/AssemblyInfo.cs` 3–4 as stated; `Sibyla.Tests.Argus.csproj` 25–26 references Platform.Infrastructure and the worker. `CompanyMatcher.cs` 42 `SameTaxId` private today. |
| F-T-6 | CLOSED | §4.3 "`--settings <staging dir>/evidence/<jobId>/permissions.json`", "written outside every working directory"; §4.5; §7 "outside the sandbox". `provision-production.ps1` 179: the worker has Modify on `D:\ApolloData\staging`; `D:\ApolloData\staging\**` is itself on the §4.5 deny list, so the file is doubly unreadable by the tool; `--help` 2.1.259 `--restricted`: "managed settings and --settings still apply". |
| F-T-7 | CLOSED | §4.6 "the published tree is checked by `publish-release.ps1` as a post-publish step"; §7 "The published tree is **not** this test's". `publish-release.ps1` 30–37 is the per-host publish loop the step attaches to (no such step today — a script change). |
| F-T-8 | CLOSED | §4.3 "`WorkerOptions.StartupCheckSeconds` (default 10) … the test passes 1"; §7 "`StartupCheckSeconds = 1`". `WorkerOptions.cs` has no such key today (added). |

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| U-1 | Medium | §4.6 Deployment order (2)/(3), Rollback; §4.7 "Release order: web → worker"; §8 R-EX-3 "web released before worker" | The text reads as two releases (a web release, then a worker release by the module). The module accepts only a release carrying **all three** host trees and activates web, api and worker in one transaction; `publish-release.ps1` publishes all three by default. A web-only or worker-only release is refused at `Precondition.Artifact`. The intent (the v2 page is live before any v2 row exists) is met by construction in one release — web's install entries precede the worker's in the plan — but a builder must decide whether to attempt two. The Rollback sentence is wrong under the module: a worker rollback rolls the web back too, so v2 rows are **not** "readable by the web page (which reads by contract id)" during a rollback — the pre-v2 page shows status `9` (`StatusLabel` default arm) and a blank form; no data is lost (`hold_reason` nullable, the old `ReadAll` ignores unknown members). | `local/deploy/src/Sibyla.Deployment.Adapters.Windows/Adapters/FileSystem/FileSystemWindowsAdapterV2.cs` 181–189 (`release.json` + `ArtifactTree(root, Web/Api/Worker)`), `…/Sibyla.Deployment.Execution/Contracts/V2/ContractsV2.cs` 104 ("release.json and the three trees verified"); `execution-order.json` Install phase (`Host:Web` entries before `Host:Worker`); `publish-release.ps1` 10; run record §7o item 1 ("12/12", one transaction per release); `Documents.razor` 507. | "The v2 web page and the v2 worker ship in **one** release id (`publish-release.ps1`, all hosts); the module verifies the three trees and activates them in one transaction, web before worker in its plan — 'web before worker' means the same release, never a separate one. A rollback by the module returns web and worker together to the previous release; v2 rows then show as status 9 with a blank form until roll-forward; nothing is lost." Three sentences. |
| U-2 | Medium | §7 `ClaudeCliPermissionTests` | Two expected values have no seam: (i) "the argument list is exactly the §4.3 flag set in the §4.3 order" — `ClaudeCliProcess.RunAsync` builds `psi.ArgumentList` and starts `ResolvedPath` in the same method; (ii) "the generated `permissions.json` allows exactly the two roots and denies the enumerated list" — no type or method that produces the file is named. The existing argument-capture pattern (`CliProcessRunnerTests` + `Fixtures/EchoArgument.ps1`) goes through `ICliProcessRunner`, which the worker does not use. Expected values are typeable; how the test reaches them is not. | `ClaudeCli.cs` 76–98, 122–132; `tests/Sibyla.Tests.Platform/CliProcessRunnerTests.cs` 38–41; `src/Sibyla.Platform.Infrastructure/Ai/CliProcessRunner.cs` 37. | Name the seams: `internal static IReadOnlyList<string> ClaudeCliProcess.BuildArguments(prompt, jobDir, packageDir, settingsPath, model, effort, outputFormat)` (called by `RunAsync`) and `internal static string PermissionsFile.Build(sandbox, package, releaseDir)` (the JSON the worker writes) — the test asserts on both under the worker's `InternalsVisibleTo`; or route the worker through `ICliProcessRunner` with a capturing fake. One sentence in §4.3, one in §7. |
| U-3 | Low | §4.3 "the source's extension is kept (`.pdf`, `.png`, `.jpg`, `.tiff`; `DocumentContentInspector` admits all four)" | The intake admits nine extensions: `.pdf .png .jpg .jpeg .tif .tiff .xml .txt .csv`. The `document<ext>` rule covers all nine; only the count is wrong. | `IngestionService.cs` 27–28 (`AllowedExtensions`); `DocumentContentInspector.cs` 36–46 (`ExpectedMime`). | "every extension `IngestionOptions.AllowedExtensions` admits (nine today)". |
| U-4 | Low | §4.7 re-extract key; §7 "the second re-extract gets key `…:<contract>:3`" | "n = the count of existing jobs for the row + 1" — every intake row also has a `storage-transfer` job (`docint:<id>:transfer`), so a literal count gives `:3` for the **first** re-extract and `:4` for the second, contradicting §7's `:3`. | `IngestionService.cs` 199–235 (both jobs added per intake), 455 (`ReleaseHeldProcessing` re-adds the fixed process key). | "n = the count of existing `document-processing` jobs for the intake (by `payload_json->>'intakeId'`) + 1". |
| U-5 | Low | §5.7 bench `--output-format stream-json` | Earlier CLI generations refused `-p --output-format stream-json` without `--verbose`; 2.1.259's `--help` is silent on it and I did not run the CLI on input. If it applies, the bench's flag set (already distinct from production by `stream-json`) needs `--verbose`. | `claude.exe --help` 140–147, 255–256. | R-EX-2 probe (b) records whether `--verbose` is required; if so it joins the bench-only flags, the production set unchanged. |
| U-6 | Low | §4.3 start-up checks "a stopped service with a failure exit" | The service carries SCM recovery actions `restart/10000/restart/60000/restart/300000`, so a mismatched worker restarts in a loop (10 s, 60 s, then every 300 s) rather than staying stopped. Health check 7 ("same pid for 20 s") still fails it, so the guard holds; the wording does not. | `provision-production.ps1` 220; `health.v2.json` check 7. | "…exits non-zero; the SCM's recovery actions restart it, so the mismatch shows as a restarting service that never holds a pid for 20 s (health check 7)". |
| U-7 | Low | §5.7 `tools/extraction-bench/` (and §4.2 `tools/skill-package/`) | `local\test.ps1` is `dotnet test Sibyla.slnx`; a project outside the solution is never compiled by the suite, so a bench that no longer builds goes unnoticed until a sitting. No `tools/` folder exists at the root today. | `local/test.ps1` 4; `Sibyla.slnx`; `ls tools` → none. | Add `tools/extraction-bench/Sibyla.Tools.ExtractionBench.csproj` to `Sibyla.slnx` under a `/tools/` folder (built, no tests), or state the bench build command beside the sitting plan. |

#### Owner questions Q-EX-0..25

All 26 recommendations are buildable as written on this host; no new question. Verified where an artefact decides: **Q-EX-15** — `C:\Apps\Sibyla\tools\claude\claude.exe --version` → `2.1.259 (Claude Code)`, exit 0; `--model` accepts a full name, `--effort` choices `low, medium, high, xhigh, max`, `--fallback-model` exists (never passed); `ResolveCliExecutable` keeps an explicit existing path (`ClaudeCli.cs` 122–127). **Q-EX-21** — every flag of the §4.3 set is in `--help` (`-p`, `--output-format`, `--add-dir`, `--tools`, `--restricted`, `--safe-mode`, `--strict-mcp-config`, `--disable-slash-commands`, `--permission-prompts`, `--settings`, `--no-session-persistence`, `--model`, `--effort`); `--restricted` says "managed settings and --settings still apply" and "confines the file tools to the working directories (--add-dir included)"; `--safe-mode` is silent on `--settings`, as the spec records for R-EX-2. **Q-EX-16** — the module ships web and worker in one release, so "released before the worker" holds by construction (U-1 wording). **Q-EX-18** — `ix_jobque_idempotency_key` is UNIQUE (`20260827172608_IntakeAndQueue.cs` 88–92); per-job evidence folders plus nullable `JobId` make "its element of `evidence_json`" identifiable; the count definition is U-4. **Q-EX-22** — nothing in this review read `D:\ApolloData\worker-claude`. **Q-EX-6/20/24/25** are contract questions; their §7 fixtures carry figures the keys hold (verified for #6, #12, #13, #25, #31, #2, #16).

#### Readiness for RED oracles

Ready. Every §7 row names an expected value, and every seam it needs either exists on main or is named in §4.3, with two builder decisions left (U-1 is deployment wording, not an oracle; U-2 needs two seam names before `ClaudeCliPermissionTests` is typed — one sentence). Typeable now from the spec and the artefacts: `ExtractionContractV2Tests` (goldens #2/#6/#16/#12/#13/#25/#31 figures verified against the keys), `SkillPackageTests` (headings at SKILL.md 16/95/156/162/410/581/595, long lines 4/5/1 recomputed, `i/lf w/crlf`, the seven rule files and `Specs/vat_rates.json` present at `a558523`; the csproj lines publish `skill\` with its `.json` files and fail NETSDK1022 without `Exclude` — rebuilt), `WorkerStartupTests` (first token of `2.1.259 (Claude Code)`; `StartupCheckSeconds = 1`), `ProcessingEvidenceTests` (nullable `JobId`; `PromptSha256` equal for `.pdf`/`.png` because the whole path is tokenised), `ExtractionResultIdentityTests`, `QueueWorkerGateTests` through `CompanyGate.ApplyAsync` (companies `{CONF PT502947721, FMAT PT509428428, GOTT PT513362061, ITOO IE3559150RH, SILA PT518198499, VIGA PT519031563}` from the keys; `SameTaxId` on "500 940 231" ↔ "PT500940231" per `CompanyMatcher.cs` 42–50), `DocumentsReviewV2Tests` (`RenderSmokeTests` 277 already seeds a `docint` row by SQL and renders `/documents`), `IngestionServiceHoldTests` (all seven transitions incl. the null-result hold), `ClaudeCliTimeoutTests` (attempt table; `jobque.state = 2`, `last_error`, `job.hold` — the fence at `QueueWorker.cs` 347–361 needs the seeded lease row), `ClaudeCliPermissionTests` (values typed; seams U-2), `EvidenceSecretScanTests`, `CompanyMatcherTests`/`AccountPeriodTests` (both exist), and every `ExtractionScoringTests` fixture (natural key unique on all 197 movements; Revolut `bmCode` ascending; `Split` figures recomputed against the keys).

#### Verified myself

- Worktree head `8ffd66d`, clean; the spec (506 lines) and the review record (1,076 lines: rounds 1–5, all dispositions) in full.
- Main `2c8e5f2`: `WorkerOptions.cs` (`ClaudeCliPath = "claude"`, 300/2/240/2/10/24, no version/model/effort/startup key), `ClaudeCli.cs` (3-arg `RunAsync`; `-p`, `--output-format json`, `--add-dir <jobDir>`, `--allowedTools Read`; timeout → `(-1, …)`; 20 s `--version` cap swallowing failures; npm fallback), `ClaudeDocumentProcessor.cs` (`DocumentToken` 26, `PromptHash` 38, sandbox copy `document<ext>` 82, evidence beside the source `attempt-N-*` with overwrite 109–112/145, `ExitCode` into the element), `QueueWorker.cs` (claim states 0/1 197–198/206–207, renewal `max(10, Lease/3)` 139, `PauseLaneAsync` private 164, gate private static 268 on top-level `recipient_tax_id`, duplicate → status 6 in-transaction 324, `CompleteAsync` private 340 with the fence 347–361, per-kind rows 376–424, evidence append 428–433, audit `job.{kind}` 440, `JobOutcomeKind` Success/Retry/Dead 506), `ClaimedJob.cs`, `QueueJob.cs` (states 0–4, `MaxAttempts = 5`), `Program.cs` (`AddWindowsService`, `BindConfiguration("Worker")`, secrets file added after `appsettings.json`, `host.Run()` uncaught), the worker csproj (Sdk.Worker, no items) and `appsettings.json` (no `Worker` section — `Worker:ClaudeCliPath` lives in `worker.json` per run record line 72, not read), `DocumentIntake.cs` (enum ends at `Quarantined = 8`; `ResultJson`/`EvidenceJson`/`CorrectedJson`; no hold reason), `DocumentContentInspector.cs` 36–46 and `IngestionService.cs` 27–28 (nine extensions), `IngestionService.cs` 199–235, 293–330, 438–458, 463–486, 520–600, `ChannelIntakeService.cs` 288 (candidate status fixed from the upload outcome), `ProcessingEvidence.cs`, `CompanyMatcher.cs`, `ExtractionContract.cs` 26, `Documents.razor` (`/documents`, evidence modal via `ReadAll`, DeadLetter "Enter manually" 176–178, `StatusLabel` default arm 507, v1 field names), `DocumentTypeRouter.DocumentTypeFor` 23–31, `DocumentCaptureService.cs` 42–45 (C6 refusal), `AccountPeriodService.Decide` 26–45, both `AssemblyInfo.cs`, Platform migrations (list; `ix_jobque_idempotency_key` unique 88–92; no `docint.hold_reason`), `local/migrate.ps1`, `local/publish-release.ps1`, `local/test.ps1`, `local/provision-production.ps1` 150–226 (worker ReadAndExecute on `worker`/`tools` 176–177, Modify on `staging`/`work` 179/185, SCM env 224, recovery actions 220), the six test projects' references, `ProcessingEvidenceTests` (DB-backed, `CannedCli`), `CliProcessRunnerTests` 38–41, `RenderSmokeTests` 266–284, `Sibyla.slnx`.
- Deployment module: `Invoke-SibylaDeployment.ps1` params (`-Execute`, `-Resume`, `-ChecksOnly`, `-Force`, `-NoRollbackOnHealth`), `health.v2.json` checks 7 and 9, `execution-order.json` phases, `execution-semantics.json` (`install-schema-refused`, `health-schema` → RolledBack/HEALTH-9, `artifact-refused`), `FileSystemWindowsAdapterV2.cs` 181–210 and 222–232 (three trees, ordinal tree hash), `ContractsV2.cs` 47–58, 104–106; run record §7o in full.
- .NET SDK 10.0.301: `Microsoft.NET.Sdk.Worker.props` 22–38 (`**\*.json` Content with `CopyToPublishDirectory`, `None Remove`), `Microsoft.NET.Sdk.Worker.targets` 15; **the two §4.6 csproj lines built and published in a scratchpad Worker-SDK project** (`out\skill\{EXTRACT.md, SKILL.md, manifest.json, references\vat_rates.json}` present, exit 0) and the same project without `Exclude` failing NETSDK1022.
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: `--version` → `2.1.259 (Claude Code)`, exit 0; `--help` for every flag named above. Never run on any input.
- Skill build at `a558523`: HEAD, `status --porcelain` empty, `ls-files --eol` = `i/lf w/crlf`; headings at 16/95/156/162/410/581/595; long lines SKILL.md 89/164/228/408 (2251/2986/3244/1993), schema.md 17/78/87/93/95 (3206/3368/8014/3326/5681), Entry Policy 53 (2116); the seven §4.2 rule files and `Specs/vat_rates.json` present; SKILL.md §6 156–162.
- Keys (read-only, python): 42 entries (35 fiscal + 5 bank + `INDEX.md` + `_sql`); fiscal top-level `fileHash`/`lineCount`/`accountPeriodRule`/…; bank `statementDoclog.fileHash`, `movements[]` with `bmCode`/`movDate`/`docDate`/`amount`/`currency`/`runningBalance`; `(movDate, amount, currency)` unique on every statement; Revolut `bmCode` ascending; six companies; headers and lines of #6, #25, #31, #2, #16, #12, #13.

#### Taken from the spec without verification

CLI runtime behaviour not exercised: the `Read` 2,000-character truncation, the `json` envelope's `num_turns`/`usage`/`modelUsage`, the `stream-json` file-read trace and whether it demands `--verbose` (U-5), the Windows `Read(...)` rule syntax and deny-over-allow precedence, whether `--settings` applies under `--safe-mode`, the usage-limit message the bench parses; that a `StartAsync` throw under `WindowsServiceLifetime` exits the process non-zero (plausible from `host.Run()` being uncaught; not run); token/context estimates and the sitting arithmetic; that the release gate's fingerprint is taken from a clean export (Slice 2 C2/E2 prompts say so; the module hashes the staged tree); the PDFs' printed figures (rounds 3–5 verified them; I checked keys, not PDFs, this round); `build_bnkmov.py` 77–84; `SyncEngineWave3.ToCashDelta`; page counts; the content of `EXTRACT.md` (unwritten); the reserve entries' doclog rows; the "2 otherwise column-sliced" Revolut rows.

### Author's dispositions — revision 7

Revision 7 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 6 (feasibility ACCEPT on revision 6; contract REVISE). Ids: `C-U-n` = the contract reviewer's U-1…U-12, `F-U-n` = the feasibility reviewer's U-1…U-7. Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 7. The changes are surgical: §4–§8, which feasibility verified, are touched only where its own findings and the contract reviewer's rules require. Decisions the coordinator took for this revision are written in and not re-opened; no new owner question.

**Contract reviewer (C-U-1 … C-U-12)**

| id | Sev | Disposition | Where in revision 7 |
|---|---|---|---|
| C-U-1 | High | fixed — `I26060014 document_id 202605` added to N-7's closed placeholder list and to `answer-key.flags.json` `placeholders` → expected `null`; §5.1 #16 "no document number printed"; the §7 fixture names it | §2.7 N-7; §5.1 #16; §7 |
| C-U-2 | High | fixed — clause (j): a utility bill (water, electricity, gas) is one `item` line per invoice (the invoice's "Total sem IVA" / VAT / total), its consumption, tariff, availability and tax rows (TRH, DGEG, IEC) summarised in `evidence.notes`; its header `net_amount` is the VAT block's base sum where the sectional subtotal excludes VAT-bearing tax rows (#27: 28.07 = 13.52 + 14.55, not 27.93; VAT 4.16 = 0.81 + 3.35); #21 (62.51 / 3.76 / 66.27, 1 line) and #27 (28.07 / 4.16 / 32.23, 1 line) typed in §7; Q-EX-20's recommendation gains the clause; clause (a) does not apply to a utility bill | §2.3 clause (j); §1.2 Q-EX-20; §4.2; §5.1 #21; §7 |
| C-U-3 | High | fixed — pre-declared correction `I26070079 dateDoc 2026-07-14 → 2026-07-22 (p.2 "Data de Fatura")`; a blank-rule row, so `account_period` is unaffected | §5.5; §5.1 #21 |
| C-U-4 | High | fixed — §2.3: when the line table is printed only in a secondary currency and the document states the conversion rate, line amounts are that rate applied per line, rounded to 2 dp, in the document's currency, flagged "converted at printed rate"; the header stays as printed; V-9's ± 0.02 absorbs the rounding; golden #10 typed to the nine EUR lines (509.01 → 439.48, 117.07 → 101.08, …; Σ net 448.87 vs 448.88) | §2.3 `net_amount`; §4.2; §5.1 #10; §7 golden row |
| C-U-5 | Medium | fixed — §2.2: when the header prints no period and every line prints the same one, the header `service_period` is that period (Locarent #24/#35); S-6 uses it | §2.2 `service_period`; §5.3 S-6; §7 golden #35 |
| C-U-6 | Medium | fixed — §2.3: a line's `vat_amount` not printed per line = `net × the document's single rate` (or the rate printed on the line), rounded per line; `net = total − vat` when only a VAT-inclusive total and its VAT are printed; each with a note; `EXTRACT.md`: a per-line percentage in a discount column is not the VAT rate (Hydra); MEO, Hydra, Mobilize, Locarent, Anaptyxis, Via Verde named | §2.3 `net_amount`, `vat_amount`; §4.2 |
| C-U-7 | Medium | fixed — V-8 = `total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02`; V-9 sums `item` lines only; branch (2) guarded by "and no printed `stamp_tax` line is inside `net` (Σ all lines ≠ net)"; noted that none of the 40 has these shapes (synthetic fixtures only) | §2.8 V-8, V-9; §2.6 branch (2) and the no-duty sentence |
| C-U-8 | Low | fixed — "unique within each of the five statements" (the cross-statement Revolut/BCP 70,000.00 pair named as harmless, pairing being per statement) | §5.2 statistics; §5.3 S-2 |
| C-U-9 | Low | fixed — S-6's R3 rule "from `DateDoc` (after the §2.6 fallback)", VFX traced (202604) | §5.3 S-6 |
| C-U-10 | Low | fixed — `EXTRACT.md`: the document's own invoice-date field wins over a date in free text; §5.1 #7 says 2026-03-30 | §4.2; §5.1 #7 |
| C-U-11 | Low | fixed — `EXTRACT.md` row: `receipt` = SKILL.md §5's Invoice-Receipt, the document proves its own payment, `payment_proof.kind ≠ none` | §4.2 |
| C-U-12 | Low | fixed — "change nothing except `VAT := VAT ?? 0`" (Viajando, BICS) | §2.6 |

**Feasibility reviewer (F-U-1 … F-U-7)**

| id | Sev | Disposition | Where in revision 7 |
|---|---|---|---|
| F-U-1 | Medium | fixed — the v2 web page and the v2 worker ship in one release id (`publish-release.ps1`, all hosts); the module verifies the three trees and activates them in one transaction, web before worker in its plan; "web before worker" means the same release, never a separate one (a partial release is refused at `Precondition.Artifact`); a module rollback returns web and worker together to the previous release, v2 rows then showing as status 9 with a blank form until roll-forward, nothing lost | §4.6 Deployment order (2), Rollback; §4.7 release order; §8 R-EX-3 |
| F-U-2 | Medium | fixed — `internal static IReadOnlyList<string> ClaudeCliProcess.BuildArguments(prompt, jobDir, packageDir, settingsPath, model, effort, outputFormat)` called by `RunAsync`, and `internal static string PermissionsFile.Build(sandbox, package, releaseDir)`; `ClaudeCliPermissionTests` asserts on both under the worker's `InternalsVisibleTo` | §4.3 seams; §7 permissions row |
| F-U-3 | Low | fixed — "every extension `IngestionOptions.AllowedExtensions` admits (nine today: `.pdf .png .jpg .jpeg .tif .tiff .xml .txt .csv`)" | §4.3 |
| F-U-4 | Low | fixed — n = the count of existing document-processing jobs for the intake (by `payload_json->>'intakeId'`) + 1; the first re-extract is `:2`, the second `:3`; §7 aligned | §4.7 Re-processing; §1.2 Q-EX-18; §7 holds row |
| F-U-5 | Low | fixed — R-EX-2 probe (b) records whether `-p --output-format stream-json` needs `--verbose`; if so it joins the bench-only flags, the production set unchanged | §4.5 probe (b) |
| F-U-6 | Low | fixed — the SCM recovery-actions wording: a mismatched worker restarts under `restart/10000/restart/60000/restart/300000` and never holds a pid for 20 s, which health check 7 fails | §4.3 start-up checks |
| F-U-7 | Low | fixed — `Sibyla.Tools.ExtractionBench` joins `Sibyla.slnx` under a `/tools/` folder, built by the suite, no tests | §5.7 |

Owner questions after revision 7: Q-EX-0…25 unchanged in number; Q-EX-20's recommendation gains clause (j). No new question.

## Round 7 — revision 7, head `64e1f89` (2026-09-06)

| Reviewer | Verdict | Critical / High / Medium / Low | Round-6 findings closed |
|---|---|---|---|
| contract | REVISE | 0 / 1 / 1 / 4 | 19 of 19 (round-6 traces re-confirmed) |
| feasibility | ACCEPT | 0 / 0 / 2 / 3 | 19 of 19 (diff 8ffd66d..64e1f89 checked hunk by hunk; RED oracles ready) |

Contract W-1 (High): on the converted AWS lines, the printed USD total of line 5 converts to 11.47 while the key holds 11.46 = converted net + converted VAT — the rule must define the converted total as the sum. Both reviewers independently raise the same Medium: the revised V-8/V-9 reject a printed stamp line inside the net, the very shape the branch-(2) guard serves (contract W-2 = feasibility W-2). Feasibility W-1: `PermissionsFile.Build` needs the sibling releases as an input. Ids are per reviewer (C-W-n, F-W-n).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** 64e1f89 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; diff 8ffd66d→64e1f89 = 37+/35− on the spec, read word by word; code read on main `86820d6`; skill build HEAD `a558523`, `status --porcelain` empty; all 35 fiscal PDFs located at their key `archivePath`, SHA-256 = key `fileHash` on 35/35)
**Verdict:** REVISE
**Counts:** Critical 0 / High 1 / Medium 1 / Low 4

Revision 7 closes all 19 round-6 findings by text I checked against the PDFs, the keys and the code: Lari's placeholder, clause (j) on both utility bills, the #21 date correction, the AWS conversion, Locarent's header period, the per-line VAT/net derivations (every derived cell recomputed = key), V-8/V-9/branch (2), and the seven feasibility items. The round-6 confirmations still hold after the edits. One cell the new conversion rule does not type: AWS line 5's `total_amount` — the printed USD 13.28 converts to 11.47, the key holds 11.46 (= 9.32 + 2.14); the other two converted totals coincide by rounding luck. One sentence fixes it. The Medium is the production shape the branch-(2) guard was written for, which the revised V-8/V-9 now reject before `Split` runs.

#### Round-6 revisit

| id | State | Revision-7 text relied on and what I verified |
|---|---|---|
| C-U-1 | CLOSED | N-7 "**Lari I26060014 `202605`** — the reference period, printed nowhere"; §5.1 #16 "no document number printed"; §7 "Lari #16 (`document_id` null against the placeholder key)". PDF raw 1–14: "INVOICE ORBISTER", "Data da Invoice: 3 de Junho de 2026", "Mês de Referência: Janeiro … Maio", "$ 372,30" — no number; key `documentId: "202605"`. |
| C-U-2 | CLOSED | Clause (j) in §2.3; Q-EX-20; §4.2; §5.1 #21/#27; §7. Águas do Porto raw 132–158: ÁGUA 32,83 / ÁGUAS RESIDUAIS 28,92 / TAXA DE RECURSOS HÍDRICOS 0,76 / "(2)IVA 6% … 62,51 3,76" / "Total sem IVA 62,51" / "TOTAL 66,27" → 1 line 62.51/3.76/66.27 = key. EDP raw 119/125/131/134–138: 13,52 / 14,41 / "A Total 27,93 sem IVA" / DGEG 0,07 / IEC 0,07 / "IVA (13,52) 6% 0,81" / "IVA (14,41 + 0,07 + 0,07) 14,55 23% 3,35" → 28.07 / 4.16 / 32.23 (line 96 "Valor: 32,23") = key `I26080035.json` 1 line, `lineCount` 1. |
| C-U-3 | CLOSED | §5.5 "**I26070079 `dateDoc` 2026-07-14 → 2026-07-22** (p.2 'Data de Fatura' … blank-rule row)"; §5.1 #21. Raw 7–8 "Emissão: 2026-07-22", 106–107 "Data de Fatura 2026-07-22", 30 (conta-corrente "Fatura 96340048 … 2026-07-14 66,27"); key `dateDoc 2026-07-14`, `accountPeriodRule null`. |
| C-U-4 | CLOSED (residual W-1) | §2.3 `net_amount` "a line table printed only in a secondary currency … converted per line at that rate, rounded to 2 dp … header stays as printed; V-9's ± 0.02 absorbs the rounding"; §5.1 #10; §7 golden #10. AWS raw 22 (1 USD = 0.86340951809 EUR), 27–37, 48–51. Recomputed (Decimal, half-up): 509.01→439.48, 117.07→101.08, 10.80→9.32, 2.48→2.14, 0.08→0.07, 0.02→0.02; Σ net 448.87 vs header 448.88, Σ VAT 103.24 = header — all nine key lines' net/VAT reproduced. Line totals: 626.08→540.56 = key, 0.10→0.09 = key, **13.28→11.47 ≠ key 11.46** (W-1). |
| C-U-5 | CLOSED (wording W-5) | §2.2 "when the header prints no period and every line prints the same one, the header period is that period"; S-6 "Locarent's R1 rows take the header period §2.2 derives"; §7 golden #35. Locarent #24 raw 25–35: all seven lines carry "Prestação nº 74 (01/08/2026 - 31/08/2026)", the header block (13–18) none. `AccountPeriodService.Decide` (main 86820d6, lines 32–40): `advance = Start > dateDoc` → 08-01 > 08-01 false → End month → 202608 = key R1 202608. |
| C-U-6 | CLOSED | §2.3 `net_amount` ("`net = total − vat` … Via Verde") and `vat_amount` ("`vat = net × the document's single rate` (or the rate printed on the line), rounded per line — MEO, Hydra, Mobilize, Locarent, Anaptyxis"); §4.2 "a per-line percentage in a discount column is not the VAT rate — Hydra". Recomputed round(net × 0.23) per line: MEO 108.20/33.84/1.84/39.23/45.21/7.82/455.77/8.78 (Σ 700.69 vs header 700.68), Hydra 41.60/5.38/1.17, Mobilize 65.31/14.27/3.56, Locarent #24 100.81/0.55/22.65/0.08/4.27 (rate column 23/0 printed per line, raw 25–35), #35 41.04/7.69/0.25/1.78/0.06, Anaptyxis 115.00/85.10/98.90/103.50/149.50/34.50/59.80 — every value = the key. Via Verde raw 30–31 "Total em Portagens 4,05 / IVA incluído 0,76" → 3.29; the five nets 3.29/1.54/1.46/1.87/0.24 = key. Hydra raw 3–5 "Dsct. 0%" column, raw 8 "23% IVA 48,15" (document level). MEO raw 47 "IVA 23,00 % 3 046,42 700,68", category subtotals at 135/167/173/180. |
| C-U-7 | CLOSED (residual W-2, W-6) | V-8 "`total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02`"; V-9 "**`item` lines only**, a printed `stamp_tax` line excluded"; §2.6 (2) "**and no printed `stamp_tax` line is inside `net`** (i.e. Σ all lines ≠ net)"; "None of the 40 prints net, VAT and a stamp line together". Fixtures under the revised V-8: (1) 100+23+4 = 127 ✓, (1′) 100+23+4+2 = 129 ✓, (2) 100+27+4 = 131 ≠ 127 — passes only because V-8 needs both net and vat non-null and the fixture is a `Split` fixture, not a validation one; the shape the guard serves fails V-8/V-9 (W-2). |
| C-U-8 | CLOSED | §5.2 "unique **within each of the five statements** … one pair coincides across statements — Revolut's 2025-09-26 +70,000.00 and BCP's"; S-2 "(pairing is per statement)". Python over the five keys: 0 `(movDate, amount, currency)` collisions within 46/10/3/87/51; exactly one cross-statement pair `('2025-09-26', 70000.0, 'EUR')` BCP + Revolut. |
| C-U-9 | CLOSED | S-6 "from `DateDoc` alone (after the §2.6 fallback — VFX's `date_doc` is null, its `DateDoc` the due date 2026-05-02 → 202604)". Recomputed the day-of-month rule on all 12 R3 rows from the key `dateDoc` (VFX's key `dateDoc` = due 2026-05-02): 12/12 = key. |
| C-U-10 | CLOSED | §4.2 "the document's own invoice-date field wins over a date in free text (Viajando #7 …)"; §5.1 #7 "→ `date_doc` 2026-03-30". Raw 23 "Data da fatura : 30 de mar. de 2026", 25 "Data de vencimento : 30 de mar. de 2026", 27 (observations) "Data da Invoice: 06/03/2026"; key 2026-03-30. |
| C-U-11 | CLOSED | §4.2 "**`receipt` = SKILL.md §5's Invoice-Receipt: the document proves its own payment, `payment_proof.kind ≠ none`**". SKILL.md line 97 at `a558523` ("`Invoice-Receipt` if it already proves payment happened … exactly the set with `DatePay`"); `DocumentTypeRouter.DocumentTypeFor("receipt") = "Invoice-Receipt"` (main line 26); 6 receipt keys all `dateDoc = dateDue = datePay`. |
| C-U-12 | CLOSED | §2.6 "steps A–C change nothing except `VAT := VAT ?? 0` (Viajando, BICS)". |
| F-U-1 | CLOSED | §4.6 (2) "**One release for web and worker** … one release id … activates them in one transaction … a web-only or worker-only release is refused at `Precondition.Artifact`"; Rollback "returns **web and worker together** … status 9 with a blank form until roll-forward"; §4.7 "web before worker inside the one release"; §8 R-EX-3. Text only; the module not re-read (feasibility's artefact). |
| F-U-2 | CLOSED | §4.3 "**`internal static IReadOnlyList<string> ClaudeCliProcess.BuildArguments(prompt, jobDir, packageDir, settingsPath, model, effort, outputFormat)`** … **`internal static string PermissionsFile.Build(sandbox, package, releaseDir)`**"; §7 permissions row "asserting on the static seams". Text only. |
| F-U-3 | CLOSED | §4.3 "every extension `IngestionOptions.AllowedExtensions` admits (nine today: …)". `IngestionService.cs` 27 on main declares `IngestionOptions.AllowedExtensions` (the nine not re-counted). |
| F-U-4 | CLOSED | §4.7 "n = the count of existing **document-processing** jobs for the intake (by `payload_json->>'intakeId'`, `job_type = document-processing` — the row's `storage-transfer` job does not count) + 1, so the first re-extract is `:2` and the second `:3`"; Q-EX-18; §7 "`…:<contract>:2` and the second `…:3`". Text only. |
| F-U-5 | CLOSED | §4.5 probe (b) "records **whether `-p --output-format stream-json` needs `--verbose`** … joins the bench-only flags, the production set unchanged". Text only. |
| F-U-6 | CLOSED | §4.3 "the SCM's recovery actions (`restart/10000/restart/60000/restart/300000`, `provision-production.ps1`) restart it … **restarting service that never holds a pid for 20 s** — which health check 7 fails". Text only. |
| F-U-7 | CLOSED | §5.7 "**added to `Sibyla.slnx` under a `/tools/` folder** so the suite builds it (no tests)". No `tools/` in the worktree today (expected before RED). |

**Round-6 confirmations re-checked after the edits (all hold):** `Split` #6 (raw 39/41/46/…/56/67: TOTAL 634,72 / 132,33, footnote "ajuste no Imposto de Selo … − 0,03", "Total(*) 767,02", no net/VAT) → (0) 767.02 / 0.00 / lines 634.72, 132.33, −0.03 = key; #25 (raw 42–48, 33: 382,82 / "Prémio antes de impostos 382,82" / 19,15 / 9,57 / 28,72 / 411,54; 35 "Isento de IVA") → (1) 382.82+0+19.15+9.57 = 411.54 → item 392.39 / stamp 19.15 = key; #31 (raw 32–35, 40: 137,65 / 12,38 / 5,37 / 17,75 / 155,40) → (1) 155.40 → −155.40, −143.02 / −12.38 = the §5.5 correction. Clause (f) #2 (raw 46–47 "(B) 13,00% 3,62 0,47 4,09 / (C) 23,00% 56,93 13,09 70,02" → 60.55/13.56/74.11 = key). Clause (h) #12 (raw 13/15/20–21: 2.031,78 / 1.990,00 / "R$ 39,80 de multa + R$ 1,98 de juros" → 41.78 = key). Clause (i) #13 (raw 32–41: 750 / 478 / 50 / "Order total 1,278.00" / "Payment processing fee 38.23" / "Amount paid 1,316.23" = key, 4 lines). `vat_rate`: Avis layout 40 "VAT @ 0% 0,00 12.676,92" with "EUS" per row → 6 × 0 = key; #25 "Isento de IVA" → both lines 0 = key (the 8 `vatRate` cells are exactly these, python). S-6 on the 24 stamped rows: 12 R3 recomputed from `DateDoc` = key; 12 R1 through `Decide` from the printed periods (Hydra 20-02..19-03 / DateDoc 12-03 → 202603; #6 01/05..01/06 advance → 202605; Google "Fee for April 2026" → 202604; AWS "April 1 - April 30, 2026" → 202604; eSIMGo "From 01/04/2026 … to 30/04/2026" → 202604; Stripe "Service Month May 2026" → 202605; Lari Jan–May → 202605; BICS ×2 "Traffic period 202607" / DateDoc 08-15 → 202607; Locarent → 202608; #25 09-01..09-30 advance → 202609; Itoorer "from 03/2026" → 202603) = key on all 12. S-4's 5 + 2: regex over the key notes gives exactly I26020038/I26030044/I26040053/I26050001/I26060014 ("+30") and I26030019/I26030031 ("set equal to DateDoc").

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| W-1 | High | §2.3 `net_amount` (secondary-currency clause) vs `total_amount` ("printed, else `net + vat`"); §7 golden #10 | AWS prints every line in USD, total, net and VAT (EC2 "USD 626.08 / Charges 509.01 / VAT 117.07"; VPC "USD 13.28 / 10.80 / 2.48"; Data Transfer "USD 0.10 / 0.08 / 0.02"). The clause converts "a line table … per line at that rate, rounded to 2 dp", and `total_amount` is "printed" — so line 5's total is 13.28 × 0.86340951809 = 11.4661 → **11.47**; the key holds **11.46** (= 9.32 + 2.14). The other two totals coincide (626.08 → 540.56 = 439.48 + 101.08; 0.10 → 0.09 = 0.07 + 0.02), so a builder typing golden #10 "to the key" contradicts the rule on one cell and a builder following the rule mis-scores that cell by design. V-7 passes either way (± 0.01), so nothing catches it. One line cell, but golden #10 cannot be typed from the spec alone. | AWS raw 33–35; key `I26050001.json` line 5 `9.32 / 2.14 / 11.46`; Decimal recomputation above. | §2.3: "on a converted line, `total_amount` = converted `net` + converted `vat` (the printed secondary-currency total is not converted separately; it is the V-7 check)"; §7 golden #10 line 5 = 9.32 / 2.14 / 11.46. |
| W-2 | Medium | §2.8 V-8, V-9, V-12; §2.6 branch (2) guard; §7 synthetic fixtures | The shape the branch-(2) guard exists for — a printed `stamp_tax` line **inside** the printed net (net 104 incl. stamp 4, VAT 23, total 127, `stamp_duty_amount` 4 since "Imposto do Selo" is printed; V-10 contemplates both present) — now fails validation before `Split`: V-8 → 104 + 23 + 4 = 131 ≠ 127; V-9 (`item` lines only) → 100 ≠ 104; the figures reconcile as printed, so an honest `reconciliation_note` is null → V-12 dead-letters. The guard is unreachable for an honest answer, and §7 has no fixture for it (fixtures (1)/(1′)/(2)/(3) all have no printed stamp line). Revision 7's V-8 fix for the stamp-outside-net shape broke the stamp-inside-net shape; neither is among the 40 (bench unaffected; production affected), and in branch (3) a zero difference would still "record a finding". | V-8/V-9/V-12 text; §2.6 (2) guard and "in branch (3) the difference is the DPRCHK finding"; §7 fixture list. | V-8: accept `total = net + vat ± 0.02` as well when a printed `stamp_tax` line exists; V-9: `Σ item lines = net` **or** `Σ all lines = net` (± 0.02); §7: a guard fixture (net 104, vat 23, duty 4 printed as a line, total 127 → V-8/V-9 pass, `Split` → no adjustment, header = Σ lines, no finding); §2.6: "(3) with a zero difference records no finding". |
| W-3 | Low | §5.2 statistics (line 413) | "2 placeholder fiscal ids, **1** placeholder document id" — revision 7 added Lari to N-7, so there are two (eSIMGo, Lari). | N-7 text; line 413. | "2 placeholder document ids". |
| W-4 | Low | §8 R-EX-2 (line 497); changelog line 7 | "the pre-declared corrections (**3** + 18 + #31)" — §5.5 now pre-declares four header cells (I24120001, I26070005, I26050010, I26070079). The changelog says "§7: goldens #10, #21, #27 typed" — #21/#27 are `ExtractionScoringTests` fixtures, not accept-corpus goldens (the golden list is #2/#6/#10/#35/#37/#40). | §5.5 list; §7 rows. | "4 + 18 + #31"; "golden #10 and the #21/#27 fixtures typed". |
| W-5 | Low | §2.2 `service_period` (line 116) | "(Locarent #24/#35: 'Prestação nº 74 (01/08/2026 - 31/08/2026)' on all seven lines)" — #35 prints "Prestação nº 73 (01/07/2026 - 15/07/2026)" on its seven lines (the annulled instalment); the example quotes #24 only. The rule and golden #35's expected value (2026-07-01..2026-07-15) are unaffected. | #35 raw 25–43; #24 raw 25–35. | "(Locarent #24: 'Prestação nº 74 (01/08/2026 - 31/08/2026)'; #35: 'Prestação nº 73 (01/07/2026 - 15/07/2026)')". |
| W-6 | Low | §2.6 branch (2) guard "(i.e. Σ all lines ≠ net)" | The parenthetical is equivalent to "no printed stamp line inside `net`" only when Σ is taken **after** step A: fixture (2) (one item 100, no printed stamp, net 100) has Σ = 100 = net before the append and 104 ≠ 100 after. Steps are written A then B, so the reading is recoverable, but a builder evaluating the guard on the raw lines sends fixture (2) to branch (3) (Net 100 / VAT 27 instead of 104 / 23). | §2.6 step order; §7 fixture (2). | "(i.e. Σ all lines, the step-A line included, ≠ net)". |

#### Owner questions Q-EX-0..25

All 26 recommendations stand as written; no new question. Verified this round where an artefact decides: **Q-EX-20** — clause (j) reproduces both utility keys from the printed VAT blocks (Águas 62,51 / 3,76 / 66,27 directly; EDP 13.52 + 14.55 and 0.81 + 3.35 from the two "IVA (…)" rows, not the sectional "A Total 27,93"), and (a)/(f) still reproduce MEO's 8 and Continente's 2; **Q-EX-24** — the first-printed rule types both headers (Águas 96340048 / 66.27 at raw 98–158 before 1185418181 / 27.12; EDP FT2026 K3426/340027209607 / 32,23 at raw 96 before CAV 3,02 at 153; "Devo pagar 36,97" layout 336); **Q-EX-25** — the fold reproduces #25 and the corrected #31 both ways; **Q-EX-6** — the 8 `vat_rate` cells are exactly Avis's six and #25's two; **Q-EX-23** — consequence unchanged. W-1 is a spec sentence, not a ruling.

#### Readiness for RED oracles

Not yet, on one cell: golden #10 line 5 `total_amount` (W-1 — 11.46 per the key, 11.47 per the rule; one sentence). W-2/W-6 change no bench oracle (none of the 40 has the shape) but add one synthetic fixture and two V-rule sentences; they should ride the same revision. Typeable now, figures recomputed by me: golden #2 (60.55 / 13.56 / 74.11; 3.62/0.47/4.09, 56.93/13.09/70.02), #6 (767.02 / 0.00; 634.72 / 132.33 / −0.03, `vat_rate` 0 on line 3, `line_count` 3), #10's nine net/VAT cells and lines 2 and 8 totals, #35 (header period 2026-07-01..2026-07-15 from the lines), Lari (372.30 / 0.00, `document_id` null), eSIMGo, VFX (1,990.00 / 41.78), Alibaba (4 lines, 1,316.23), #21 (62.51 / 3.76 / 66.27, `date_doc` 2026-07-22 against the corrected key, `date_due` 2026-08-11 printed), #27 (28.07 / 4.16 / 32.23, `document_id` FT2026K3426340027209607, `date_due` 2026-08-25), #25 and #31 both ways, the synthetic `Split` fixtures (1) 104/23, (1′) 106/23 + item 102, (2) 104/23, (3) 100 with Σ 104 and the 4.00 finding, the S-6 fixtures on all 24 stamped rows (R1 through `Decide`, R3 by day-of-month after the fallback, Locarent's derived header period), the occurrence-order fixture, S-4's three branches (5 + 2 seeds verified), the pairing key (unique within each statement), the 18 description corrections and #31's.

#### Verified myself

Worktree head `64e1f89`, clean; the spec (508 lines) in full and the word-level diff against `8ffd66d`; the review record (1,253 lines: rounds 1–6 verbatim and every disposition block through revision 7) in full. Skill build HEAD `a558523`, `status --porcelain` empty: schema.md lines 17 (24 DOCLOG), 78 (32 FDCHDR), 87 (18 FDCDTL), 93 (BNKACC), 95 (26 BNKMOV) — the §2.2/§2.3/§2.4/§2.5b lists match column by column; SKILL.md 95–162 (§5 Invoice-Receipt rule at 97; §6 stamp/fold rule at 160). All 35 fiscal PDFs resolved from the keys' `archivePath` basenames, SHA-256 = key `fileHash` on 35/35, dumped with `pdftotext -raw` (and `-layout` for the 19 revision-7 documents); read at the cited lines: Lari, AWS, Locarent #24/#35, Mobilize, Viajando, Águas do Porto (both invoices, all rows, both dates), EDP (summary page, first invoice in full, the VAT block, CAV), MEO (period, VAT summary, category subtotals), Hydra, Anaptyxis, Via Verde, Tranquilidade #6/#25/#31, Continente, VFX, Alibaba, Avis (layout), Google, eSIMGo, Stripe, BICS ×2, Itoorer (the R1 periods). Keys (40, python): 35 fiscal / 5 bank, 109 lines, 197 movements; the headers and lines of the 19 touched keys in full; `vatRate` 8 / quantity 6 / unitPrice 6, all on Avis and #25; R1 12 / R3 12 / blank 10 / R2 1 with `accountPeriodEvidence`; 6 receipts; no null header amount; the S-4 notes; `(movDate, amount, currency)` unique within each statement, one cross pair; `INDEX.md`. Arithmetic (Decimal, half-up): all nine AWS conversions incl. the three line totals; per-line VAT on MEO/Hydra/Mobilize/Locarent ×2/Anaptyxis (37 cells) and Via Verde's five nets; every `Split` trace and synthetic fixture under the revised V-8; all 24 `account_period` cells. Code on main `86820d6`: `AccountPeriodService.Decide` and `R3` (lines 26–62), `CompanyMatcher` (`SameTaxId` still private at 42, `Bare`/`HasCountryPrefix` 48–50), `DocumentTypeRouter.DocumentTypeFor` (23–31), `IngestionOptions.AllowedExtensions` declared at `IngestionService.cs` 27. `tools/` absent in the worktree.

#### Taken from the spec without verification

The CLI's flag semantics, `Read` truncation, `CLAUDE.md` discovery, Windows path-rule syntax, `--settings`/`--safe-mode` and `--verbose` behaviour; the deployment module's one-release/three-tree semantics, `Precondition.Artifact`, health checks 7 and 9, SCM recovery actions, `migrate.ps1`, ACLs (F-U-1/5/6/7 closed on text only); the .NET SDK csproj facts; token, context and sitting estimates; `git archive` CRLF and the long-line counts; `build_bnkmov.py` 77–84, `bnk_statement_parsers.py`, `SyncEngineWave3.ToCashDelta`, `DocumentCaptureService` C6; the worker/queue/ingestion/web code cited in §4.3/§4.7 (the `BuildArguments`/`PermissionsFile.Build`/`JobCompletion`/`TimeoutLaneMonitor`/`CompanyGate` seams are named, not yet written); the statement PDFs and their printed balances, the Revolut print order and the 15 + 2 + 1 description defects (rounds 3–6, keys re-read, PDFs not); Regus's and MEO's printed periods as reported-only rows (keys recomputed under R3, PDFs not re-read this round); Stripe's and Via Verde's line-to-row correspondence beyond the totals; the Awin PDF (no text layer); page counts; the reserve entries and their doclog rows; `EXTRACT.md` (unwritten).

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** 64e1f89 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; code read on main `86820d6` — `git diff --stat 2c8e5f2 86820d6 -- src local tests Sibyla.slnx` is empty, so the code round 6 verified is unchanged; skill build HEAD `a558523`, `status --porcelain` empty)
**Verdict:** ACCEPT
**Counts:** Critical 0 / High 0 / Medium 2 / Low 3

Revision 7 closes all nineteen round-6 findings by text I checked against the worker, ingestion and queue code on main, the pinned CLI's `--version`/`--help`, the deployment module's adapter and order oracle, the solution and publish/provision scripts, the 40 keys, and `pdftotext` of the four PDFs whose §7 fixtures changed (AWS #10, Águas do Porto #21, EDP #27, Lari #16) plus both Locarent documents. The diff (37 insertions / 35 deletions, taken line by line with `--word-diff`) touches §4–§8 only where round 6 asked; every changed mechanism is buildable on this host and every §7 row still names an expected value and a seam that exists on main or is named in §4.3. The two Mediums are places a builder decides alone: the new `PermissionsFile.Build(sandbox, package, releaseDir)` seam cannot produce the "every other release" deny entries from its three arguments without directory IO or a fourth argument; and the revised V-8 (`total = net + vat + duty + other`) rejects, with a null `reconciliation_note`, exactly the raw shape `Split` branch (2) exists to handle (none of the 40 has it).

#### Round-6 revisit

| id | State | Revision-7 text relied on; what I verified |
|---|---|---|
| C-U-1 | CLOSED | §2.7 N-7 "**Lari I26060014 `202605`** — the reference period, printed nowhere; the PDF has no invoice number"; §5.1 #16 "no document number printed"; §7 "Lari #16 (`document_id` null against the placeholder key)". `pdftotext -raw` of `Gott_Invoice_LariIntercambio_202606_01.pdf` (14 lines): no invoice number anywhere; "Data da Invoice: 3 de Junho de 2026"; key `documentId = "202605"`, `dateDoc 2026-06-03`. |
| C-U-2 | CLOSED | §2.3 clause (j); Q-EX-20; §4.2; §5.1 #21; §7 "#21 and EDP #27 (clause (j): one line each — 62.51 / 3.76 / 66.27 and 28.07 / 4.16 / 32.23 …)". Águas do Porto raw lines 153–158: "IVA 3,76 / (2)IVA 6% … 62,51 3,76 / 62,51 Total sem IVA / 66,27 TOTAL"; key 1 line 62.51/3.76/66.27. EDP raw 119/125/131/134–138: 13,52 / 14,41 / "A Total 27,93 sem IVA" / DGEG 0,07 / IEC 0,07 / "IVA (13,52) 6 % 0,81" / "IVA (14,41 + 0,07 + 0,07) 14,55 23 % 3,35"; arithmetic 13.52 + 14.55 = 28.07, 0.81 + 3.35 = 4.16, 28.07 + 4.16 = 32.23 = line 96 "Valor: 32,23"; key 1 line 28.07/4.16/32.23. |
| C-U-3 | CLOSED | §5.5 "**I26070079 `dateDoc` 2026-07-14 → 2026-07-22** (p.2 'Data de Fatura' …; a blank-rule row)"; §5.1 #21. Raw lines 7–8 "Emissão: 2026-07-22", 106–107 "Data de Fatura 2026-07-22", 30 "Fatura 96340048 … 2026-07-14 66,27" (the conta-corrente row); key `dateDoc 2026-07-14`, `accountPeriodRule None`. |
| C-U-4 | CLOSED | §2.3 `net_amount` secondary-currency rule; §4.2; §5.1 #10; §7 golden #10. AWS `-layout` line 27 "(1 USD = 0.86340951809 EUR) EUR 448.88 USD 519.89", detail lines 32–45 and 61–65 USD only. Recomputed at that rate (2 dp, half-up): 509.01 → 439.48, 117.07 → 101.08, 10.80 → 9.32, 2.48 → 2.14, 0.08 → 0.07, 0.02 → 0.02 — all nine key lines; Σ net 448.87 vs header 448.88, Σ VAT 103.24 exact. |
| C-U-5 | CLOSED | §2.2 `service_period` "when the header prints no period and every line prints the same one, the header period is that period (Locarent #24/#35 …)"; S-6; §7 golden #35. Locarent #24 raw 25–35: all seven lines carry "(01/08/2026 - 31/08/2026)", no header period; `AccountPeriodService.Decide(2026-08-01, 2026-08-01..08-31)` on main: `advance = Start > dateDoc` false → last month 202608 = key R1 202608. NC #35: all seven lines "(01/07/2026 - 15/07/2026)". |
| C-U-6 | CLOSED | §2.3 `vat_amount`/`net_amount` "Derived only where not printed per line"; §4.2 discount-column rule. Python over the keys: `round(net × 0.23, 2)` equals every non-zero line VAT on MEO, Hydra, Mobilize, Locarent #24/#35, Anaptyxis (0 mismatches); MEO Σ 700.69 vs header 700.68 (0.01, inside V-9); Via Verde `total − vat = net` on all 5 lines. |
| C-U-7 | CLOSED | §2.8 V-8 "`total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02`", V-9 "`item` lines only"; §2.6 (2) "and no printed `stamp_tax` line is inside `net` (Σ all lines ≠ net)". Text present; fixtures (1)/(1′)/(2)/(3) recomputed (104/23; 106/23 + item 102; 104/23; 100 with Σ 104). Residual on V-8 vs branch (2): W-2. |
| C-U-8 | CLOSED | §5.2 "unique **within each of the five statements** … one pair coincides across statements"; S-2. Python: 0 collisions on `(movDate, amount, currency)` in 46/10/3/87/51; exactly one cross-statement pair `(2025-09-26, 70000.00, EUR)`. |
| C-U-9 | CLOSED | S-6 "from `DateDoc` alone (after the §2.6 fallback — VFX's `date_doc` is null, its `DateDoc` the due date 2026-05-02 → 202604)". `AccountPeriodService.R3`: `Day <= 10` → prior month → 202604. |
| C-U-10 | CLOSED | §4.2 "the document's own invoice-date field wins over a date in free text (Viajando #7 …)"; §5.1 #7 "`date_doc` 2026-03-30". Key `I26030064` `dateDoc 2026-03-30`. PDF not re-read (round 6's evidence). |
| C-U-11 | CLOSED | §4.2 "`receipt` = SKILL.md §5's Invoice-Receipt: the document proves its own payment, `payment_proof.kind ≠ none`". Text only; `DocumentTypeRouter` mapping verified round 6. |
| C-U-12 | CLOSED | §2.6 "steps A–C change nothing except `VAT := VAT ?? 0` (Viajando, BICS)". |
| F-U-1 | CLOSED | §4.6 "(2) **One release for web and worker** … `publish-release.ps1` publishes all three host trees; the module … verifies the three trees and activates them in one transaction, the web install entries preceding the worker's … a web-only or worker-only release is refused at `Precondition.Artifact`"; Rollback "returns **web and worker together** … status 9 with a blank form until roll-forward"; §4.7 "web before worker inside the one release"; §8 R-EX-3. `publish-release.ps1` 10 (`$Hosts` default `web, api, worker`); `FileSystemWindowsAdapterV2.cs` `Artifact()` builds `PublishedArtifactV2` from `ArtifactTree(root, Web/Api/Worker)`; `ContractsV2.cs` 104 "release.json and the three trees verified"; `execution-order.json`: Install `FileSystem.PublishTree Host:Web` before `Host:Worker`, Configure `Process.Recycle Pool:Web` before `Process.Stop Service`, Health `Process.Start Service` — web live before the worker starts on the new release. |
| F-U-2 | CLOSED | §4.3 "`internal static IReadOnlyList<string> ClaudeCliProcess.BuildArguments(prompt, jobDir, packageDir, settingsPath, model, effort, outputFormat)`, which `RunAsync` calls to build `psi.ArgumentList`, and `internal static string PermissionsFile.Build(sandbox, package, releaseDir)`"; §7 permissions row asserts on both under the worker's `InternalsVisibleTo`. `ClaudeCli.cs` 22 (`public sealed class ClaudeCliProcess`, primary ctor — an internal static fits), 72–79 (today's list built inline). Residual on `Build`'s inputs: W-1. |
| F-U-3 | CLOSED | §4.3 "for every extension `IngestionOptions.AllowedExtensions` admits (nine today: `.pdf .png .jpg .jpeg .tif .tiff .xml .txt .csv`)". `IngestionService.cs` 10 (`class IngestionOptions`), 27–29 (that exact set); `DocumentContentInspector.cs` 36–46 (nine `ExpectedMime` entries). |
| F-U-4 | CLOSED | §4.7 "n = the count of existing **document-processing** jobs for the intake (by `payload_json->>'intakeId'`, `job_type = document-processing` — the row's `storage-transfer` job does not count) + 1, so the first re-extract is `:2` and the second `:3`"; Q-EX-18; §7 holds row. `QueueJob.cs` 22–23 (`"document-processing"`, `"storage-transfer"`); migration `IntakeAndQueue.cs` 52–53 (`job_type`, `payload_json jsonb`); `IngestionService.cs` 199–235 (both jobs per intake, payload `{"intakeId":"…"}`). |
| F-U-5 | CLOSED | §4.5 probe (b) "records **whether `-p --output-format stream-json` needs `--verbose`** on 2.1.259 — if so, `--verbose` joins the bench-only flags". `--help` 255: `--verbose  Override verbose mode setting from config`. Not run on input (as required). |
| F-U-6 | CLOSED | §4.3 "the SCM's recovery actions (`restart/10000/restart/60000/restart/300000`, `provision-production.ps1`) restart it, so a mismatched worker shows as a **restarting service that never holds a pid for 20 s** — which health check 7 fails". `provision-production.ps1` `sc.exe failure … actions= restart/10000/restart/60000/restart/300000` (line ≈ 220); `health.v2.json` check 7 (round 6). |
| F-U-7 | CLOSED | §5.7 "**added to `Sibyla.slnx` under a `/tools/` folder** so the suite builds it (no tests)". `Sibyla.slnx` has `/src/` and `/tests/` `<Folder>` elements of the same shape; `local/test.ps1` = `dotnet test Sibyla.slnx`; no `tools/` at the root today (created by the slice). |

#### Diff check

`git diff --word-diff -U0 8ffd66d 64e1f89` on the spec, every hunk read. Outside §4–§8 the changes are §1.2 Q-EX-20 (clause (j)), §2.2 `service_period`, §2.3 (clause (j), the secondary-currency and per-line derivation sentences), §2.6 (the branch-(2) guard, "change nothing except `VAT := VAT ?? 0`", the "none of the 40 prints net, VAT and a stamp line together" note), §2.7 N-7 (Lari), §2.8 V-8/V-9, and the two revision notes. In §4–§8:

| Where | Change | Buildable? |
|---|---|---|
| §4.2 `EXTRACT.md` row | (a)–(j); per-rate summary, insurer, utility, secondary-currency, per-line derivation and discount-column rules; `receipt` = Invoice-Receipt; the invoice-date-field rule | Yes — content of a file this repo writes; no mechanism changed |
| §4.3 start-up | SCM recovery wording (F-U-6) | Yes — matches `provision-production.ps1` |
| §4.3 sandbox copy | nine `AllowedExtensions` (F-U-3) | Yes — matches code |
| §4.3 seams | `BuildArguments(…)` and `PermissionsFile.Build(…)` (F-U-2) | Yes — internal statics under a new `[InternalsVisibleTo("Sibyla.Tests.Platform")]`; see W-1 on `Build`'s inputs |
| §4.5 probe (b) | records whether `--verbose` is needed (F-U-5) | Yes — `--verbose` exists on 2.1.259; the record is an R-EX-2 output |
| §4.6 (2), Rollback | one release id; module rollback returns web and worker together; status 9 with a blank form (F-U-1) | Yes — `publish-release.ps1` default hosts, the adapter's three-tree artifact, the order oracle; "refused at `Precondition.Artifact`" holds by construction (W-5 on the failure code) |
| §4.7 release order | "web before worker inside the one release" | Yes — order oracle |
| §4.7 re-extract | count over `job_type = document-processing` by `payload_json->>'intakeId'`; `:2` then `:3` (F-U-4) | Yes — column names and job-type constants match main |
| §5.1 #7, #10, #16, #21 | notes only | n/a |
| §5.2 / S-2 | uniqueness within each statement (C-U-8) | Yes — recomputed |
| S-6 | R3 rule after the `DateDoc` fallback; Locarent header period (C-U-5/9) | Yes — `Decide`/`R3` on main reproduce 202608 and 202604 |
| §5.5 | #21 `dateDoc` correction (C-U-3) | Yes — a CSV row; PDF confirms |
| §5.7 | bench in `Sibyla.slnx` under `/tools/` (F-U-7) | Yes |
| §7 goldens | #10 conversions, #35 header period; Lari/#21/#27 fixtures; holds row `:2`/`:3`; permissions row seams | Yes — every figure recomputed against the keys and PDFs |
| §8 R-EX-3 | "web and worker in the one release, web before worker in the module's plan" | Yes |

Nothing in §4.1, §4.4, §6 changed. The prompt line, the flag set and its order, the evidence layout, the attempt table, the csproj lines, the migration step and health checks 7/9 are byte-identical to revision 6.

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| W-1 | Medium | §4.3 seams; §4.5 deny list; §7 `ClaudeCliPermissionTests` | `PermissionsFile.Build(sandbox, package, releaseDir)` must emit `C:\Apps\Sibyla\worker\<every other release>\**`, which is not derivable from three path strings: either the static enumerates `Directory.GetDirectories(Path.GetDirectoryName(releaseDir))` at call time (IO inside the "pure" seam; the test then needs a temp worker root with fake sibling releases) or it takes the sibling list as an argument. A wholesale `worker\**` deny is excluded by the spec's own deny-outranks-allow note. The expected JSON is typeable either way; which way is the builder's alone. | §4.5 deny list; §4.3 seam signature; `provision-production.ps1` release-folder pattern `^\d{8}-\d{6}-[0-9a-f]{7}$`. | `PermissionsFile.Build(sandbox, package, releaseDir, otherReleaseDirs)` with the worker enumerating `C:\Apps\Sibyla\worker\*` (release-id pattern) at job start; the test passes two fake siblings and asserts both deny lines. One clause. |
| W-2 | Medium | §2.8 V-8 vs §2.6 branch (2); V-12 | The revised V-8 (`total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02`) fails on the raw answer of the very shape branch (2) is written for — net printed, the duty already inside the printed VAT/tax total, `net + vat = total` (fixture (2): 100 + 27 + 4 = 131 ≠ 127). The document's own figures reconcile, so `reconciliation_note` is null by its §2.2 definition, and V-12 dead-letters the honest answer before `Split` runs; branch (2) becomes unreachable in production. None of the 40 has this shape (the spec says so); the §7 fixture (2) is a scorer fixture, so no oracle is untypeable. | §2.8 V-8, V-12; §2.6 (2); §7 fixture (2) figures. | V-8 passes when either `total = net + vat + extra ± 0.02` (branch (1) shape) or `total = net + vat ± 0.02` with `(vat ?? 0) ≥ \|duty\|` (branch (2) shape); add a `reject/V-8-…` case for a third shape. One sentence. |
| W-3 | Low | §4.3 `BuildArguments`; §4.5 probe (b) | If probe (b) records that `stream-json` needs `--verbose`, the bench-only flag has to come from the same seam (`outputFormat = stream-json` ⇒ `--verbose`) or the bench builds its own list; the spec says only that it "joins the bench-only flags". | §4.3 signature; §4.5 (b). | "`BuildArguments` adds `--verbose` after `--output-format stream-json` when R-EX-2 says it is required; the production list is unchanged." |
| W-4 | Low | §2.3 `net_amount` (AWS rule); §7 golden #10 | The conversion sentence sits under `net_amount` only; golden #10 also converts VAT (117.07 → 101.08) and needs a line total (540.56, printed as USD 626.08). Both readings of `total_amount` ("printed, else net + vat") give 540.56 here; another document could differ by 0.01 (inside V-7). | AWS layout lines 34–35; key line 2 439.48 / 101.08 / 540.56. | "line `net_amount`, `vat_amount` and `total_amount` are each converted at the printed rate". |
| W-5 | Low | §4.6 (2) "refused at `Precondition.Artifact`" | A release folder missing a host tree makes the adapter's `ArtifactTree` call `Directory.EnumerateFiles` on an absent path, which throws; whether the run reports `SIBYLA-S2-PRECONDITION-ARTIFACT` or an adapter fault is not exercised by the oracles (the `artifact-refused` case is an injected refusal). Either way no partial release activates. | `FileSystemWindowsAdapterV2.cs` `ArtifactTree`; `execution-semantics.json` `artifact-refused`. | "is refused by the module's artifact precondition (a missing tree cannot form a release)". |

#### Owner questions Q-EX-0..25

All 26 recommendations remain buildable as written on this host; no new question. Verified this round where an artefact decides: **Q-EX-20** — clause (j) reproduces both utility keys from the PDFs (#21 62.51/3.76/66.27; #27 28.07/4.16/32.23 with 28.07 = 13.52 + 14.55 and 4.16 = 0.81 + 3.35); **Q-EX-18** — the count over `job_type = document-processing` by `payload_json->>'intakeId'` is expressible on `jobque` as migrated (`job_type varchar(50)`, `payload_json jsonb`), the storage-transfer job excluded, `ix_jobque_idempotency_key` unique; **Q-EX-16** — "released before the worker" holds by the module's plan inside one release (Install publishes Web before Worker; Configure recycles the web pool before the worker service is stopped; Health starts the worker), so the page is live before any v2 row can exist; **Q-EX-21** — `--permission-prompts` accepts `none` ("anything that would prompt is denied automatically"), `--verbose` exists, `--restricted`/`--safe-mode`/`--settings` help text unchanged from round 6; **Q-EX-15** — `--version` → `2.1.259 (Claude Code)`, exit 0; **Q-EX-22** — nothing in this review read `D:\ApolloData\worker-claude`; **Q-EX-6/24/25** — contract questions, their §7 figures carried from rounds 5–6 and unchanged. W-2 is a spec sentence in the contract reviewer's §2.8, not a ruling.

#### Readiness for RED oracles

Ready. Every §7 row names an expected value, and every seam it needs exists on main or is named in §4.3; the two Mediums are builder decisions that do not block typing an oracle (W-1: the test can create a temp worker root; W-2: fixture (2) is a scorer fixture and the V-8 reject file is typeable to either wording). Typeable now, figures recomputed by me this round: golden #10 (nine EUR lines from the printed rate; Σ 448.87 vs 448.88), golden #35 and the Locarent #24 S-6 case (header period from the lines → 202608 through `Decide`), the #21/#27 clause-(j) fixtures, the Lari `document_id` null fixture and its N-7 row, the #21 `dateDoc` correction row, the per-statement uniqueness the pairing fixture rests on, the `:2`/`:3` idempotency keys, the `permissions.json` seam's allow/deny list (modulo W-1's sibling source), the `BuildArguments` list in the §4.3 order with `-p` first. Carried from round 6 unchanged and re-checked as unchanged text: `ExtractionContractV2Tests` (#2/#6/#25/#31/#12/#13/#16), `SkillPackageTests`, `WorkerStartupTests`, `ProcessingEvidenceTests`, `QueueWorkerGateTests` through `CompanyGate.ApplyAsync`, `DocumentsReviewV2Tests`/`IngestionServiceHoldTests`, `ClaudeCliTimeoutTests` (`JobCompletion.CompleteAsync`, `TimeoutLaneMonitor.Record`), `EvidenceSecretScanTests`, `CompanyMatcherTests`/`AccountPeriodTests`, and the rest of `ExtractionScoringTests`.

#### Verified myself

- Worktree head `64e1f89`, clean; the spec (508 lines) and the review record (1,253 lines: rounds 1–6 verbatim and every disposition block, revisions 2–7) in full; `git diff 8ffd66d 64e1f89` (37/35) read hunk by hunk with `--word-diff`.
- Main `86820d6` (identical to `2c8e5f2` under `src/`, `local/`, `tests/`, `Sibyla.slnx`): `ClaudeCli.cs` (sealed class, primary ctor, `RunAsync` 59–103 building `psi.ArgumentList` inline, `ResolveCliExecutable` 105–115), `WorkerOptions.cs` (no version/model/effort/startup key), `ClaudeDocumentProcessor.cs` 26/38/82/109–118/145, `QueueWorker.cs` 87–100 (job-type switch), 330–380 (`CompleteAsync` private, the fence, `isTransfer`), `QueueJob.cs` 22–23/37, `IngestionService.cs` 10/27–29 (`IngestionOptions.AllowedExtensions`, nine), 190–235, 290–330 (`SubmitReviewAsync`), 440–470, 555–600 (both job-add paths and their fixed keys), `DocumentContentInspector.cs` 30–50, migration `20260827172608_IntakeAndQueue.cs` 52–53/61/89–91, `AccountPeriodService.cs` 20–60 (`Decide`, `R3`), the worker csproj (Sdk.Worker, no items), `src/Sibyla.Platform.Infrastructure/Properties/AssemblyInfo.cs` (Platform, Browser), the worker's `Properties/` (only `launchSettings.json`), `Sibyla.Tests.Platform.csproj` 23–26 and `Sibyla.Tests.Argus.csproj` 24–27 (both reference the worker), `Sibyla.slnx` (`/src/`, `/tests/` folders), `local/test.ps1`, `local/publish-release.ps1` (whole file), `local/provision-production.ps1` 170–230 (ACLs, `sc.exe failure` actions, SCM environment).
- Deployment module: `FileSystemWindowsAdapterV2.cs` 170–235 (`Artifact`, `ArtifactTree`, `TreeHash`), `ContractsV2.cs` 104, `execution-order.json` (all phases; Install/Configure/Health entries listed), `execution-semantics.json` (`artifact-refused`).
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: `--version` → `2.1.259 (Claude Code)`, exit 0; `--help` lines for `--add-dir`, `--disable-slash-commands`, `--effort` (low/medium/high/xhigh/max), `--fallback-model`, `--model`, `--no-session-persistence`, `--output-format` (text/json/stream-json), `--permission-prompts` (host/none), `-p`, `--restricted`, `--safe-mode`, `--settings`, `--strict-mcp-config`, `--tools`, `--verbose`. Never run on any input.
- Skill build: HEAD `a558523`, `status --porcelain` empty. PDFs via `pdftotext` at their key `archivePath`: AWS #10 (`-layout`, both pages), Águas do Porto #21 (`-raw`, 249 lines), EDP #27 (`-raw`, lines 1–175), Lari #16 (`-raw`, 14 lines), Locarent #24 and NC #35 (`-raw`, lines 1–60).
- Keys (read-only, python, UTF-8): I26050001, I26070079, I26080035, I26060014, I26080017, I26080018, I26070038, I26030025, I26040011, I26050052, I26020038, I26030064 (headers, lines, rules, evidence); the five bank keys' `(movDate, amount, currency)` per-statement uniqueness and the single cross-statement pair. Arithmetic: the six AWS conversions and both sums; per-line VAT at 23 % on six documents; Via Verde `total − vat`; EDP's 28.07/4.16/32.23; `Split` fixtures (1)/(1′)/(2)/(3); `Decide` on Locarent #24; `R3` on VFX.

#### Taken from the spec without verification

CLI runtime behaviour not exercised: the `Read` 2,000-character truncation, the `json` envelope's `num_turns`/`usage`/`modelUsage`, the `stream-json` trace and whether it needs `--verbose`, the Windows `Read(...)` rule syntax and deny-over-allow precedence, whether `--settings` applies under `--safe-mode`, the usage-limit message the bench parses; that a `StartAsync` throw under `WindowsServiceLifetime` exits non-zero (plausible from `host.Run()` uncaught; not run); the module's actual failure code for a release folder missing a tree (W-5); the .NET SDK csproj facts (round-4/5/6 evidence, not rebuilt this round); token/context estimates and the sitting arithmetic; that the release gate's fingerprint is taken from a clean export; the PDFs I did not open this round (#2, #6, #7 Viajando, #12, #13, #25, #31, Hydra's discount column, MEO/Mobilize/Anaptyxis/Via Verde line tables, the five statements and their printed balances — rounds 3–6 verified them; I checked their keys and arithmetic, not their text); `build_bnkmov.py` 77–84; `SyncEngineWave3.ToCashDelta`; `DocumentTypeRouter` (round 6); page counts; the content of `EXTRACT.md` (unwritten); the reserve entries' doclog rows; the "2 otherwise column-sliced" Revolut rows.

### Author's dispositions — revision 8

Revision 8 of `apollo-argus-extraction-v1-spec.md` (2026-09-06) addresses round 7 (feasibility ACCEPT on revisions 6 and 7; contract REVISE). Ids: `C-W-n` = the contract reviewer's W-1…W-6, `F-W-n` = the feasibility reviewer's W-1…W-5. Dispositions: **fixed** (the spec now says it), **owner question** (in §1.2 with a recommendation), **rejected** (with the reason). "Where" names the section of revision 8. The changes are surgical — nothing beyond these items is touched. Decisions the coordinator took for this revision are written in and not re-opened; no new owner question.

**Contract reviewer (C-W-1 … C-W-6)**

| id | Sev | Disposition | Where in revision 8 |
|---|---|---|---|
| C-W-1 | High | fixed — on a converted line, `total_amount` = converted `net` + converted `vat`; the printed secondary-currency total is not converted separately, it is the V-7 check; golden #10 line 5 = 9.32 / 2.14 / 11.46 stated (this supersedes F-W-4's "each converted" suggestion, which would give 11.47) | §2.3 `net_amount`; §7 golden row |
| C-W-2 | Medium | fixed — V-8 passes when either `total = net + vat + extra ± 0.02` (branch-(1) shape) or `total = net + vat ± 0.02` when a printed `stamp_tax` line exists or `(vat ?? 0) ≥ \|duty\|` (branch-(2) shape); V-9: `Σ item lines = net` or `Σ all lines = net` (± 0.02); §2.6 branch (3) with a zero difference records no finding; §7 gains the guard fixture (net 104 incl. a printed stamp line 4, VAT 23, total 127 → V-8/V-9 pass, `Split` makes no adjustment, header = Σ lines, no finding) and one reject case for a third shape | §2.8 V-8, V-9; §2.6; §7 |
| C-W-3 | Low | fixed — "2 placeholder document ids (eSIMGo, Lari)" | §5.2 statistics |
| C-W-4 | Low | fixed — "4 + 18 + #31" in R-EX-2; the revision-7 changelog reads "golden #10 and the #21/#27 fixtures typed" | §8 R-EX-2; the revision-7 note |
| C-W-5 | Low | fixed — Locarent #35 quoted as "Prestação nº 73 (01/07/2026 - 15/07/2026)" beside #24's | §2.2 `service_period` |
| C-W-6 | Low | fixed — the guard parenthetical reads "(i.e. Σ all lines, the step-A line included, ≠ net)" | §2.6 branch (2) |

**Feasibility reviewer (F-W-1 … F-W-5)**

| id | Sev | Disposition | Where in revision 8 |
|---|---|---|---|
| F-W-1 | Medium | fixed — `PermissionsFile.Build(sandbox, package, releaseDir, otherReleaseDirs)`; the worker enumerates `C:\Apps\Sibyla\worker\*` matching `^\d{8}-\d{6}-[0-9a-f]{7}$` at job start, excluding its own release, and passes the siblings (the seam does no IO); the test passes two fake siblings and asserts both deny lines | §4.3 seams; §7 permissions row |
| F-W-2 | Medium | fixed — as C-W-2 (both V-8 shapes, V-9 over item or all lines, the guard fixture, the third-shape reject case) | §2.8; §2.6; §7 |
| F-W-3 | Low | fixed — `BuildArguments` adds `--verbose` after `--output-format stream-json` only when R-EX-2 says it is required; the production list is unchanged; asserted in `ClaudeCliPermissionTests` | §4.3; §7 |
| F-W-4 | Low | superseded by C-W-1 (the coordinator's decision) — `net_amount` and `vat_amount` are each converted at the printed rate, but the line `total_amount` is their sum, not a separate conversion (AWS line 5: 11.46, not 11.47); the reviewer's concern (VAT and total on converted lines are covered) is met | §2.3 `net_amount`; §7 golden row |
| F-W-5 | Low | fixed — "is refused by the module's artifact precondition (a missing tree cannot form a release)" | §4.6 (2) |

Owner questions after revision 8: Q-EX-0…25 unchanged; no new question.

## Round 8 — revision 8, head `601edf8` (2026-09-06) — ACCEPTED

| Reviewer | Verdict | Critical / High / Medium / Low | Round-7 findings closed |
|---|---|---|---|
| contract | ACCEPT | 0 / 0 / 1 / 3 | 11 of 11 ("the contract is complete": no cell on the 40 keys is underivable; all 35 fiscal keys pass V-7/V-8/V-9 on their values) |
| feasibility | ACCEPT | 0 / 0 / 0 / 4 | 11 of 11 (diff 64e1f89..601edf8 read word by word; nothing in §4–§8 unbuildable; RED oracles ready) |

**Closing verdict.** Both independent reviewers ACCEPT the same head, `601edf8`, with Critical 0 and High 0: the specification is accepted at revision 8 after eight two-reviewer rounds (contract REVISE ×7 then ACCEPT; feasibility REVISE ×5 then ACCEPT ×3 on revisions 6, 7 and 8). The residual findings below are carried into the build slice as a revision-9 amendment to be made, with dispositions, before the corresponding oracle is typed; none blocks a RED oracle:

- C-X-1 (Medium): V-9's VAT clause must also accept `Σ lines.vat + |duty| = header.vat ± 0.02` when `duty` is non-null and no `stamp_tax` line is printed (the branch-(2) shape); fixture (2) states its line VAT 23.00.
- C-X-2, C-X-3, F-X-2, F-X-3 (Low): wording — the V-8 second-shape label; the reject case's null `reconciliation_note`; the reject file lives in the Platform rejection corpus; fixture (3) is Split-level.
- C-X-4 = F-X-1 (Low): the R-EX-2 `--verbose` flag lives in `const bool StreamJsonNeedsVerbose` on `ClaudeCliProcess`, false at RED.
- F-X-4 (Low): the sibling-release enumeration root is the parent of the release directory (`C:\Apps\Sibyla\worker` in production).

### Reviewer: contract — report verbatim

**Reviewer:** contract
**Head reviewed:** 601edf8 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; diff 64e1f89→601edf8 = 17+/15− on the spec, read word by word; skill build HEAD `a558523`, `status --porcelain` empty; AWS PDF SHA-256 `241354fb…` = key `fileHash`)
**Verdict:** ACCEPT
**Counts:** Critical 0 / High 0 / Medium 1 / Low 3

Revision 8 does what the dispositions say and nothing else. The converted-total rule reproduces all three AWS line totals (540.56, 11.46, 0.09) with V-7 still passing on line 5 against the printed 13.28 × rate (11.47, a 0.01 difference); the four V-8/V-9/branch-(2)/branch-(3) sentences are consistent with each other and with the five fixtures and the reject case, each recomputed; every one of the 35 fiscal keys passes V-7/V-8/V-9 and `lineCount` on its values (109 lines). I found no cell on the 40 keys that document + spec do not derive: **the contract is complete.** The one Medium is the VAT-side twin of round 7's W-2 — V-9's "likewise for `vat`" still rejects the raw branch-(2) answer (duty inside the printed VAT) when the line VAT is the true VAT — a production-shape validator residual, not a key cell, and not a blocker.

#### Round-7 revisit

| id | State | Revision-8 text relied on and what I verified |
|---|---|---|
| C-W-1 | CLOSED | §2.3 `net_amount`: "**On a converted line, `net_amount` and `vat_amount` are each converted at the printed rate and `total_amount` = converted net + converted VAT** — the printed secondary-currency total is not converted separately; it is the V-7 check (AWS line 5: 10.80 / 2.48 / 13.28 USD → 9.32 / 2.14 / **11.46**, not 13.28 × rate = 11.47)"; §7 golden #10 "line 5 is 9.32 / 2.14 / **11.46**". Decimal half-up at 0.86340951809: 509.01→439.48, 117.07→101.08, sum 540.56 (printed 626.08×r = 540.56); 10.80→9.32, 2.48→2.14, sum 11.46 (13.28×r = 11.47, V-7 diff 0.01 ≤ 0.01); 0.08→0.07, 0.02→0.02, sum 0.09 (0.10×r = 0.09). Key `I26050001.json` lines 2/5/8 = 439.48/101.08/540.56, 9.32/2.14/11.46, 0.07/0.02/0.09; the six zero lines 0/0/0; header 448.88/103.24/552.12 as printed (PDF layout 8–9, 27–28). |
| C-W-2 | CLOSED (residual X-1, VAT side) | V-8: "passes when **either** `total = net + vat + (duty ?? 0) + (other ?? 0) ± 0.02` … **or** `total = net + vat ± 0.02` when a printed `stamp_tax` line exists or `(vat ?? 0) ≥ \|duty\|` … A third shape fails"; V-9: "`Σ item lines.net = header.net ± 0.02` **or** `Σ all lines.net = header.net ± 0.02` … and likewise for `vat`"; §2.6 "a zero difference (a printed stamp line already inside the net, the guard's shape) records no finding"; §7 guard fixture and `reject/V-8-third-shape.json`. Recomputed: guard (net 104 incl. printed stamp 4, vat 23, duty 4, total 127) → V-8 shape 1 131≠127, shape 2 127=127 with a printed stamp line ✓; V-9 net: Σ item 100≠104, Σ all 104=104 ✓; VAT 23=23 ✓; `Split`: step A appends nothing (stamp line printed), (1) 131≠127, (2) 127=127 and 23≥4 but Σ all lines 104 = net → guard blocks, (3) → no adjustment, header 104 = Σ lines 104, zero difference → no finding. = the §7 sentence. Reject (100/23/4/129, no stamp line): shape 1 127≠129, shape 2 123≠129 → fails ✓. |
| C-W-3 | CLOSED | §5.2 "2 placeholder fiscal ids, 2 placeholder document ids (eSIMGo, Lari)"; N-7 (line 241) names exactly eSIMGo `Org3957statementnoinvoicenumberprinted` and Lari `202605`; key `I26060014.json` `documentId = "202605"`. |
| C-W-4 | CLOSED | §8 R-EX-2 "the pre-declared corrections (4 + 18 + #31)"; §5.5 pre-declares four header cells (I24120001, I26070005, I26050010, I26070079) + the 18 descriptions; the revision-7 changelog now reads "§7: golden #10 and the #21/#27 fixtures typed". |
| C-W-5 | CLOSED | §2.2 "(Locarent #24: 'Prestação nº 74 (01/08/2026 - 31/08/2026)' on all seven lines; #35: 'Prestação nº 73 (01/07/2026 - 15/07/2026)' on its seven)". `pdftotext -raw`: #24 raw 25–35 seven lines "Prestação nº 74 (01/08/2026 - 31/08/2026)"; #35 raw 25–43 seven lines "Prestação nº 73 (01/07/2026 - 15/07/2026)". Key `I26080018` `accountPeriod 202607`, 7 lines. |
| C-W-6 | CLOSED | §2.6 branch (2) "(i.e. Σ all lines, the step-A line included, ≠ net)". Fixture (2) recomputed with the appended line: Σ 104 ≠ 100 → branch (2) → Net 104, VAT 23. |
| F-W-1 | CLOSED | §4.3 "`PermissionsFile.Build(sandbox, package, releaseDir, otherReleaseDirs)` … the worker enumerates `C:\Apps\Sibyla\worker\*` matching the release-id pattern `^\d{8}-\d{6}-[0-9a-f]{7}$` at job start, excluding its own release, and passes the siblings; the seam itself does no IO"; §7 "passed two fake sibling release directories and the test asserts both deny lines". Text only (feasibility's artefact). |
| F-W-2 | CLOSED (residual X-1) | As C-W-2: both V-8 shapes, V-9 over item or all lines, the guard fixture, the third-shape reject case — all present and recomputed. |
| F-W-3 | CLOSED | §4.3 "(it adds `--verbose` after `--output-format stream-json` **only** when R-EX-2 records that the pinned CLI requires it — the production list, `json`, is unchanged)"; §7 "`BuildArguments` with `outputFormat = stream-json` carries `--verbose` only under the R-EX-2 flag". Text only; see X-4 on where the flag lives. |
| F-W-4 | CLOSED (superseded by C-W-1) | The §2.3 sentence converts net and VAT each and defines the total as their sum — the reviewer's concern (VAT and total on converted lines covered) is met; "each converted" on the total would give 11.47 ≠ key. |
| F-W-5 | CLOSED | §4.6 (2) "a web-only or worker-only release is refused by the module's artifact precondition (a missing tree cannot form a release)". Text only. |

#### Diff check

`git diff --numstat 64e1f89 601edf8` = 17 / 15 on the spec; every hunk read. Hunks: the title (revision 8); the revision note; the changelog (the revision-7 note's "§7" clause reworded per C-W-4, a new revision-8 note inserted); §2.2 `service_period` (C-W-5); §2.3 `net_amount` (C-W-1); §2.6 `Split` — the guard parenthetical (C-W-6) and the zero-difference clause (C-W-2); V-8 and V-9 (C-W-2/F-W-2); §4.3 seams (F-W-1, F-W-3); §4.6 (2) (F-W-5); §5.2 statistics (C-W-3); §7 golden row (C-W-1), permissions row (F-W-1, F-W-3), scoring row (C-W-2: guard fixture, reject case, the "none in the guard fixture" assertion); §8 R-EX-2 (C-W-4). Every hunk maps to a round-7 id; nothing outside the round-7 items moved — V-7, V-10, V-12, the `Split` traces for #6/#16/#25/#31, the Signs rule, the header `net_amount`/`vat_amount`/`total_amount` rows, the fixtures (1)/(1′)/(2)/(3) and all other §4–§8 text are byte-identical to revision 7. The review record's round-7 section and the revision-8 dispositions are appended only (186 insertions).

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| X-1 | Medium | §2.8 V-9 ("likewise for `vat`") vs §2.6 branch (2); V-12; §7 fixture (2) | Revision 8 opened V-8 and V-9's net side for the branch-(2) shape, but V-9's VAT clause still rejects the raw answer of `Split` branch (2) proper — the duty inside the printed VAT/tax total, no printed stamp line (fixture (2): header vat 27 = VAT 23 + duty 4). The line's `vat_amount` is the printed or derived VAT (23, "`vat = net × the document's single rate`"), so Σ item lines.vat = Σ all lines.vat = 23 ≠ 27; the figures reconcile as printed, so `reconciliation_note` is null → V-12 dead-letters before `Split`. Branch (2) stays unreachable for an honest answer whose line VAT is the true VAT. None of the 40 has the shape (bench unaffected); §7 fixture (2) is a scorer fixture and states no line VAT, so no oracle is untypeable. | My recomputation under the revision-8 text: "(2) V-8 shapes (False, True), V-9 net True, **V-9 vat False**"; V-9 text line 255; fixture (2) at §7. | V-9 VAT clause: "`Σ lines.vat = header.vat ± 0.02`, or — when `duty` is non-null and no `stamp_tax` line is printed — `Σ lines.vat + \|duty\| = header.vat ± 0.02` (the branch-(2) shape)"; state fixture (2)'s line VAT (23.00) in §7 and assert V-9 passes on it. One clause; can ride with the code. |
| X-2 | Low | §2.8 V-8 parenthetical; §7 guard fixture | V-8 labels its second alternative "the branch-(2) shape", and §7 calls the guard fixture "the branch-(2) shape", but `Split` routes the guard fixture (stamp inside the net) to branch (3) with a zero difference — the §2.6 sentence says so. The effect is consistent; the label is not. | §2.6 "in branch (3) … a zero difference (… the guard's shape)"; V-8 line 254; §7 guard fixture. | V-8: "(the duty-inside shape: branch (2), or branch (3) with a zero difference)"; §7: drop "(the branch-(2) shape)" or say "V-8's second shape". |
| X-3 | Low | §7 `reject/V-8-third-shape.json` | "neither shape, refused" is a refusal only through V-12 with `reconciliation_note = null`; the sentence does not say the fixture's note is null (with a note the answer is accepted with a finding). | V-12 line 258. | "(…, `reconciliation_note` null → neither shape, refused)". |
| X-4 | Low | §4.3 `BuildArguments`; §7 permissions row "only under the R-EX-2 flag" | The seam's seven arguments carry no R-EX-2 result; where the "flag" lives (a `WorkerOptions` key, a constant on `ClaudeCliProcess`) is unstated, so the test's expected list depends on a choice the builder makes alone. | §4.3 signature line 323; §7 permissions row. | Name it: "a `const bool StreamJsonNeedsVerbose` on `ClaudeCliProcess`, set at R-EX-2; the test asserts both values". |

#### Owner questions Q-EX-0..25

Unchanged from revision 7; no new question, and nothing in revision 8 needs a ruling. Re-checked where an artefact decides this round: **Q-EX-25** — the fold and `Split` reproduce #25 (392.39 / 19.15, header 411.54) and #31 (−143.02 / −12.38, −155.40) unchanged (traces byte-identical to revision 7; both keys pass V-8/V-9 on their values); **Q-EX-4** — the appended stamp line's behaviour in every fixture recomputed; **Q-EX-12** — V-12 is what X-1 and X-3 turn on (the recommendation stands; X-1 is a V-9 clause, not a ruling). The AWS conversion rule (C-W-1) is a spec sentence, not a ruling.

#### Readiness for RED oracles

Ready. Golden #10 is now fully typeable from the spec: nine lines 0/0/0, 439.48/101.08/540.56, 0/0/0, 0/0/0, 9.32/2.14/11.46, 0/0/0, 0/0/0, 0.07/0.02/0.09, 0/0/0; header 448.88/103.24/552.12 as printed; V-7 on all nine exact; V-9 Σ net 448.87 vs 448.88 inside ± 0.02. Goldens #2, #6, #35, #37, #40 untouched by the diff and their headers/lines pass V-7/V-8/V-9 on the key values (#35: −260.41/−50.82/−311.23, seven lines summing exactly). Scoring fixtures recomputed under the revision-8 text: (1) → Net 104 / VAT 23 / stamp 4; (1′) → 106 / 23 / item 102 / stamp 4; (2) → 104 / 23 / stamp 4 (guard passes because Σ all lines 104 ≠ 100 after step A); (3) → Net 100, Σ lines 104, finding 4.00, no failure — fixture (3) fails both V-8 shapes on the raw answer, which is by design (it is the non-reconciling shape; a note admits it under V-12); guard → no adjustment, header = Σ lines, no finding; reject → neither V-8 shape. X-1 changes no bench oracle (none of the 40 has the shape) and adds one clause and one figure; X-2/X-3/X-4 are wording. None blocks typing.

#### Verified myself

Worktree head `601edf8`, clean; the revision-8 spec (510 lines) at §2.2/§2.3/§2.6/§2.8/§4.3/§4.6/§5.2/§5.5/§7/§8 and the whole word-level diff against `64e1f89` (17/15, every hunk); the review record's round 7 (both reports verbatim) and the revision-8 dispositions in full. Skill build HEAD `a558523`, `status --porcelain` empty; AWS #10 (`Gott/Payables/202605/Gott_Invoice_AWS_202605_01.pdf`, SHA-256 = key `fileHash`) via `pdftotext -layout`: rate at line 27, EC2 626.08/509.01/117.07 (33–35), VPC 13.28/10.80/2.48 (39–41), Data Transfer 0.10/0.08/0.02 (61–63), header 552.12/103.24/448.88 (8–9, 27–28); Locarent #24 and NC #35 via `-raw` (the seven "Prestação nº" lines each). Keys (read-only, python, UTF-8): `I26050001` header and nine lines; `I26080018`, `I26080017`, `I26060014`; all 35 fiscal keys swept — 109 lines, V-7 on every line, header `net + vat = total ± 0.02`, Σ lines = header net and VAT ± 0.02, `lineCount` = lines on 35/35, 0 violations. Arithmetic (Decimal, half-up): the six AWS conversions, the three line totals both ways, the V-7 margin on line 5; V-8 (both shapes), V-9 (net and VAT, both alternatives) and `Split` (steps A–C, the guard, the zero-difference rule) on fixtures (1), (1′), (2), (3), the guard fixture and the reject case. §5.5's four pre-declared header cells; N-7's two placeholder document ids.

#### Taken from the spec without verification

The round-7 contract reviewer's re-confirmed traces on the 35 keys beyond what the diff touches (Split on #6/#25/#31, clauses (f)–(j), the per-line VAT/net derivations, S-6 on the 24 stamped rows, the S-4 seeds) — I relied on the diff showing that text unchanged and on the key sweep, not on re-reading those PDFs; the code seams (`BuildArguments`, `PermissionsFile.Build`, `JobCompletion`, `TimeoutLaneMonitor`, `CompanyGate`), the release-id pattern, the deployment module's artifact precondition, `--verbose`/`stream-json` behaviour on 2.1.259 and everything else in §4–§6 (feasibility's angle, two consecutive ACCEPTs); the statement PDFs and their printed balances; `EXTRACT.md` (unwritten); the reserve entries.

### Reviewer: feasibility — report verbatim

**Reviewer:** feasibility
**Head reviewed:** 601edf8 (worktree `D:\fileStorage\repos\GOTT.Apollo-worktrees\apollo-extraction-spec`, branch `ops/argus-extraction-v1-spec`, clean; `git diff --stat 64e1f89 601edf8` = 17+/15− on the spec, every hunk read; code read on main `86820d6` at `D:\fileStorage\repos\GOTT.Apollo` — the same head round 7 read, `git diff --stat 86820d6 HEAD -- src local tests Sibyla.slnx` empty; skill build HEAD `a558523`, `status --porcelain` empty; pinned CLI `--version` → `2.1.259 (Claude Code)`, exit 0)
**Verdict:** ACCEPT
**Counts:** Critical 0 / High 0 / Medium 0 / Low 4

Revision 8 closes all eleven round-7 findings by text I checked against the AWS key and its arithmetic, the two fixtures' arithmetic under the revised V-8/V-9 and `Split`, the worker code and ACLs on main, the release-id pattern in `provision-production.ps1` and the module's `ReleaseId.TryParse`, the pinned CLI's `--help`, and the module's `Precondition.Artifact` adapter and semantics oracle. The diff touches §4–§8 only at the seven places the dispositions name; everything rounds 6–7 verified is byte-identical or unchanged on main. Nothing in §4–§8 is unbuildable on this host; every §7 test names an expected value and a seam that exists on main or is named in §4.3. The four Lows are wording a builder resolves alone.

#### Round-7 revisit

| id | State | Revision-8 text relied on; what I verified |
|---|---|---|
| C-W-1 | CLOSED | §2.3 `net_amount`: "**On a converted line, `net_amount` and `vat_amount` are each converted at the printed rate and `total_amount` = converted net + converted VAT** — the printed secondary-currency total is not converted separately; it is the V-7 check (AWS line 5: … 9.32 / 2.14 / **11.46**, not 13.28 × rate = 11.47)"; §7 golden #10 "line 5 is 9.32 / 2.14 / **11.46**". Key `I26050001.json` line 5 = 9.32 / 2.14 / 11.46 (read). Decimal half-up at 0.86340951809: 10.80 → 9.32, 2.48 → 2.14, 13.28 → 11.47; 9.32 + 2.14 = 11.46; line 2: 439.48 + 101.08 = 540.56 = key; line 8: 0.07 + 0.02 = 0.09 = key; V-7 holds on all three by construction. |
| C-W-2 | CLOSED | §2.8 V-8 (both shapes, "A third shape fails"), V-9 ("`Σ item lines` … **or** `Σ all lines`"); §2.6 "a zero difference (a printed stamp line already inside the net, the guard's shape) records no finding"; §7 guard fixture and `reject/V-8-third-shape.json`. Recomputed: (1) 100+23+4 = 127 shape-1 ✓; (1′) 100+23+4+2 = 129 ✓; (2) 100+27 = 127 with 27 ≥ 4 shape-2 ✓; guard 104+23 = 127 with a printed stamp line shape-2 ✓, V-9 `Σ all` = 100+4 = 104 = net ✓, `Split`: no append (line printed), (1) 131 ≠ 127, (2) blocked by the guard (Σ all lines = net), (3) difference 0 → no finding; reject 100+23+4 = 127 ≠ 129 and 123 ≠ 129 → neither shape, null note → V-12 refuses. |
| C-W-3 | CLOSED | §5.2 "2 placeholder fiscal ids, 2 placeholder document ids (eSIMGo, Lari)" (line 415). |
| C-W-4 | CLOSED | §8 R-EX-2 "the pre-declared corrections (4 + 18 + #31)" (line 499) — §5.5 pre-declares I24120001, I26070005, I26050010, I26070079 = 4; the revision-7 changelog (line 9) reads "golden #10 and the #21/#27 fixtures typed". |
| C-W-5 | CLOSED | §2.2 `service_period` "(Locarent #24: 'Prestação nº 74 (01/08/2026 - 31/08/2026)' on all seven lines; #35: 'Prestação nº 73 (01/07/2026 - 15/07/2026)' on its seven)". `pdftotext -raw` of `Gott_CreditNote_Locarent_202608_01.pdf` (key `I26080018` `archivePath`): raw 25/28/31/34/37/40/43 all carry "Prestação nº 73 (01/07/2026 - 15/07/2026)" — seven lines. |
| C-W-6 | CLOSED | §2.6 branch (2) "(i.e. Σ all lines, the step-A line included, ≠ net)". Fixture (2) after step A: 100 + 4 = 104 ≠ 100 → branch (2) → 104 / 23 as §7 states. |
| F-W-1 | CLOSED | §4.3 "**`internal static string PermissionsFile.Build(sandbox, package, releaseDir, otherReleaseDirs)`** … the worker enumerates `C:\Apps\Sibyla\worker\*` matching the release-id pattern `^\d{8}-\d{6}-[0-9a-f]{7}$` at job start, excluding its own release, and passes the siblings; the seam itself does no IO"; §7 "passed two fake sibling release directories and the test asserts both deny lines". Pattern = `provision-production.ps1` lines 33 and 197 verbatim and the module's `ReleaseId.TryParse` (`ValueObjectsV2.cs` 397–404: 23 chars, digits 0–7, digits 9–14, lower hex 16–22). `C:\Apps\Sibyla\worker` today holds seven such directories plus `CURRENT.txt`/`PREVIOUS.txt` (files, and excluded by the pattern). `.\SibylaWorker` has ReadAndExecute on `C:\Apps\Sibyla\worker` with `ContainerInherit,ObjectInherit` (`provision-production.ps1` 158–166, 176), so the enumeration is permitted. |
| F-W-2 | CLOSED | As C-W-2: the raw shape branch (2) serves now passes V-8 (fixture (2): 100+27 = 127, 27 ≥ 4) and V-9 (`Σ item` = 100 = net before step A); branch (2) is reachable in production. |
| F-W-3 | CLOSED | §4.3 "`BuildArguments` … (it adds `--verbose` after `--output-format stream-json` **only** when R-EX-2 records that the pinned CLI requires it — the production list, `json`, is unchanged)"; §7 "`BuildArguments` with `outputFormat = stream-json` carries `--verbose` only under the R-EX-2 flag". `--help` 2.1.259 line 255: `--verbose  Override verbose mode setting from config`; `--output-format` line 140 (`text`/`json`/`stream-json`). Where the flag lives is X-1 (Low). |
| F-W-4 | CLOSED (superseded by C-W-1) | The disposition names the coordinator's decision: net and VAT each converted, the line total = their sum, not a separate conversion. My round-7 concern (VAT and total on converted lines covered by a rule) is met; the key's 11.46 confirms the chosen wording is the one the golden needs. |
| F-W-5 | CLOSED | §4.6 (2) "a web-only or worker-only release is refused by the module's artifact precondition (a missing tree cannot form a release)". `FileSystemWindowsAdapterV2.cs` 189 builds `PublishedArtifactV2` from `ArtifactTree(root, Web/Api/Worker)`; `ArtifactTree` 201–207 runs `Directory.EnumerateFiles(path, …)` on the host subfolder, which throws on a missing tree; `execution-semantics.json` case `artifact-refused` (199–205): outcome `Failed`, every downstream disposition `Skipped:NotReached`, `host: unchanged`. The wording no longer names a failure code it cannot prove. |

#### Diff check

`git diff 64e1f89 601edf8` on the spec, every hunk read word by word. Outside §4–§8: the title, the revision-8 note, the new revision-7 changelog line and the corrected revision-6 changelog (C-W-4); §2.2 (C-W-5); §2.3 (C-W-1); §2.6 (C-W-6, the zero-difference sentence); §2.8 V-8/V-9 (C-W-2). In §4–§8:

| Where | Change | Buildable? |
|---|---|---|
| §4.3 seams (line 323) | `BuildArguments` adds `--verbose` after `--output-format stream-json` only under the R-EX-2 record; `PermissionsFile.Build` gains `otherReleaseDirs`; the worker enumerates siblings by the release-id pattern at job start, excluding its own release; the seam does no IO | Yes — `--verbose` exists on 2.1.259; the pattern is the one `provision-production.ps1` and the module already use; the worker's ACL on `C:\Apps\Sibyla\worker` inherits to children; a pure static with an `IEnumerable<string>` argument is trivially testable with two fake paths |
| §4.6 (2) (line 343) | "refused by the module's artifact precondition (a missing tree cannot form a release)" | Yes — holds by the adapter's construction and the `artifact-refused` oracle |
| §5.2 statistics (line 415) | "2 placeholder document ids (eSIMGo, Lari)" | n/a (count) |
| §7 golden row (line 478) | #10 line 5 = 9.32 / 2.14 / 11.46 | Yes — equals the key; V-7 holds |
| §7 permissions row (line 485) | two fake sibling directories → two deny lines; `--verbose` only under the flag | Yes — both assertions typeable on the static seams |
| §7 scoring row (line 486) | the guard fixture (V-8/V-9 pass, `Split` no adjustment, header = Σ lines, no finding); `reject/V-8-third-shape.json` | Yes — arithmetic recomputed above; the reject file follows the `reject/<V-n>-<name>.json` convention (X-2 on which row names it) |
| §8 R-EX-2 (line 499) | "4 + 18 + #31" | n/a (count) |

Nothing else in §4.1–§4.7, §5, §6, §7 or §8 changed: the prompt line, the flag set and its order, the evidence layout, the attempt table, the csproj lines, the migration step, health checks 7/9, the bench plan, the `Split` traces for #6/#16/#25/#31 and fixtures (1)/(1′)/(2)/(3) are byte-identical to revision 7. The code, the CLI binary and the skill build are the artefacts rounds 6–7 verified (same heads).

#### New findings

| id | Sev | Section | Finding | Evidence | Suggested fix |
|---|---|---|---|---|---|
| X-1 | Low | §4.3 `BuildArguments`; §7 permissions row | "only under the R-EX-2 flag" names no home for the flag: the seven-parameter signature has no slot for it, so it must be a constant (or a `WorkerOptions` key) whose value R-EX-2 sets; at RED the test can only assert the default (no `--verbose`) and be flipped at R-EX-2. Buildable either way; the builder chooses. | §4.3 signature `BuildArguments(prompt, jobDir, packageDir, settingsPath, model, effort, outputFormat)`; §8 R-EX-2 probe (b). | "a `const bool StreamJsonNeedsVerbose` on `ClaudeCliProcess`, `false` at RED, set by R-EX-2's record; the test asserts the list for both values". |
| X-2 | Low | §7 scoring row vs rejection-corpus row | `reject/V-8-third-shape.json` is listed inside the `ExtractionScoringTests` row (`tests/Sibyla.Tests.Argus`), but rejection files live under `tests/Sibyla.Tests.Platform/golden/extract-v2/reject/` and are exercised by `ExtractionContractV2Tests.EveryRejectionNamesItsRule` with an `expected.txt`. Which project holds the file is ambiguous. | §7 rows 1–2 and the scoring row. | Move the clause to the rejection-corpus row ("plus `V-8-third-shape`"), or write "(in the Platform rejection corpus)". |
| X-3 | Low | §7 fixture (3) after the revised V-8 | Fixture (3) (net 100, vat 23, duty 4, total 130) fails both V-8 shapes on a raw answer (127 ≠ 130; 123 ≠ 130) — it is the third shape with a different total — so in production it reaches `Split` only with a `reconciliation_note`. As a scorer fixture applied to `Split` directly it stays typeable, and "branch (3) never fails the document (V-12 applies to the raw answer only)" is consistent; the row just does not say the fixture is `Split`-level. Pre-existing text (round 7's contract reviewer noted the same on fixture (2)). | §2.8 V-8; §7 fixture (3) figures; §2.6 branch (3). | "(a `Split`-level fixture; as a raw answer it needs a `reconciliation_note` to pass V-8)". |
| X-4 | Low | §4.3 sibling enumeration | The enumeration root is the literal `C:\Apps\Sibyla\worker\*`; the bench (`ExtractionRunner` under the production flag set, §5.7) runs from `tools/extraction-bench/bin`, where the literal would deny production releases (harmless) rather than its own siblings (none). Deriving the root from the release directory's parent (`AppContext.BaseDirectory`'s parent) covers both; the literal is production-only wording. | §4.3; §5.7. | "the parent of the release directory (`C:\Apps\Sibyla\worker` in production)". |

#### Owner questions Q-EX-0..25

All 26 recommendations remain buildable as written on this host; no new question, as the revision says. Verified this round where an artefact decides: **Q-EX-15** — `C:\Apps\Sibyla\tools\claude\claude.exe --version` → `2.1.259 (Claude Code)`, exit 0; **Q-EX-21** — `--verbose`, `--output-format`, `--restricted`, `--safe-mode`, `--settings`, `--permission-prompts`, `--no-session-persistence` present in `--help` as round 7 recorded; **Q-EX-16** — one release by the module holds by construction (`ArtifactTree` for Web/Api/Worker; the `artifact-refused` oracle); **Q-EX-22** — nothing in this review read `D:\ApolloData\worker-claude`; **Q-EX-18** — unchanged text. C-W-1/C-W-2 are spec sentences, not rulings.

#### Readiness for RED oracles

Ready. Every §7 row names an expected value and every seam it needs exists on main or is named in §4.3, including the new items: golden #10 line 5 (11.46 = key), the guard fixture (V-8 shape 2, V-9 `Σ all`, `Split` branch (3) with a zero difference and no finding), the third-shape reject case (neither V-8 shape, null note → refused), `PermissionsFile.Build` with two fake siblings (two deny lines from a pure static), `BuildArguments` with `stream-json` (X-1 decides where the flag lives, not its value at RED). Carried from rounds 6–7 unchanged and re-checked as unchanged text and unchanged code: `ExtractionContractV2Tests`, `SkillPackageTests`, `WorkerStartupTests`, `ProcessingEvidenceTests`, `QueueWorkerGateTests`, `DocumentsReviewV2Tests`/`IngestionServiceHoldTests`, `ClaudeCliTimeoutTests`, `EvidenceSecretScanTests`, `CompanyMatcherTests`/`AccountPeriodTests`, the rest of `ExtractionScoringTests`.

#### Verified myself

- Worktree head `601edf8`, clean; the spec (511 lines) in full; the review record (1,439 lines: rounds 1–7 verbatim and every disposition block, revisions 2–8) in full; `git diff 64e1f89 601edf8 -- docs/apollo-argus-extraction-v1-spec.md` (17/15) read hunk by hunk.
- Main `86820d6` (branch `main`; `git diff --stat 86820d6 HEAD -- src local tests Sibyla.slnx` empty — the code rounds 6–7 read): `ClaudeCli.cs` in full (sealed class, primary ctor, `RunAsync` 59–103 building `psi.ArgumentList` inline with `-p`, `--output-format json`, `--add-dir`, `--allowedTools Read`; `ResolveCliExecutable` 105–115), `ClaudeDocumentProcessor.cs` 26/38/79/82/103/108 (`DocumentToken`, `PromptHash`, sandbox, evidence beside the source), `QueueWorker.cs` 25–29/66–127/164/244–248/340/375 (slots, job-type switch, `PauseLaneAsync` and `CompleteAsync` private), the worker csproj (Sdk.Worker, no items) and `Properties/` (only `launchSettings.json`), `Sibyla.slnx` (`/src/`, `/tests/`; no `/tools/` yet), both `AssemblyInfo.cs` (Platform: Platform+Browser; Web: Browser), `Sibyla.Tests.Platform.csproj` 23–26 and `Sibyla.Tests.Argus.csproj` 24–27 (both reference the worker), `local/publish-release.ps1` in full (`-Hosts` default `web, api, worker`; `dotnet publish` only; release id `yyyyMMdd-HHmmss-commit7`), `local/provision-production.ps1` 33, 158–180, 197 (the release-id regex twice; `Set-ExactAcl` with `ContainerInherit,ObjectInherit`; worker ReadAndExecute on `worker` and `tools`, Modify on `staging`).
- Deployment module: `FileSystemWindowsAdapterV2.cs` 29, 180–210 (`Precondition.Artifact` → `Artifact` → three `ArtifactTree` calls; `EnumerateFiles` on the host path), `ValueObjectsV2.cs` 385–412 (`ReleaseId.TryParse`, failure code `PreconditionArtifact`), `FailureCodesV2.cs` 24, `execution-semantics.json` 199–205 (`artifact-refused`), `execution-order.json` 59–61/138/145/180 (PublishTree Web → Api → Worker; Recycle Pool:Web; Process.Stop/Start Service).
- Pinned CLI `C:\Apps\Sibyla\tools\claude\claude.exe`: `--version` (exit 0) and `--help` only — lines for `--add-dir`, `--allowedTools`, `--disable-slash-commands`, `--effort`, `--fallback-model`, `--model`, `--no-session-persistence`, `--output-format` (140–144), `--permission-prompts`, `-p`, `--restricted`, `--safe-mode`, `--settings`, `--strict-mcp-config`, `--tools`, `--verbose` (255). Never run on any input.
- Production layout: `C:\Apps\Sibyla\worker` listing (seven release directories matching the pattern, `CURRENT.txt`, `PREVIOUS.txt`); `C:\Apps\Sibyla\{api,tools,web,worker}`. No release contents opened.
- Skill build HEAD `a558523`, `status --porcelain` empty. PDFs via `pdftotext -raw`: Locarent NC #35 (`Gott/Payables/202608/Gott_CreditNote_Locarent_202608_01.pdf`, seven "Prestação nº 73 (01/07/2026 - 15/07/2026)" lines at raw 25–43).
- Keys (read-only, python, UTF-8): `I26050001.json` (header 448.88 / 103.24 / 552.12 EUR; the nine lines, line 5 = 9.32 / 2.14 / 11.46, line 2 = 439.48 / 101.08 / 540.56, line 8 = 0.07 / 0.02 / 0.09), `I26080018.json` and `I26080017.json` (`archivePath`). Arithmetic (Decimal, half-up): the nine AWS conversions incl. all three printed USD line totals; V-8 both shapes on fixtures (1)/(1′)/(2)/(3), the guard fixture and the reject case; `Split` on the guard fixture and fixture (2) with the step-A parenthetical.

#### Taken from the spec without verification

CLI runtime behaviour not exercised: the `Read` 2,000-character truncation, the `json` envelope's `num_turns`/`usage`/`modelUsage`, the `stream-json` trace and whether it needs `--verbose` (R-EX-2 probe (b)), the Windows `Read(...)` rule syntax and deny-over-allow precedence, whether `--settings` applies under `--safe-mode`, the usage-limit message the bench parses; that a `StartAsync` throw under `WindowsServiceLifetime` exits non-zero; the exact failure code the module reports when a host tree folder is absent (the adapter throws; the oracle covers the injected refusal); the .NET SDK csproj facts (rounds 4–6 evidence, not rebuilt); token/context estimates and the sitting arithmetic; the PDFs not opened this round (#10 AWS — its figures taken from round 7 and the key; #2, #6, #7, #12, #13, #21, #24, #25, #27, #31, Hydra, MEO, Mobilize, Anaptyxis, Via Verde, the five statements and their printed balances — rounds 3–7 verified them); `build_bnkmov.py` 77–84; `SyncEngineWave3.ToCashDelta`; `DocumentTypeRouter`; `AccountPeriodService` (rounds 6–7); `IngestionService`/`ChannelIntakeService`/migrations cited in §4.7 (rounds 5–7, code unchanged since); page counts; the content of `EXTRACT.md` (unwritten); the reserve entries' doclog rows; the "2 otherwise column-sliced" Revolut rows.

### Author's dispositions — revision 9 (build slice)

Revision 9 of `apollo-argus-extraction-v1-spec.md` (2026-09-07, branch `ops/argus-extraction-v1-red`, step R0 of the RED phase) is the amendment the closing verdict of round 8 carried into the build slice: the eight residual findings of the accepted round (contract 0/0/1/3, feasibility 0/0/0/4), applied exactly as that verdict states them, before the corresponding oracle is typed. Ids: `C-X-n` = the round-8 contract reviewer's X-1…X-4, `F-X-n` = the round-8 feasibility reviewer's X-1…X-4. Disposition: **fixed** (the spec now says it). "Where" names the section of revision 9. Nothing beyond these eight items is touched; any further spec change in this slice needs a new review round. No new owner question; D-EX-4 stands.

**Contract reviewer (C-X-1 … C-X-4)**

| id | Sev | Disposition | Where in revision 9 |
|---|---|---|---|
| C-X-1 | Medium | fixed — V-9's VAT clause reads: `Σ lines.vat = header.vat ± 0.02`, or — when `duty` is non-null and no `stamp_tax` line is printed — `Σ lines.vat + \|duty\| = header.vat ± 0.02` (the branch-(2) shape); fixture (2) now states one item line 100.00 with `vat_amount` 23.00 (the true VAT) and asserts V-9 passes on the raw answer (23.00 + 4.00 = 27.00) | §2.8 V-9; §7 scoring row, fixture (2) |
| C-X-2 | Low | fixed — V-8's second alternative is labelled "the duty-inside shape: branch (2), or branch (3) with a zero difference"; the guard fixture in §7 reads "V-8 (its second shape) and V-9 pass" — the label "the branch-(2) shape" is dropped there | §2.8 V-8; §7 scoring row, the guard fixture |
| C-X-3 | Low | fixed — the reject case reads "…, no printed stamp line, `reconciliation_note` null → neither shape, refused" (the refusal is V-12's) | §7 scoring row and the rejection-corpus row |
| C-X-4 | Low | fixed (= F-X-1) — `BuildArguments` adds `--verbose` after `--output-format stream-json` only when `const bool StreamJsonNeedsVerbose` on `ClaudeCliProcess` is true; `false` at RED, set by R-EX-2's record; the permissions test asserts the list for both values | §4.3 seams; §7 permissions row |

**Feasibility reviewer (F-X-1 … F-X-4)**

| id | Sev | Disposition | Where in revision 9 |
|---|---|---|---|
| F-X-1 | Low | fixed (= C-X-4) — as above: the constant on `ClaudeCliProcess`, `false` at RED, both values asserted | §4.3; §7 permissions row |
| F-X-2 | Low | fixed — `V-8-third-shape.json` is listed on the rejection-corpus row with its figures and the full path `tests/Sibyla.Tests.Platform/golden/extract-v2/reject/`; the scoring row says the case lives in the Platform rejection corpus and no longer carries the `reject/` file name itself | §7 rejection-corpus row; §7 scoring row |
| F-X-3 | Low | fixed — fixture (3) is marked "a `Split`-level fixture: as a raw answer it fails both V-8 shapes and needs a `reconciliation_note` to pass V-12" | §7 scoring row, fixture (3) |
| F-X-4 | Low | fixed — the sibling enumeration root is the parent of the release directory (`AppContext.BaseDirectory`'s parent — `C:\Apps\Sibyla\worker` in production; under the bench, `tools/extraction-bench/bin`'s parent, where there are no siblings); the literal is production-only wording | §4.3 `PermissionsFile.Build` |

Owner questions after revision 9: Q-EX-0…25 ruled by D-EX-4, unchanged; no new question. Revision 9 is not a review round: it enters the record so the checkpoint reviewers of §8 can verify the eight carried items against the diff `601edf8..<R0 commit>` of the spec and nothing else.
