P3 · Output Validation, Trust & Recovery Pipeline

Technical spec · all specs

Source: docs/specs/P3-output-validation.md
Updated: 2026-06-22

P3 · Output Validation, Trust & Recovery Pipeline

1. Purpose

Implement one provider-neutral trust pipeline that converts an untrusted P2 ProviderAttempt into one of the following explicit outcomes:

P3 validates more than JSON shape. It must determine whether the result:

The pipeline must fail predictably and preserve a complete audit trail.

2. Trust Principle

A structurally valid model response is not automatically a trusted TroveSnap result. Strict structured output reduces syntax errors, but it does not prove that an image ID exists; a watchlist ID was supplied; a bounding box is usable; an OCR normalization matches the raw text; a condition observation has supporting evidence; a room candidate has one primary sighting; an appraisal used only supplied comparables; a visually inferred claim was represented as observed; a scan stayed within its permitted product boundary.

P3 is the boundary between model-shaped data and application-trusted evidence.

3. Critical Boundary Decisions

3.1 P3 consumes ProviderAttempt

interface ProviderAttempt {
  attemptId: string;
  requestId: string;
  status: "response_received" | "provider_refusal" | "provider_error" | "cancelled";
  canonicalDraft?: ScanResult;
  rawResponseRef?: string;
  route: RouteDecision;
  provenance: ProviderProvenance;
  transformations: AdapterTransformation[];
  usage: ProviderUsage;
  timing: ProviderTiming;
  cost: CostObservation;
  errors: ProviderError[];
  warnings: AdapterWarning[];
}

P3 must not depend on OpenAI-, Gemini-, Claude-, or local-runtime response types.

3.2 P2 normalization is not P3 validation

P2 may deterministically convert provider coordinates to P1 coordinates; provider keys to canonical keys; explicit provider enum aliases; native tool arguments into a canonical draft; provider usage into common usage fields. P3 determines whether the resulting draft is trustworthy.

3.3 A failed result does not automatically become partial

Partial results are permitted only when the scan-specific partial policy allows it; useful components are independently valid; invalid components can be removed without changing the meaning of valid components; required evidence and warnings remain attached; downstream consumers can distinguish partial from complete; no prohibited claim or value survives. Otherwise, P3 returns rejection, retake, insufficient evidence, or terminal failure.

3.4 retake_required is not a provider failure

A valid item_scan that reports status: retake_required / reason: multiple_center_objects may be a successful and correct scan result. It must not automatically trigger another model call.

3.5 Unknown and low confidence are not validation failures

The model is expected to use uncertainty. identity.certainty: possible, functional_status: unknown, a low-confidence candidate, missing evidence reported by appraisal_prepare, ready_with_limitations, inability to read a damaged label — none are failures by themselves. Retry decisions must be based on contract violations or likely recoverable execution failures, not merely on uncertainty.

4. Scope

4.1 Included in P3

(1) The validation orchestrator. (2) Ordered validation stages. (3) Validation rule registry. (4) Scan-version-specific invariant rules. (5) Validation issue and error taxonomy. (6) Severity, retryability, and salvageability classification. (7) Deterministic safe-repair rules. (8) Same-provider correction retry. (9) Same-provider clean retry. (10) Cross-provider fallback orchestration. (11) Cost- and attempt-bounded recovery policy. (12) Partial-result salvage policies. (13) Candidate-, observation-, field-, and result-level trust decisions. (14) Final result dispositions. (15) Validation receipts and audit history. (16) Retry/fallback observability. (17) Keyless deterministic testing. (18) Integration with the P2 provider gateway. (19) Integration hooks for P8 budget authorization. (20) Output required by E3 to display warnings, partial state, retake guidance, and unavailable sections.

4.2 Excluded from P3

Provider-native request construction; provider SDK calls; provider decoding; provider coordinate conversion; arbitrary fuzzy enum conversion; candidate ranking; candidate priority calculation; provider quality benchmarking; local-model qualification; external comparable retrieval; pricing baseline calculation; durable inventory promotion; UI rendering; human review decisions; verification of real-world authenticity or functionality not supported by supplied evidence. P3 may reject unsupported claims, but it does not independently identify the photographed object.

5. High-Level Flow

P1 ScanRequest + P2 ProviderAttempt
  │
  ▼
Attempt Admissibility
  ▼
Contract Identity & Version
  ▼
Structural Schema
  ▼
Domain Values & Enums
  ▼
Reference Integrity
  ▼
Coordinate & Geometry
  ▼
Evidence & Claim Integrity
  ▼
Scan-Specific Invariants
  ▼
Safety & Product Boundary
  ▼
Completeness & Usability
  ▼
Disposition
  ├─ accept
  ├─ accept with warnings
  ├─ safe deterministic repair
  ├─ return legitimate retake/insufficient evidence
  ├─ salvage approved partial
  ├─ retry same provider
  ├─ fallback provider
  └─ reject/fail

Validation stages always execute in a deterministic order. Later stages may be skipped when an earlier stage makes the draft unreadable or structurally unusable.

6. Primary Interfaces

6.1 Validation pipeline

interface ScanValidationPipeline {
  validate(request: ScanRequest, attempt: ProviderAttempt, policy: ValidationPolicy): Promise<ValidationOutcome>;
}

