← back to Docs source: system/specs/GATES.md

GATES — The Gate Library Spec

Status: DRAFT v2.0 — command-center review pending What this is: the contract for gatelib.py — one shared, stdlib-only Python library every gate imports — and for the thin per-gate scripts that call it. The v1 audit found thirteen scripts that were ~85% the same program with the grouping key swapped, counting calendar dates instead of evidence, re-firing passed proposals for up to 30 days, and carrying an injection-grade auto-pass bypass. The library absorbs the clone code and fixes the mechanics once, everywhere. Gates stay dumb; a clever judge can be argued into a sophisticated mistake; a counter cannot.


1. Shape and ground rules

gates/
  gatelib.py              # the library — single file, stdlib only, no third-party deps
  <gate-name>-gate.py     # thin per-gate scripts (~40–60 lines)
config/
  gate-ramp.yaml          # ramp config (machine-readable; no prose modes)
data/gates/
  <gate>-state.json       # unified naming — always "<gate>-state.json"
  cooldowns.json          # shared cooldown ledger
  consumed.json           # consumed direction keys (per gate)
  ramp-events.jsonl       # graduation/demotion audit trail (append-only)
  qualified/<gate>.jsonl  # qualified output — APPENDED, never overwritten
  console/<gate>.json     # force-pass / hold / demote — OUTSIDE the worker-writable tree

Ground rules the library enforces uniformly:

2. load_ramp_config(gate_name) -> GateConfig

Reads config/gate-ramp.yaml, resolves the gate's active phase from gate state (the config file is doctrine; state is state), expands like: inheritance, applies defaults:, and falls back to built-in steady-state values if the file is absent — a missing config degrades, never crashes.

GateConfig fields (all consumed somewhere in §3; per-gate extras under params):

CFG.threshold             # int — corroboration threshold (tier-derived, CONSEQUENCE.md §2)
CFG.lookback_runs         # int — window in distinct qualifying runs (Decision 3)
CFG.day_ceiling           # int — staleness ceiling in days; the ONLY day-denominated field
CFG.consecutive_runs      # int — 0 = not required
CFG.cooldown_epochs       # int — K, default 2 (Decision 6)
CFG.min_distinct_runs     # int — corroboration floor
CFG.min_distinct_provenance  # int — corroboration floor (Decision 13)
CFG.reviewer_required     # bool
CFG.phase                 # str — active ladder phase, resolved from state
CFG.ladder                # list — phases with graduate_when predicates
CFG.stakes                # [bool×5] — the five answers, tier derives from yes-count
CFG.key_fields            # list — the direction-key or-chain
CFG.keywords              # tuple — the dumb parser's startswith filter
CFG.params                # dict — deterministic-filter extras (budgets, caps, DA bounds)

The schema validator rejects any time-elapsed graduation key (days_elapsed and kin) — graduation is evidence-based, never calendar-based (Decision 7).

3. Function inventory (the contract)

count_corroboration(group, cfg) -> CorroborationCount

The counting rule, in one place, correct:

pairs = {(obs.run_id, fp) for obs in group for fp in obs.provenance_fingerprints}
count = len(pairs)
passes = (count >= cfg.threshold
          and len({p[0] for p in pairs}) >= cfg.min_distinct_runs
          and len({p[1] for p in pairs}) >= cfg.min_distinct_provenance)

check_cooldown(gate, direction_key, epoch, cfg) -> CooldownStatus

Damping decoupled from thresholds — what makes threshold-1 building safe:

mark_consumed(gate, direction_key, epoch) / is_consumed(...)

Fixes the v1 standing re-execution bug (audit R2): a direction that executes is marked consumed in data/gates/consumed.json ({"<gate>": {"<key>": {"consumed_epoch": 41}}}) and cannot re-qualify from pre-consumption evidence. Re-qualification requires fresh observations with epoch > consumed_epoch. Consumption is written by the executor alongside record_execution; the gate checks it before counting. Consumed keys report as CONSUMED in output, distinct from blocked and suppressed.

evaluate_graduation(gate, state, cfg) -> Optional[PhaseChange]

Runs at the end of every gate cycle:

write_qualified(gate, result)

apply_console(gate, result) -> result

The human/arbiter console — the mechanism v1 described and never built:

Exit-code and state-file conventions (unified)

CodeMeaning
0PASSED (≥1 qualified) or clean no-candidates run
3BLOCKED — no consensus (feeds deadlock clock)
4BLOCKED — deterministic filter (budget/cap/DA); never feeds deadlock clock
5Deadlock escalation emitted (arbiter-request artifact written, not just printed)
2Gate error (bad config, lock timeout, parse failure of gate's own state)

State files are always data/gates/<gate>-state.json (the v1 strategy-consensus-state.json divergence is retired). Standard state keys: {phase, cycle, last_run_id, pass_streak, runs_seen, deadlock_start_run, warnings}.

4. The per-gate script (thin wrapper shape)

#!/usr/bin/env python3
"""voice-pattern-gate.py — thin wrapper over gatelib."""
import gatelib
GATE = "voice-pattern"

def main():
    cfg, state = gatelib.load_ramp_config(GATE), gatelib.load_state(GATE)
    obs = gatelib.scan_recommendations(GATE, cfg)          # parses run_id, epoch, provenance
    obs = gatelib.window(obs, cfg.lookback_runs, cfg.day_ceiling)
    result = gatelib.evaluate(GATE, obs, cfg, state)       # group, dedupe, count_corroboration
    result = gatelib.apply_cooldowns(GATE, result, cfg)
    result = gatelib.apply_console(GATE, result)
    gatelib.write_qualified(GATE, result)
    gatelib.evaluate_graduation(GATE, state, cfg)
    gatelib.save_state(GATE, state)
    gatelib.gate_exit(result)

if __name__ == "__main__":
    main()

Gates with extra semantics (worker-create budget, rich-media caps, link-quality DA tiers, rich-media-budget) insert their deterministic filter between evaluate and apply_console — logic preserved, relocated, its blocks exiting 4. Deterministic filters import only infrastructure (config, state, qualified-writer, console, exits) and keep their own decision logic: they are not consensus and stay plain scripts (CONSEQUENCE.md §4).

5. DELETED: the AUTO_PASS_SOURCES bypass

v1's playbook gate auto-passed any proposal whose YAML contained source: human or source: admin — a field inside attacker-influenceable worker output granting instant bypass; one successful injection defeats the gate entirely (audit R4). This mechanism is deleted, not migrated:

  1. **Provenance must be attested by the ingesting sensor, never declared by the proposer.** A proposal's provenance[] entries are valid only when their attested_by sensor has a matching ingest record; self-declared provenance is stripped and the proposal is judged on what remains (usually: killed as provenance-less).
  2. Authority-passes are console force-pass entries (§3) — files outside the worker-writable tree. ML saying "ship it" becomes a console entry plus a ledgered grant, not a magic string a worker can emit.
  3. No gate may implement any proposal-content-triggered bypass. The library exposes no hook for one; a gate script found reading proposal fields for pass/skip decisions outside key_fields/keywords is a self-knowledge finding.

6. The values-gate template (carried by every worker)

Deliberately the dumbest gate in the system — a checklist, not a judge:

Provenance: function inventory generalizes the gate-mechanic audit §3 proposal; counting/cooldown/graduation semantics from flaws-doc Decisions 2, 3, 4, 6, 7, 13; deletions R2/R3/R4/R9 per audit; values-gate mandate from VALUES.md header and SYSTEM.md §2.