ABSoW Stamp Clear — Centralized in Write Service

Date: 2026-07-30 (session 65) Type: decision + service change Commits: entirely-api a161f07 (code + tests), d7dc8c2 (workflow diagram)

Rule

The migration-026 invariant — any SOW line create/delete/scope edit clears projects.absow_reconciled_at/by — is enforced inside services/supabase_write_service.py, not in routes:

  • create_sow_line, create_synthetic_sow_line, delete_record (sow_lines), and update_sow_line (only when the write touches a scope key).
  • _SOW_SCOPE_KEYS = permitted quantity + linked product + linked project. As-built / inspected / notes / production-days / post_production writes do NOT clear — applying as-built IS the reconciliation, so field_inspection.update_absow (and therefore the ABSoW proposal apply path) never self-clears. No opt-out flag exists or is needed.
  • Billing pseudo-products exempt: product codes ADM-* and PRD-* (trip charges, rush fees, PRD-DAY production-day propagation) never clear the stamp — billing, not scope. Decision made with Dustin 2026-07-30.

Do NOT add per-route clears. The three cert_gen route-level clears were removed; every current and future SOW-mutating path (document parser reparse, field paths, intake, portal_files, whatsapp, trip charge, service-layer creators) inherits the centralized clear.

Audit trail

audit_log.db now records category absow: unreconcile (inside clear_absow_reconciliation with caller + prior stamp, and the manual portal route), reconcile (manual portal route), proposal_apply (absow_proposal_service.apply_proposal). Closes the attribution gap from the VAB-9002 stamp investigation (2026-07-30), where an out-of-band create_sow_line_from_product_code call added scope without touching the stamp and nothing recorded who/what changed reconciliation state.

Known edge (accepted)

Parent-service enrichment (create_synthetic_sow_line for missing WTR/ENC parents at cert-validation/invoice time) DOES clear the stamp — strict reading of the invariant. A reconciled project that gets enriched will need one re-reconcile round-trip; self-heals because the parent then exists.

cert-generation-v3 · confidence-routing · migration 026 · tests/test_absow_stamp_clear.py