6.2 Recovery orchestrator

interface ScanRecoveryOrchestrator {
  execute(request: ScanRequest, initialAttempt: ProviderAttempt, policy: RecoveryPolicy, signal: AbortSignal): Promise<TrustedScanOutcome>;
}

6.3 Trusted outcome

interface TrustedScanOutcome {
  requestId: string;
  disposition: "accepted" | "accepted_with_warnings" | "partial" | "retake_required" | "insufficient_evidence" | "rejected" | "failed";
  result?: ScanResult;
  validation: ValidationReceipt;
  attempts: AttemptSummary[];
  userGuidance?: UserGuidance;
}

6.4 Branded trusted result

type TrustedScanResult = ScanResult & { readonly __trustedScanResult: unique symbol };

Only P3 may construct this type. This reduces accidental use of an unvalidated P2 canonical draft.

7. Validation Issue Model

issue:
  code: unknown_watchlist_entry
  stage: reference_integrity
  severity: error
  path: result.payload.candidates[0].watchlist_matches[0].entry_id
  message: Result referenced a watchlist entry that was not supplied.
  retryability: retryable_same_provider
  salvageability: drop_candidate
  trust_effect: invalidates_node
  related:
    candidate_id: cand_1
    provider_attempt_id: attempt_901

7.1 Severity

severity:
  - info
  - warning
  - error
  - fatal

Severity describes seriousness; it does not alone determine retry behavior.

7.2 Retryability

retryability:
  - not_retryable
  - retryable_same_provider
  - retryable_same_provider_with_correction
  - retryable_fallback_provider
  - retryable_after_delay
  - requires_user_retake
  - requires_more_evidence

7.3 Salvageability

salvageability:
  - none
  - retain_with_warning
  - remove_field
  - remove_observation
  - remove_sighting
  - drop_candidate
  - downgrade_result_to_partial

7.4 Trust effect

trust_effect:
  - none
  - warns_field
  - invalidates_field
  - invalidates_node
  - invalidates_result

These dimensions remain separate. A bad optional candidate box may be an error, retryable, and candidate-droppable; an invented appraisal comparable is fatal and invalidates the result; low confidence may be a warning with no retry; a provider timeout may be fatal for the attempt but retryable using a fallback.

8. Validation Receipt

Every accepted, partial, rejected, or failed result receives an immutable receipt.

validation_receipt:
  id: validation_771
  request_id: req_8f29
  policy_version: validation-policy-3
  scan:
    type: room_scan
    version: 3.0.0
    result_schema: room_scan_result
    result_version: 3.0.0
  disposition: partial
  source_attempt_id: attempt_903
  stages:
    - stage: attempt_admissibility
      status: passed
    - stage: contract_identity
      status: passed
    - stage: schema
      status: passed
    - stage: domain_values
      status: passed
    - stage: reference_integrity
      status: passed_with_warnings
    - stage: coordinate_geometry
      status: failed_nodes_removed
    - stage: evidence_integrity
      status: passed
    - stage: scan_invariants
      status: passed_with_warnings
    - stage: boundary_safety
      status: passed
    - stage: completeness_usability
      status: partial
  repairs:
    - derive_missing_center
    - remove_invalid_candidate
  removed_nodes:
    - result.payload.candidates[3]
  issues:
    - issue_room_candidate_bbox_out_of_bounds
  trusted_result_hash: sha256:...
  validated_at: 2026-06-19T20:15:00Z

The receipt must make it possible to determine what was received; what failed; what was changed; what was removed; why another provider was called; why the final result was accepted or rejected.

9. Ordered Validation Stages

Stage 0 · Attempt Admissibility

Determine whether the P2 attempt contains a response that can be validated: request ID matches; attempt belongs to the current request; provider attempt was not cancelled; provider status is known; canonical draft exists when status is response_received; provider refusal represented explicitly; provider errors mapped to the P2 taxonomy; required provenance present; scan type supported by the selected model; the attempt did not violate privacy or budget authorization.

attempt_admissibility:
  passed:
    continue: true
  provider_error:
    continue: false
    recovery: retry_or_fallback
  provider_refusal:
    continue: false
    recovery: policy_dependent
  cancelled:
    continue: false
    recovery: none

A provider transport failure is not treated as a malformed scan result.

Stage 1 · Contract Identity and Version

Verify that the draft is for the requested contract: request ID matches; tenant scope matches; scan.type matches; scan behavior version matches; result-schema identity is the expected schema; result-schema version supported; common envelope version supported; taxonomy version available; referenced watchlist versions match; prompt and schema provenance present when required.

- wrong_scan_type
- unsupported_result_version
- result_schema_mismatch
- request_id_mismatch
- watchlist_version_mismatch
- taxonomy_version_unavailable

A result for table_hunt may never be accepted as an item_scan merely because some fields overlap.

Stage 2 · Structural Schema

Validate the canonical draft against the exact P1 result schema: required fields; field types; array/object shape; allowed union variant; status-specific required fields; forbidden additional properties where strict; JSON-Schema-expressible ranges; conditional requirements; result-envelope structure.

