# ADR-006: PRICE_UNMATCHED uses a frozen, source-replayed manual review

## Status

Accepted

## Date

2026-08-06

## Context

The fixed price table can legitimately lack an exact normalized price key. Treating every such result as a permanent
failure makes staff re-upload unchanged XML after a separate price decision, while accepting edited spreadsheets or
mutable price-table changes would create a second intake path and weaken provenance.

## Decision

Only a non-empty error set entirely composed of `PRICE_UNMATCHED` becomes `review_required`. ARR stores one
privacy-minimized review case per run, grouped missing keys, revisioned actor/price history and candidate comparisons.
It creates no Finance version, daily download, rejected Finance version, `arr.processing_failed`, or monthly event.

After every key has a non-negative integer value, ARR freezes canonical JSON bound to the job, case, original XML
SHA-256, business date, processor version, rule hash and complete issue set. The operator/API surface uses integer text
(including explicit `0`); ARR normalizes that value to exact `numeric(18,2)` and canonical `.00` manifest text before
replay. Finalization re-materializes the registered source XML and replays it with the fixed price table plus that
manifest. Independent validation must accept the same manifest before one atomic Finance-version activation, case
completion and `arr.daily_version_committed` event.

## Consequences

- `POST /api/jobs` remains XML-only; there is no Excel draft/download/upload-back workflow.
- Staff can change only a reviewed missing key's non-negative integer price, including explicit `0`; fixed pricing rules and XML fields
  remain immutable.
- Review mutations require session authentication, CSRF and optimistic revision checks. Frozen item/manifest facts are
  immutable and permanently audited without free-form notes or guest detail.
- Infrastructure failures after freeze become `generation_failed` and may retry with the identical manifest. A
  deterministic final replay failure closes the review and run as failed. Processor/rule identity drift requires cancel
  then fresh upload.
- The legacy `direct_mcp` path uses an isolated v3 success/failure projection: it rejects manual overrides and maps
  price misses to normal v3 failures, so it cannot gain review behavior from the active v4 processor.

## Related

- `database/017_daily_price_review.sql`
- `database/018_daily_review_manual_override_artifact.sql`
- `arr_web/programmatic.py`
- `arr_ingestion/postgres.py`
- `arr-opera-daily-ingest/scripts/process_daily.py`
