feat: German invoices and corpus eval (0.5.0, stacked on #6) #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/nl-de-corpus"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #6: merge that first, then retarget this to main.
What
Measured on the generated corpus (200 NL + 200 DE, one layout per language)
Before 0/400 accepted. Now every field is right on every invoice read (NL 190, DE 175 once checksums are set aside); 0 silent errors; also 0 on 6 fresh clutter seeds. As generated, ~95% of IBANs and VAT numbers fail their checksums, so almost everything goes to review for that reason; that is the generator, not the reader. One layout per language: this does not give a real-world rate.
Checked
340 hermetic tests, ruff, import-linter, pre-commit.
🤖 Generated with Claude Code
🤖 Auto-review status ·
jonah/safe-ai-toolsPR #7📋 Review posted — fixes required, awaiting human action.
Last updated: 2026-10-06T20:30:10.968Z
[STATUS: FIXES_REQUIRED]
Review of
feat/nl-de-corpusvsfeat/invoice-to-csv(1 commit:23cea49, 21 files, +452/−50).Verification run:
pytest -m "not integration"→ 340 passed ·ruff check .→ clean ·lint-imports→ 4/4 contracts kept.Must fix
Silent gold corruption in the corpus loader —
eval/corpus.py:68,83,87:_invoicereads the VAT rate, subtotal, and VAT amount fromrows[0]only, then stamps that single rate onto every line (vat_rate=rate) and emits exactly oneVatLine. The format is per-row ("header fields repeated", and the CSV carries aBTW Tarief/MwSt Satzcolumn per line item), yet every other line field is read fromr. Verified: a gold CSV with rates[9, 21]loads as gold rates[9, 9]— a correct extraction then scores as wrong, silently, which is the worst failure mode for an accuracy harness. Readr[names["rate"]]per row and groupVatLines by rate (the pattern already exists indata/invoices/generate.py:_vat_block), orassertsingle-rate at load time.Malformed corpus crashes with raw tracebacks; missing PDFs are miscounted —
eval/corpus.py:99raises bareStopIterationon an unknown CSV header;corpus.py:62raisesValueErroron an empty/unreadable gold date;corpus.py:67raisesIndexErroron a header-only CSV. None are caught byeval/__main__.main(onlyFileNotFoundErroris), so the user gets a stack trace instead of theparser.errorusage path used everywhere else in that CLI. Conversely,corpus.py:101never checks the pairedpdf/<stem>.pdfexists — a missing PDF flows intoingest.py:23(except Exception→IngestError) and is scored as an extraction failure (unreadable PDF (FileNotFoundError)in the review-reasons table), silently inflating the refusal counts that CHANGELOG reports. Fail fast with a clear message on corpus-shape errors; check the PDF is present at load.New scoring logic ships untested —
eval/score.py:101-110,135-137:Report.reasons,_label(including itslayout not recognisedspecial case), and the top-10 markdown table are new behavior with zero assertions —test_eval_score.pywas only touched for an error-string rename. Project rule §1.VIII / ponytail: non-trivial logic leaves one runnable check. One small test: aRejectedwith("iban_checksum_failed: x", "layout not recognised: …")→ correctreasonsdict + table rows.Should fix (low)
eval/score.py:48,136:reasonscounts reason occurrences, not invoices — one invoice missing bothibanandsuppliercounts 2 undermissing_field, but the comment and the column header say "Invoices". Rename the column or count distinct invoices per label.ground.py:35-36: full-year forms exist padded and unpadded (3.5.2026), two-digit-year forms only padded (03.05.25).parse_dateaccepts3.5.25, so such an invoice extracts fine and then fails grounding → spurious review (fail-safe, but noise). Addf"{day.day}-{day.month}-{yy}"/f"{day.day}.{day.month}.{yy}"for symmetry.pdf/+csv/corpus is on no branch and in no directory (git grepacross all refs: only the loader exists). README documents--corpus DIRbut there's no way to regenerate the corpus the reported 190/175/0-silent-errors figures came from. Commit the generator or link where it lives.Checked and clean
_owncustomer-block hiding (header.py:114-130) — traced all branches: label-only line hides exactly_MAX_BLOCKlines, block-end line resets and stays visible, old drop-any-customer-line behavior preserved; German/customer tests cover it.slash_order+_slash_dateorder settling (numbers.py:69-92) — contradictory evidence →None→ refuse; tests cover all four cases.amount_convention(numbers.py:36-45) —2025.0006can no longer count as EN cents; fallback to the old 3–4 decimal pass preserved.header.py:109-110) — documented behavior change, fails safe (review, never accept),ExtractErrormessage is user-safe.validate.py:79-86) — missing/unknown VAT number skips only the rate check; the identifier/missing-field problem still fires, exactly as the CHANGELOG claims.VERSION, bothpyproject.toml,uv.lockall 0.5.0); euro-sign stripping intable.pycan only cause an extra grounding review, never a silent wrong output.🤖 Auto-review status ·
jonah/safe-ai-toolsPR #7📋 Review posted — fixes required, awaiting human action.
Last updated: 2026-10-06T21:40:54.752Z
[STATUS: FIXES_REQUIRED]
Review of
feat/nl-de-corpusvsfeat/invoice-to-csv— 2 commits (23cea49,3552156), 22 files. The new commit (3552156) is small and its core fix is sound, but all three must-fix items from the previous review are still unaddressed, plus one misleading artifact in the new commit.Verification run:
pytest -m "not integration"→ 341 passed (340 + 1 new) ·ruff check .→ clean ·lint-imports→ 4/4 contracts kept.Must fix — unaddressed from previous review (re-verified, all still reproduce)
Silent gold corruption in the corpus loader —
eval/corpus.py:67-87: unchanged since23cea49._invoicestill readsrate/subtotalfromrows[0]only and stamps it on every line (vat_rate=rate, line 83) and into a singleVatLine(line 87). Reproduced just now: a gold CSV with per-row rates[9, 21]loads as[9, 9]with one wrongVatLine— a correct extraction scores as wrong, silently.test_eval_corpus.pyonly uses single-rate fixtures, so it can't catch this. Readr[names["rate"]]per row and groupVatLines by rate, orassertsingle-rate at load.Malformed corpus crashes with raw tracebacks; missing PDFs miscounted —
eval/corpus.pyunchanged. Verified: unknown CSV header →StopIteration(line 99); header-only CSV →IndexError(line 67);maincatches onlyFileNotFoundError(eval/__main__.py:44), so all escape to a stack trace instead of theparser.errorpath. Andcorpus.py:101-102still never checkspdf/<stem>.pdfexists (grep exists() eval/→ none) — a missing PDF loads fine and is scored as an extraction failure, inflating the refusal counts the CHANGELOG reports.New scoring logic still ships untested —
eval/score.py:101-110,135-137:Report.reasons,_label(incl. thelayout not recognisedspecial case), and the top-10 table still have zero assertions (grep reasons\|_label test_eval_score.py→ no output). The test file's only change on this branch is an error-string rename. One small test with aRejectedcarrying("iban_checksum_failed: x", "layout not recognised: …")asserting thereasonsdict and table rows.Should fix — unaddressed from previous review
eval/score.py:48,136:reasonscounts reason occurrences (aCounterover all reasons of all rejected invoices — one invoice missing two fields undermissing_fieldcounts 2), but the comment says "how many invoices" and the column header says "Invoices". Rename the column or count distinct invoices per label.ground.py:35-36: full-year forms exist padded and unpadded (day.month.year), two-digit-year forms only padded (% 100:02d).parse_dateaccepts the unpadded form → extracts fine, fails grounding, spurious review. Addf"{day.day}-{day.month}-{yy}"/f"{day.day}.{day.month}-{yy}"for symmetry.CHANGELOG.md:21-24rewritten with new figures (DE 175→200 read, NL 24/200 and DE 26/200 accepted, "87% VAT fail", "DE 181+"), but thepdf/+csv/corpus generator is still in no directory on no ref (git grep "Leverancier Naam"across all refs → only the loader and its tests;data/invoices/generate.pywritespdfs/+gold.jsonl, a different format). The DE jump 175→200 is not explained by the one-line IBAN fix (the old 25 DE refusals were postcode-driven extraction failures, unrelated to IBAN) — it comes from the regenerated corpus, which nobody can reproduce. Commit the generator or link where it lives. Related:eval/__main__.py:45tells--corpususers "See data/invoices/generate.py", which cannot generate that format.New issues in
3552156rules/header.py:136:# "DE73 4811799" in a VAT number is not the start of an IBAN, but_VAT(line 64) isDE\d{9}contiguous — it doesn't match a spaced VAT. Verified:USt-IdNr.: DE73 4811799 | …on an IBAN line still yields two candidates (['DE734811799S6586704660', 'DE38362125644960532269']) →_ibanreturnsNone→ review. Fail-safe, not silent, but if PDF extraction or the invoice prints the VAT grouped/spaced the fix silently doesn't apply while the comment says it does. Either match the spaced form too or reword the comment to the contiguous case.Checked clean (new commit)
_VAT.submasking itself is correct: a real NL (18) or DE (22) IBAN can never match_VAT(the(?![A-Za-z0-9])lookahead fails mid-IBAN — verified), a compact-VAT line now yields exactly one candidate, and the added testtest_a_german_vat_number_on_the_same_line_is_not_taken_for_an_ibancovers the fix with a fullread_headerassertion.ust_idnr_de("DE136695976")→ True,btw_nl("NL820646660B01")→ True; the corpus-shapedNL550455977B28still fails as claimed.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.