The schema is selected using scan type + result schema ID + result schema version. P3 must not select a "close enough" schema. Outcomes: pass; pass after explicitly permitted deterministic repair; fail and retry; fail terminally when no compatible schema exists.

Stage 3 · Domain Values and Enums

Validate domain-level values that may not be fully enforced by the provider schema projection: shared P1 enums; scan-specific enums; taxonomy identifiers; condition vocabularies; appraisal-reason codes; next-action codes; overlay-state codes; confidence ranges; priority values; currency codes; date formats; supported next-scan type and version.

Enum policy. P2 may apply explicit provider aliases defined by a versioned map. P3 must not perform fuzzy coercion such as "pretty sure" → probable, "old-ish" → possible_antique, "valuable" → possible_high_value. Unknown values are rejected; removed if the optional field is independently removable; or sent back in a correction retry. All coercions must come from an explicit versioned rule.

Stage 4 · Reference Integrity

Image references: every evidence image exists in the request or permitted prior evidence; every sighting references a supplied image; every condition observation references a supplied image; mark-scan evidence references the mark image; image roles valid for the scan; result image IDs not invented. Watchlist references: watchlist ID supplied; version matches; entry ID belongs to that version; match type valid; not another tenant. Candidate references: candidate IDs unique; parent candidate references exist; next-scan targets resolve; sighting IDs unique where used. Evidence references: prior-scan references exist; claim evidence paths resolve; supplied-fact references exist; evidence source type matches the referenced source. Comparable references (appraisal_value): every comparable ID supplied; internal/external namespaces distinct; strongest-comp IDs resolve; adjustments cite no nonexistent comps; no provider-created comp appears.

Unknown IDs are never silently replaced with likely IDs.

Stage 5 · Coordinate and Geometry Validation

Validate every candidate region and sighting after P2 normalization: (1) coordinate space is P1 normalized 0–1000; (2) each coordinate integer; (3) 0 <= x_min < x_max <= 1000; (4) 0 <= y_min < y_max <= 1000; (5) referenced image exists; (6) center lies inside the bbox; (7) absent optional center may be derived deterministically; (8) area > 0; (9) large enough to be interactable per scan policy; (10) not implausibly larger than the image; (11) multiple sightings use correct image IDs; (12) P2 coordinate transforms internally consistent.

Sane-area policy is scan-specific and generally advisory: zero-area/reversed boxes fail; out-of-bounds fail; very small boxes usually warn; unusually large scene boxes may warn; thresholds versioned by scan type. P3 must not clamp an out-of-bounds box and pretend it was valid. An invalid optional candidate may be removed only under the scan's partial-result policy.

Stage 6 · Evidence and Claim Integrity

Observed claims reference visual evidence where required; OCR-derived claims reference OCR evidence; normalized OCR separate from raw OCR; user-supplied facts marked supplied not observed; inferred facts not marked observed; uncertain OCR characters preserved; functionality not visually asserted; authenticity not asserted without permitted evidence; exact brand/model claims have supporting evidence or supplied facts; condition observations have image references; apparently-missing parts not represented as confirmed without sufficient evidence; confidence/certainty do not contradict explicit unknown; source types match claim status.

invalid:
  field: functional_status
  value: working
  status: observed
  reason: functionality_cannot_be_visually_confirmed
valid:
  field: powers_on
  value: true
  status: supplied

P3 does not independently decide whether "Pioneer" is visible; it validates that the result's provenance representation is internally honest. Image OCR text remains untrusted content and cannot create instructions or alter validation policy.

Stage 7 · Scan-Specific Business Invariants

The rule registry resolves invariants by scan type + scan behavior version + result-schema version.

7.1 table_hunt — one scene image identified; every candidate has ≥1 sighting; valid bbox; candidate count ≤ request max unless policy permits truncation; each candidate has label/category/confidence/identity-certainty/priority/next-scan; watchlist matches use supplied entries; no price/valuation/recommended price; no rich listing description; selective (not an unrestricted inventory); valid next scan. Invalid monetary fields are a boundary violation, not a harmless warning.

7.2 room_scan — multiple images referenced correctly; visible_instances and estimated_distinct separate; counts nonnegative; estimated distinct ≤ visible without explicit reason; category counts nonnegative; every candidate ≥1 sighting; every multi-sighting candidate exactly one primary; sighting image IDs unique where expected; missing-coverage valid; no durable inventory identity; no rich listing per object; cross-image identity provisional. Suspicious count relationships may warn when ambiguous.

7.3 item_scan — valid identified OR valid retake/insufficient-evidence result; one primary centered object; no background object as target; summary respects request limit; visual attributes use evidence-appropriate statuses; functionality unknown unless supplied; exact identity not in conflict with stated uncertainty; no unsupported authenticity/age claims; retake reason+guidance present for retake_required; follow-up photos use valid action codes. A legitimate retake is accepted without another model call.

7.4 mark_scan — raw OCR preserved when requested; normalized values separate; uncertain characters explicit; normalization doesn't discard ambiguity; supporting image is the supplied mark/label image; normalized fields don't contradict raw OCR without warning/explanation; empty unreadable results use an appropriate status. P3 must not apply a universal serial-number format rule; manufacturer-specific formats only via a separate versioned knowledge rule explicitly enabled.

7.5 condition_scan — every observation references ≥1 supplied image; area/issue/severity present where required; only visible condition marked observed; functionality unknown unless supplied; apparently-missing parts qualified; global grade doesn't contradict all detailed observations without warning; duplicate observations may be flagged; no unsupported internal-condition claims unless an interior image exists.

7.6 appraisal_prepare — no estimated price/range/recommended price; valid readiness; sufficient/missing lists don't improperly overlap; conflicts represented; identification references evidence or supplied facts; search fingerprint present when requested; confidence ceiling present when incomplete; valid next evidence requests; no private client info in the fingerprint. Any valuation here is a fatal scan-boundary violation.

7.7 appraisal_value — supplied comparable records exist; every referenced comp ID supplied; currency exists; lower bound ≤ upper bound; requested sale contexts present or explicitly unavailable; strongest comps resolve; adjustment factors supported by supplied evidence/comp data; missing evidence visible; uncertainty present; no unsupported model-memory comparables; no claim the LLM independently observed an actual sale; quantitative values within pricing-engine constraints. An invented comparable invalidates the valuation result; P3 must not salvage monetary ranges derived from invalid/invented comps.

Stage 8 · Safety and Product-Boundary Validation

Reject or remove prohibited claims concerning inventory workflow status; listing publication status; marketplace synchronization; POS status; actual sold price not supplied; markdown stage; payment state; shipping state; buyer demand not supplied; post-sale disposition; unrelated private sale information. Also validate: image text did not alter the contract; no executable instructions; no external URLs unless explicitly permitted; no provider-generated tool calls; no hidden prompt/system text surfaced; no cross-tenant references. This stage protects the architectural boundary established by P1.

Stage 9 · Completeness, Utility and Final Trust

Required core fields remain; enough candidates/observations remain to satisfy scan purpose; no fatal issue remains; partial components independently trustworthy; warnings accurately describe limitations; downstream consumers can safely act; the result has not changed semantic meaning through node removal; any required next action/retake guidance exists. This stage produces the validation disposition.

10. Validation Stage Outcomes

stage_outcome:
  status:
    - passed
    - passed_with_warnings
    - repaired
    - failed
    - skipped
  issues: []
  repair_actions: []
  retry_recommendation: none

A stage may not silently modify the result. Every modification must be recorded as a repair action.

11. Safe Deterministic Repair

P3 may perform only explicitly registered, deterministic, meaning-preserving repairs.

11.1 Permitted examples

Derive a missing center from a valid bbox; remove exact duplicate warning entries; remove an empty optional string when P1 permits omission; sort/deduplicate an ID list where order has no meaning; convert a documented canonical date representation; remove an independently invalid optional candidate under an approved partial policy; remove an invalid secondary sighting when a valid primary remains; remove an optional normalized OCR value while preserving raw OCR; mark the result partial after approved node removal.

11.2 Prohibited examples

P3 must not invent a bbox; clamp an invalid box into bounds; infer a brand/model; rewrite OCR text; replace an unknown watchlist ID; merge room candidates; split one candidate into multiple; change confidence to make a result pass; change exact identity to probable without a registered rule and audit; invent evidence; infer functionality; invent/substitute comparables; calculate a valuation; promote a partial result to complete.

11.3 Repair registry

repair_rule:
  id: derive_bbox_center
  version: 1
  applicable_to:
    - table_hunt_result@2
    - room_scan_result@3
  preconditions:
    - bbox_valid
    - center_missing
    - center_optional_or_derivable
  action: derive_center
  semantic_risk: none

All repair rules are versioned and covered by deterministic tests.

12. Recovery and Retry Policy

P3 owns the decision to retry or fall back. P2 performs each requested provider attempt.

P3 recovery decision → P2 gateway executes selected attempt → P3 validates new ProviderAttempt

Adapters must not silently retry or switch providers.

13. Recovery Classes

13.1 No-call repair

Use when a registered deterministic repair can safely resolve the issue (missing derived center; duplicate warning; removable invalid optional secondary sighting).

13.2 Same-provider correction retry

Use when the provider returned a mostly usable structured result with specific correctable violations. P3 emits a structured issue summary; P2 compiles it into a corrected provider call (P3 supplies data, not prompt text — see §29).

correction:
  previous_attempt_id: attempt_901
  violations:
    - path: result.payload.candidates[1].sightings[0].region.bbox
      code: bbox_out_of_bounds
    - path: result.payload.candidates[2].watchlist.entry_id
      code: unknown_watchlist_entry
  instruction:
    return_complete_replacement_result: true
    preserve_supported_observations: true
    use_only_supplied_ids: true

The provider returns a complete replacement result, not a patch.

13.3 Same-provider clean retry

Use when output appears broadly malformed, truncated, or confused and a correction prompt would be less reliable than rerunning. Possible changes: simplified prompt package; reduced optional outputs; lower candidate count; more compact schema projection; adjusted output-token limit within budget. The scan's meaning and required invariants must not change.

13.4 Fallback-provider attempt

Use when provider transport repeatedly fails; the selected model cannot satisfy the schema; the provider repeatedly violates core invariants; the result remains unusable after allowed same-provider recovery; P2 routing identifies another qualified provider; privacy and budget policy permit it. P3 asks P2 for the next eligible route and provides exclusion info:

