Skip to content

Phase 5: Control gate and deterministic summary - #16

Merged
withrvr merged 1 commit into
claude/erp-collection-reporting-workflow-iycqtofrom
phase/05-control-gate
Aug 31, 2026
Merged

withrvr merged 1 commit into
claude/erp-collection-reporting-workflow-iycqtofrom
phase/05-control-gate

Conversation

@withrvr

@withrvr withrvr commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Phase 5: Control gate and deterministic summary

What this adds

The 5% exception-rate control gate (control/gate.py), never tuned to pass, reporting both denominators per QA_PREP.md Q8. A deterministic Jinja narrative (ai/fallback.py), no LLM, built first as the fallback chain's last rung. Run.status is now PASSED/BLOCKED/FAILED (retiring the Phase 3 placeholder COMPLETED). GET /collections/summary carries the full "blocked payload."

Assessment requirement served

MASTER_PLAN.md Phase 5 done-when: "dataset A blocks, dataset B passes, both visible via API." scripts/make_fixtures.py generates dataset_b_clean.xlsx (every row touched by any exception rule removed -- passes by construction), and both outcomes are asserted directly against real execute_run calls in tests/persistence/test_service.py and through the actual HTTP API in tests/api/test_api.py.

Docs updated

  • README.md - [x] ARCHITECTURE.md - [x] API.md
  • RULES.md (no rule changes) - [x] DEVELOPMENT.md - [x] CHANGELOG.md

Verification

uv run pytest app/collections/tests -v   # 142 passed (132 -> 142)
uv run mypy app/collections/control app/collections/ai/fallback.py app/collections/service.py app/collections/models.py app/collections/api   # clean (besides the pre-existing openpyxl stub gap)
uv run alembic check   # models.py matches the migration exactly

Known gaps carried to next phase

  • No LLM yet -- Phase 6 adds Ollama/LiteLLM above this deterministic fallback.
  • dataset_b_clean.xlsx has 0 exceptions (not just under 5%) -- functionally sufficient for this phase's done-when; Phase 9's fixture pass can hand-tune it to a nonzero-but-passing rate if a more realistic demo number is wanted.

Every run now either PASSES or is BLOCKED at the 5% exception-rate
gate -- never silently reported as clean, and never tuned to pass:
dataset A blocks (47.2%), dataset B passes (0%), both verified end to
end and visible via GET /collections/summary.

- control/gate.py: evaluate_gate, a pure function. QA_PREP.md Q8's
  denominator ambiguity ("exception records over invoice records")
  resolved by reporting both exception_row_rate (drives the gate) and
  distinct_invoice_rate, rather than picking one silently.
- ai/fallback.py: render_summary, a deterministic Jinja narrative --
  no LLM, no network, cannot fail short of a programming bug. Built
  first, as the last rung of Phase 6's LLM fallback chain.
- service.py: wires control + summarise into execute_run between
  calculate and persist.
- models.py: Run gains gate_threshold/exception_row_rate/
  distinct_invoices_affected/distinct_invoice_rate/narrative.
  RunStatus retires the placeholder COMPLETED for the real
  PASSED/BLOCKED outcomes -- no schema change needed, since status was
  already a plain indexed string, not a native enum.
- scripts/make_fixtures.py: generates fixtures/dataset_b_clean.xlsx by
  removing every row dataset A's own rules touch -- passes by
  construction, not hand-picked to look clean.
- api/summary.py: SummaryOut now carries the full "blocked payload"
  QA_PREP.md Q7 describes -- the rate, both denominators, the
  threshold, and the deterministic narrative.

Refs: docs/ARCHITECTURE.md's control gate section, docs/API.md
@withrvr
withrvr merged commit 6377c5a into claude/erp-collection-reporting-workflow-iycqto Aug 31, 2026
3 of 7 checks passed
@withrvr
withrvr deleted the phase/05-control-gate branch August 31, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant