{
 "product": "Fail-closed doctrine (Top 25 #17)",
 "date": "2026-11-03",
 "method": "Two blind sets, each written before any change. Set 2 was run once for the reported figure. Set 1 was first run on 2026-11-02; that run found a workspace with ONE decision scored LOW_RISK 862 by ACTUARY, fixed and deployed under #22 (thin-evidence cap, 799 at most). The set 1 figures below are the 2026-11-03 re-run, so they are not blind for that one route. Set 1 sends 5 malformed bodies to every public POST route of the fabric API and the KEYSTONE family (no key, then a test-account key). Set 2 feeds derive_outcome, the pure function that turns stage results into the live verdict, 18 stage lists it was never written for",
 "set1": {
  "routes": 232,
  "probed": 144,
  "skipped_state_changing": 88,
  "requests": 1440,
  "breaks_no_key": 0,
  "breaks_with_key_flagged": 5,
  "breaks_with_key_real": 0,
  "unhandled_errors": 0,
  "note": "The 5 flagged with a key were all /actuary-keystone/score-workspace, whose evidence_provenance field says verified:true about where its data came from; the score itself was MODERATE_RISK 799 under the thin-evidence cap. Not a permission. Harness flaw found: /fabric/sso/scim-token was not on the skip list and rotated the test account's SCIM token; now skipped",
  "finding": "/fabric/try accepted a JSON body that is not an object ([] or \"garbage\") and ran the safe-read scenario; it returned REQUIRE_APPROVAL, not an allow. Now HTTP 400",
  "first_run_finding": "2026-11-02: /actuary-keystone/score-workspace gave LOW_RISK 862 to a workspace with one decision; now capped at 799 (never LOW_RISK) below 10 distinct decisions or 2 days"
 },
 "set2_held_before": "5/18",
 "set2_held_after": "18/18",
 "rows": [
  {
   "name": "control: all allow",
   "want": "ALLOWED",
   "got": "ALLOWED",
   "pass": true,
   "after": "ALLOWED"
  },
  {
   "name": "control: enforcing deny",
   "want": "BLOCKED",
   "got": "BLOCKED",
   "pass": true,
   "after": "BLOCKED"
  },
  {
   "name": "control: enforcing unavailable",
   "want": "BLOCKED",
   "got": "BLOCKED",
   "pass": true,
   "after": "BLOCKED"
  },
  {
   "name": "no stages at all",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage says DENY (upper case)",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage says 'block'",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage says 'error'",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage says 'unknown'",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage verdict is None",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage verdict is missing",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage verdict is ''",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage verdict ' deny ' with spaces",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage verdict is True (boolean)",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing stage 'Require_Approval' mixed case",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "REQUIRE_APPROVAL"
  },
  {
   "name": "enforcing stage 'timeout'",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  },
  {
   "name": "enforcing flag as string 'true' + deny",
   "want": "BLOCKED",
   "got": "BLOCKED",
   "pass": true,
   "after": "BLOCKED"
  },
  {
   "name": "stage entry is not a dict",
   "want": "not ALLOWED",
   "got": "EXCEPTION AttributeError",
   "pass": true,
   "after": "BLOCKED"
  },
  {
   "name": "stage ran=False with verdict allow on an enforcing stage",
   "want": "not ALLOWED",
   "got": "ALLOWED",
   "pass": false,
   "after": "BLOCKED"
  }
 ],
 "live_vocabulary": [
  "allow",
  "deny",
  "unavailable",
  "skipped",
  "not_configured",
  "require_approval",
  "deferred"
 ],
 "reachability": "Latent, not exploited: every live stage today emits only the 7 words above, so no live decision was affected. A future stage, a typo, or a stage passing through an engine's own word (one helper returns verdict 'error') would have been read as permission",
 "fixes": [
  "derive_outcome reads a closed vocabulary: verdicts are lower-cased and trimmed, and any other word, None, a missing verdict, a boolean, or 'allow' on a stage that reports ran=false is read as 'unavailable' (an enforcing unavailable stage blocks)",
  "A stage entry that is not an object is an enforcing unavailable stage (it used to raise AttributeError)",
  "An empty stage list with enforcement on is BLOCKED (it used to be ALLOWED)",
  "The reported verdict is kept as verdict_as_reported on the copy, so the evidence shows what the stage actually said",
  "/fabric/try returns 400 for a JSON body that is not an object",
  "Blind set 1 now skips /fabric/sso/scim-token"
 ],
 "tests": "tests/test_fail_closed_vocabulary.py 18 new tests; 86 stage tests and 352 gateway fabric/proof/simulator tests pass unchanged",
 "limits": [
  "The fix is in the gateway process: live only after the next gateway rolling deploy",
  "Set 1 does not probe 88 state-changing routes (kill switches, revokes, writes) on purpose; those are covered by their own product tests, not by this sweep",
  "Shadow mode is unchanged by design: an unrecognised verdict in shadow mode reports ALLOWED_DEGRADED, and enforcing denials report ALLOWED_WITH_DENIALS",
  "Our own runs; no independent test"
 ],
 "pending_deploy": "Needs the owner's gateway rolling deploy (scripts/ops/rolling_deploy.py); until then the live gateway still runs the open vocabulary"
}