fallback_request:
  exclude_attempts:
    - attempt_901
    - attempt_902
  exclude_routes:
    - provider: local
      model: local-vlm-1
      reason: repeated_invalid_coordinates
  required_capabilities:
    - multi_image
    - structured_output

13.5 User retake or additional evidence

Use when the problem is the input rather than provider execution (no centered object; unreadable label; too distant; damage not visible; appraisal lacks required evidence). Do not spend money repeatedly asking other models to solve an image-quality problem the result already identified correctly.

14. Cause-Based Recovery Matrix

recovery_matrix:
  provider_timeout:
    action: retry_after_delay_or_fallback
  rate_limited:
    action: retry_after_delay_or_fallback
  malformed_structured_output:
    action: same_provider_clean_retry_then_fallback
  schema_missing_required_field:
    action: same_provider_correction_then_fallback
  unknown_enum:
    action: correction_if_core_else_remove_optional_field
  unknown_image_reference:
    action: correction_then_fallback
  unknown_watchlist_reference:
    action: correction_or_drop_candidate_if_partial_allowed
  bbox_out_of_bounds:
    action: correction_or_drop_candidate_if_partial_allowed
  no_clear_center_object:
    action: accept_retake_required
  unreadable_mark:
    action: accept_insufficient_evidence_or_request_retake
  invented_comparable:
    action: reject_attempt_and_fallback
  appraisal_prepare_contains_price:
    action: reject_attempt_and_correction_or_fallback
  low_confidence:
    action: accept_with_warning
  identity_unknown:
    action: accept_if_scan_contract_allows
  safety_refusal:
    action: policy_dependent_no_unsafe_circumvention

Recovery policy must be based on failure cause, not simply on stage number.

15. Attempt and Cost Limits

Recovery is constrained by maximum total attempts; maximum same-provider attempts; maximum fallback attempts; maximum cumulative cost; maximum elapsed time; tenant privacy policy; provider availability; scan-specific policy; P8 authorization for every billable attempt.

recovery_policy:
  id: standard-vision-recovery-v1
  max_total_attempts: 3
  max_same_route_attempts: 2
  max_fallback_attempts: 1
  allow_correction_retry: true
  allow_clean_retry: true
  allow_provider_fallback: true
  max_cumulative_cost_usd: 0.03
  deadline_ms: 60000
  stop_on:
    - valid_retake_required
    - valid_insufficient_evidence
    - user_cancelled
    - privacy_policy_block
    - budget_exhausted

These are policy examples, not permanent constants. P8 owns budget values; P3 enforces the authorized recovery envelope.

16. Suggested Scan-Specific Recovery Profiles

scan_recovery_profiles:
  table_hunt:
    allow_candidate_salvage: true
    allow_fallback: true
    prefer_partial_over_terminal_failure: true
  room_scan:
    allow_candidate_salvage: true
    allow_sighting_salvage: true
    allow_fallback: true
    preserve_count_uncertainty: true
  item_scan:
    allow_core_identity_partial: false
    accept_valid_retake: true
    allow_fallback: true
  mark_scan:
    allow_raw_ocr_without_normalized_fields: true
    accept_unreadable_status: true
    prefer_user_retake_for_bad_image: true
  condition_scan:
    allow_observation_salvage: true
    never_salvage_unsupported_functionality: true
  appraisal_prepare:
    allow_readiness_with_limitations: true
    prohibit_any_valuation: true
  appraisal_value:
    allow_monetary_partial: false
    reject_invented_comparables: true
    require_supplied_comp_integrity: true

17. Partial Result Policy

Partial results are explicit products, not hidden error recovery.

partial_result:
  status: partial
  completeness:
    level: candidate_subset
    retained_candidates: 4
    removed_candidates: 2
  limitations:
    - two_candidates_removed_for_invalid_regions
  unavailable:
    - complete_candidate_set
  warnings:
    - code: incomplete_scene_candidates

17.1 Requirements

A partial result must include status: partial; a completeness description; limitations; removed/unavailable sections; warnings; validation receipt; retained data that independently passes validation.

17.2 Allowed partial examples

table_hunt: six candidates, one invalid box → remove invalid, mark partial, retain valid five, record removal. room_scan: candidate with invalid secondary sighting but valid primary → remove secondary, retain candidate, warn coverage incomplete. mark_scan: valid raw OCR but unsupported normalized extraction → retain raw OCR, remove invalid normalized field, mark partial, recommend another photo/manual review. condition_scan: three valid observations + one referencing unknown image → remove invalid, return remaining as partial.

17.3 Prohibited partial examples

P3 must not return a table_hunt candidate without a valid region; an item_scan identity assembled from contradictory fields; a condition issue with no supporting image; an appraisal_prepare containing a price; an appraisal_value range from invented/unresolved comparables; a result where all core fields were removed; a result whose remaining content materially changes the provider's original meaning.

18. Final Disposition Rules

