feat(parser): re-architect ReceiptParser into a rule/engine pipeline #77

Closed
opened 2026-07-07 09:24:15 +01:00 by TimCane · 0 comments
TimCane commented 2026-07-07 09:24:15 +01:00 (Migrated from github.com)

Re-architect ReceiptParser from one ~320-line static class into the
rule/engine pipeline sketched in the receipt-parser design notes, scoped to
restaurant/bar/cafe receipts.

Design

  • Decision: docs/adr/0006-receipt-parser-pipeline.md
  • Detector/rule/score reference: docs/15-receipt-parsing.md

Every sub-issue inherits these plus the invariants below; they are the durable
home for the detail (regex catalogue, layouts, scoring, OCR-misread maps), so
the issues stay terse and point at the docs.

Goal

  • Cleaner, extensible parser: small named rules instead of one growing method.
  • Parse more real receipts correctly, each new behaviour proven by a fixture.

Target shape

OcrResult -> normalize -> classify lines -> item rules + engine -> bill
detectors + engine -> validators -> ParsedReceipt. New code under
BillSplitter.Domain/Parsing/**, pure, no new dependencies.

Invariants (every sub-issue)

  • The 33-fixture corpus stays green. An existing expected.json changes only
    when a capability deliberately improves that receipt, with justification.
  • Facade ReceiptParser.Parse(OcrResult) stays static -> OcrWorker.cs:70
    never changes.
  • Money stays long minor units; the server owns all math.
  • No new persistence; discount/negative lines stay parked in Warnings.
  • Diagnostics are in-memory / test-only. No receipt text in prod logs.
  • Contract docs updated in the same PR (docs/06 parsing, docs/11 testing). No
    wire-contract change is expected, so docs/03-05 stay untouched.

Phase A - structural extraction (behaviour-preserving, one PR per layer)

  • A1 scaffold parsing pipeline skeleton and static facade
  • A2 extract text normalizer
  • A3 extract line classifier
  • A4 extract item rules and selection engine
  • A5 extract bill detectors and engine
  • A6 add in-memory parse-decision trace

Phase B - capabilities (each ships with new fixtures)

  • B7 warn when receipt totals do not reconcile
  • B8 fix OCR character misreads in money spans
  • B9 attach modifier lines to their item
  • B10 merge wrapped item names across lines (gated)
  • B11 reorder scrambled lines by bounding box (last, riskiest)
  • B12 de-duplicate repeated receipt copies

Sequencing: A1->A5 in order, A6 after A5; then B7/B8/B9, then B10, then B11,
then B12.

Out of scope

  • Weight-priced, fuel, and supermarket layouts.
  • Multi-currency / locale-decimal expansion beyond the current corpus.
  • Modelling discounts in the domain - its own future epic (Bill.DiscountMinor
    plus Redis, DTO, docs/02-06).
  • Any prod logging of parse decisions.
Re-architect `ReceiptParser` from one ~320-line static class into the rule/engine pipeline sketched in the receipt-parser design notes, scoped to restaurant/bar/cafe receipts. ## Design - Decision: `docs/adr/0006-receipt-parser-pipeline.md` - Detector/rule/score reference: `docs/15-receipt-parsing.md` Every sub-issue inherits these plus the invariants below; they are the durable home for the detail (regex catalogue, layouts, scoring, OCR-misread maps), so the issues stay terse and point at the docs. ## Goal - Cleaner, extensible parser: small named rules instead of one growing method. - Parse more real receipts correctly, each new behaviour proven by a fixture. ## Target shape `OcrResult` -> normalize -> classify lines -> item rules + engine -> bill detectors + engine -> validators -> `ParsedReceipt`. New code under `BillSplitter.Domain/Parsing/**`, pure, no new dependencies. ## Invariants (every sub-issue) - The 33-fixture corpus stays green. An existing `expected.json` changes only when a capability deliberately improves that receipt, with justification. - Facade `ReceiptParser.Parse(OcrResult)` stays static -> `OcrWorker.cs:70` never changes. - Money stays `long` minor units; the server owns all math. - No new persistence; discount/negative lines stay parked in `Warnings`. - Diagnostics are in-memory / test-only. No receipt text in prod logs. - Contract docs updated in the same PR (docs/06 parsing, docs/11 testing). No wire-contract change is expected, so docs/03-05 stay untouched. ## Phase A - structural extraction (behaviour-preserving, one PR per layer) - [x] A1 scaffold parsing pipeline skeleton and static facade - [x] A2 extract text normalizer - [x] A3 extract line classifier - [x] A4 extract item rules and selection engine - [x] A5 extract bill detectors and engine - [x] A6 add in-memory parse-decision trace ## Phase B - capabilities (each ships with new fixtures) - [x] B7 warn when receipt totals do not reconcile - [x] B8 fix OCR character misreads in money spans - [x] B9 attach modifier lines to their item - [x] B10 merge wrapped item names across lines (gated) - [x] B11 reorder scrambled lines by bounding box (last, riskiest) - [x] B12 de-duplicate repeated receipt copies Sequencing: A1->A5 in order, A6 after A5; then B7/B8/B9, then B10, then B11, then B12. ## Out of scope - Weight-priced, fuel, and supermarket layouts. - Multi-currency / locale-decimal expansion beyond the current corpus. - Modelling discounts in the domain - its own future epic (`Bill.DiscountMinor` plus Redis, DTO, docs/02-06). - Any prod logging of parse decisions.
Sign in to join this conversation.
No description provided.