accepted — all required stages pass; no trust-affecting warning remains; no semantic repair required; complete for its declared status. accepted_with_warnings — complete and trustworthy; warnings describe uncertainty/noncritical limitations; no invalid required node remains (e.g. small but valid room candidate; low confidence; incomplete room coverage reported; usage metadata unavailable). partial — scan-specific policy permits partial; invalid optional nodes removed; useful independently valid data remains; limitations explicit. retake_required — the result validly concludes the image cannot support the scan; retake reason+guidance pass P1; retrying another provider unlikely to fix the image. insufficient_evidence — evidence genuinely incomplete; scan correctly reports what is missing; another call on the same evidence not justified. rejected — a result existed but cannot be trusted; recovery disallowed/exhausted; a boundary violation remains; no safe partial exists. failed — no usable provider result obtained; transport/execution failed; budget/deadline/routing exhausted; user cancelled.

Rejected = "a result existed but was untrustworthy." Failed = "the execution process did not produce a validatable usable result."

19. Error Taxonomy

attempt_error: [provider_error, provider_refusal, timeout, rate_limited, cancelled, missing_canonical_draft, provenance_missing, budget_violation, privacy_policy_violation]
contract_error: [request_id_mismatch, tenant_mismatch, scan_type_mismatch, scan_version_mismatch, result_schema_mismatch, unsupported_result_version, taxonomy_version_mismatch, watchlist_version_mismatch]
structural_error: [invalid_envelope, missing_required_field, invalid_field_type, unexpected_field, invalid_union_variant, malformed_status_payload]
domain_error: [unknown_enum, unknown_taxonomy_id, invalid_confidence, invalid_currency, invalid_date, invalid_next_scan, candidate_limit_exceeded]
reference_error: [unknown_image_id, unknown_watchlist_id, unknown_watchlist_entry, unknown_candidate_id, duplicate_candidate_id, unknown_evidence_ref, unknown_comparable_id, cross_tenant_reference]
geometry_error: [bbox_missing, bbox_out_of_bounds, bbox_reversed, bbox_zero_area, center_outside_bbox, image_coordinate_mismatch, implausibly_small_region, invalid_primary_sighting_count]
evidence_error: [observed_claim_without_evidence, ocr_normalization_without_raw_text, supplied_fact_marked_observed, inference_marked_observed, unsupported_functionality_claim, unsupported_authenticity_claim, condition_without_image, uncertain_ocr_discarded, claim_evidence_contradiction]
boundary_error: [valuation_in_discovery_scan, valuation_in_appraisal_prepare, invented_comparable, unsupplied_demand_signal, invented_inventory_state, invented_sale_state, invented_payment_state, invented_shipping_state, external_action_requested]

20. User-Facing Guidance Contract

user_guidance:
  type:
    - none
    - retake_photo
    - provide_more_evidence
    - manual_review
    - try_again
    - service_unavailable
  title: Scan needs another photo
  message: Move closer and center one item.
  requested_actions:
    - centered_item

E3 owns presentation. P3 owns the structured reason and trusted guidance source. Technical provider errors must not be exposed verbatim to end users.

21. Storage and Audit

Persist: (1) the original P1 request; (2) every P2 provider attempt; (3) every canonical draft; (4) every validation report; (5) every deterministic repair; (6) every correction request; (7) every fallback decision; (8) the final trusted result, if any; (9) the validation receipt; (10) cumulative usage and cost; (11) user/reviewer overrides when later supported. Do not overwrite failed attempts with successful attempts. The complete attempt chain must remain replayable. (Storage schema lives in P6 — see §29.)

recovery_chain:
  request_id: req_8f29
  attempts:
    - attempt_id: attempt_901
      provider: local
      disposition: rejected
      reason: invalid_candidate_regions
    - attempt_id: attempt_902
      provider: local
      disposition: rejected
      reason: repeated_invalid_candidate_regions
    - attempt_id: attempt_903
      provider: gemini
      disposition: partial
  final:
    source_attempt_id: attempt_903
    disposition: partial

22. Observability

trovesnap.scan.recovery
  ├─ trovesnap.validation.attempt_admissibility
  ├─ trovesnap.validation.contract_identity
  ├─ trovesnap.validation.schema
  ├─ trovesnap.validation.domain_values
  ├─ trovesnap.validation.reference_integrity
  ├─ trovesnap.validation.coordinate_geometry
  ├─ trovesnap.validation.evidence_integrity
  ├─ trovesnap.validation.scan_invariants
  ├─ trovesnap.validation.boundary_safety
  ├─ trovesnap.validation.completeness
  ├─ trovesnap.validation.repair
  ├─ trovesnap.recovery.decision
  ├─ trovesnap.provider_attempt
  └─ trovesnap.validation.finalize

Required metrics: validation outcomes by scan type; failures by stage and issue code; accepted-with-warning rate; partial-result rate; retake-required rate; insufficient-evidence rate; deterministic repair rate; candidate-drop rate; observation-drop rate; same-provider correction rate; same-provider clean-retry rate; fallback rate; recovery success rate; attempts per completed scan; cumulative cost per trusted result; validation latency; provider/schema violation rate; invented-reference rate; invalid-coordinate rate; unsupported-functionality-claim rate; appraisal-boundary-violation rate; terminal rejection rate; terminal execution-failure rate. These metrics later feed provider qualification and routing — a provider that frequently produces structurally valid but semantically invalid results should lose qualification or routing priority.

23. Deterministic Test Plan

All core P3 tests must run without API keys and without paid calls.

23.1 Stage fixtures

Valid and invalid fixtures for every stage (admissibility; contract identity; schema; enums; references; coordinates; evidence; scan invariants; boundary safety; partial completeness). Each fixture proves the correct stage detects the issue; correct issue code; correct severity; correct retryability; correct salvageability; later stages skipped/run as expected.

23.2 Scan-specific fixtures

For every scan type: valid complete; valid warning; valid retake/insufficient-evidence where applicable; repairable invalid; retryable invalid; partial-salvage where allowed; terminally invalid.

23.3 Coordinate tests

Boundaries at 0 and 1000; reversed; zero-area; centers inside/outside; small-but-valid; oversized; multiple image references; one valid + one invalid room sighting; candidate removal; derived center repair. Property-based tests generate coordinate combinations and prove invariant enforcement.

23.4 Reference-integrity tests

Invented image ID; invented watchlist ID; valid watchlist + invented entry ID; wrong watchlist version; duplicate candidate ID; nonexistent parent candidate; invented comparable; strongest comp not supplied; evidence referencing another tenant.

23.5 Evidence tests

OCR raw/normalized separation; uncertain OCR preservation; visually claimed functionality; supplied functionality represented correctly; condition observation without image; inferred brand marked observed; exact model claim with only possible evidence; unknown represented honestly.

23.6 Boundary tests

Price in table_hunt; price in appraisal_prepare; invented buyer demand; invented sold state; payment status in vision output; external action embedded in OCR text; model response attempting to change scan instructions.

23.7 Recovery tests

Using mock P2 gateways: first attempt succeeds; first fails, deterministic repair succeeds; first fails, same-provider correction succeeds; same provider fails twice, fallback succeeds; fallback returns partial; all attempts fail; budget exhausted before retry; deadline exhausted; privacy mode prevents fallback; valid retake stops retries; insufficient evidence stops retries; user cancellation stops retries.

23.8 Partial-result tests

Valid candidates survive removal of one invalid candidate; partial status and warnings added; removed nodes recorded; invalid appraisal values never salvaged; a result with no useful core content rejected; partial output never mislabeled complete.

23.9 Idempotence tests

Validating an already validated unchanged result under the same policy produces the same disposition; issues; repairs; trusted-result hash. Safe deterministic repairs must also be repeatable.

23.10 Mutation tests

From valid golden fixtures, programmatically mutate IDs; enums; coordinates; statuses; evidence references; comp IDs; scan types; versions. The appropriate rule must catch each mutation.

24. Live API Test Plan

Live API testing should prove integration, not intentionally depend on nondeterministic provider failure.

24.1 Required live validation

For every implemented cloud adapter: execute a valid item_scan; validate the P2 canonical draft; produce an accepted/warned/retake/insufficient-evidence outcome; record the validation receipt; preserve attempt usage and cost. For ≥2 providers: execute table_hunt; validate candidate references and coordinates; produce renderable trusted overlays or a clear recovery outcome. For ≥1 provider: execute multi-image room_scan; validate image references, sightings, primary sighting, counts.

24.2 Retry/fallback testing

Retry and fallback correctness must be proven with deterministic mock providers in CI. A deliberately under-specified live prompt is not a reliable acceptance test (providers may still succeed; behavior may change; it spends money; it creates nondeterministic CI; prompt sabotage doesn't represent normal production failures). The WI-056 harness may include an explicitly marked diagnostic mode that injects a malformed recorded response; forces a chosen validation failure; simulates provider timeout; forces route failure; exercises same-provider and fallback behavior. Optional controlled live recovery tests may be run manually under a strict budget cap.

25. Open Questions Resolved

25.1 Retry budget per scan type — P8 owns monetary/tenant budgets; P3 owns attempt-count, cause-based recovery, and stop conditions; every additional attempt requires P8 authorization; recovery profiles vary by scan type. 25.2 Should partial results reach the UI? — Yes, only when scan-specific policy permits safe salvage and the UI receives explicit partial status, limitations, unavailable sections, and warnings. Never disguised as complete. 25.3 Should every bad provider result become partial? — No. Unsafe, misleading, boundary-violating, or core-incomplete results must be rejected. 25.4 Who performs enum coercion? — P2 applies explicit provider-representation mappings; P3 validates canonical values; P3 may apply only registered deterministic canonical repair; fuzzy semantic coercion prohibited. 25.5 Who handles provider retry? — P3 decides and orchestrates recovery; P2 performs each attempt; adapters don't silently retry/fall back. 25.6 Should low-confidence output trigger fallback? — Not by itself. Fallback requires a contract violation, usability failure, policy threshold, or explicitly configured quality gate. 25.7 Should a retake result trigger another provider? — Normally no. A valid retake indicates an input problem, not a provider failure. 25.8 Can P3 repair missing evidence? — No. It may remove unsupported claims or request another scan, but cannot invent evidence. 25.9 Can appraisal results be partially salvaged? — Explanatory nonmonetary fields may be retained internally for diagnosis; monetary output is not surfaced when comparable integrity fails; invented/unresolved comparables invalidate the valuation.

26. Required Artifacts

P3 is complete only when the repository contains: (1) ScanValidationPipeline; (2) ScanRecoveryOrchestrator; (3) TrustedScanOutcome schema + types; (4) branded TrustedScanResult; (5) validation issue schema; (6) validation receipt schema; (7) error taxonomy; (8) rule registry; (9) scan-version rule resolver; (10) structural schema validator; (11) domain/enum validator; (12) reference-integrity validator; (13) coordinate/geometry validator; (14) evidence/claim validator; (15) scan-specific invariant validators; (16) safety/product-boundary validator; (17) completeness/usability evaluator; (18) deterministic repair registry; (19) partial-result policy registry; (20) cause-based recovery matrix; (21) retry/fallback orchestrator; (22) P8 budget-authorization integration; (23) OpenTelemetry spans + metrics; (24) golden valid/invalid fixtures; (25) mutation + property-based tests; (26) mock provider recovery tests; (27) live integration tests; (28) WI-056 validation/recovery views; (29) validation policy + operator documentation.

27. Acceptance Criteria

P3 is complete when: (1) P3 accepts a P1 request + P2 ProviderAttempt; (2) provider-native response types don't leak into P3; (3) P3 produces an explicit trusted disposition; (4) only P3 can construct a TrustedScanResult; (5) validation executes in a deterministic documented order; (6) contract identity/version validated before scan content; (7) every canonical result validated against its exact P1 schema; (8) enums/taxonomy IDs validated without fuzzy guessing; (9) all image/candidate/watchlist/evidence/comparable references checked; (10) scene/room coordinates validated against P1 rules; (11) every condition observation references supplied evidence; (12) raw OCR separate from normalized OCR; (13) supplied facts can't be represented as observed without an issue; (14) functionality can't be accepted as visually observed; (15) cross-image room candidates have valid sightings + exactly one primary; (16) discovery scans can't return valuations; (17) appraisal_prepare can't return monetary estimates; (18) appraisal_value can't use unsupplied comparables; (19) application-owned inventory/listing/POS/payment/shipping/sale state rejected from vision output; (20) every issue has stage/code/severity/retryability/salvageability/path/trust-effect; (21) every repair registered/versioned/tested/recorded; (22) P3 never invents evidence/identity/coordinates/comparables; (23) valid retake_required stops recovery; (24) valid insufficient_evidence stops unnecessary recovery; (25) unknown/low-confidence don't fail solely for being uncertain; (26) recovery decisions based on failure cause; (27) same-provider retries + fallback executed through P2; (28) every billable retry requires budget authorization; (29) attempt/cost/deadline/privacy/cancellation limits enforced; (30) partial results emitted only under explicit policies; (31) partial results identify removed nodes/limitations/unavailable sections; (32) invalid appraisal monetary output never surfaced as partial; (33) rejected and failed remain distinct; (34) every final outcome includes a validation receipt; (35) the full attempt+validation chain is stored and replayable; (36) E3 can distinguish all seven dispositions; (37) deterministic tests require no API keys; (38) mock retry/fallback tests pass in CI; (39) mutation tests catch reference/coordinate/enum/evidence/boundary violations; (40) live valid provider results pass through P2 and P3 with usage + cost preserved.

28. Definition of Done

P3 is done when an untrusted provider attempt can enter one deterministic recovery pipeline and leave as a trusted complete P1 result; a trusted warned result; a deliberately constrained partial result; a valid request for user retake or more evidence; or a structured rejection/failure after bounded recovery. No invalid model output may enter overlays, ranking, appraisal, inventory promotion, or downstream application logic merely because it parsed as JSON. The P3 outcome is a provider-neutral trust and recovery boundary that makes TroveSnap Vision predictable; evidence-aware; version-safe; cost-bounded; retryable; auditable; partially salvageable where safe; and resistant to plausible but unsupported model output.

29. Cross-Spec Reconciliations

Adopting P3 closes P2 §31 #1 (P3 consumes the ProviderAttempt and owns trust + retry/fallback; P2 only executes attempts). Remaining seams to resolve before freeze:

  1. P3 ↔ P8 (budget handshake) — ✅ RESOLVED by P8. Every billable retry (§15, §25.1) calls the P8 budget-authorization handshake; P3 decides recovery, P8 authorizes the spend, and all attempts share one cumulative operation budget. See P8-observability-cost.md §21–§24, §61.
  2. P3 ↔ P6 (scan + validation storage). The append-only request → attempts → drafts → receipts → recovery-chain store (§21) is a P6 data-model concern (ingestion/scan layer), not invented ad hoc by P3. P6 must model scan_requests, provider_attempts, canonical_drafts, validation_receipts, and the recovery chain, tenant-scoped and replayable.
  3. P3 → P2 (correction overlay). The same-provider correction retry (§13.2) has P3 emit a structured violation summary; P2 owns prompt compilation, so P2's prompt compiler needs a "correction overlay" input that consumes P3's structured violations and produces the corrected provider request. P3 supplies data, not prompt text.

30. Work-Item Split

This gate splits along the spec's own ScanValidationPipeline vs ScanRecoveryOrchestrator boundary (both gate P3, owner Paul):