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:
- stdlib-only, deliberately dumb parsing. The line-oriented proposal parser stays (no YAML lib) — deterministic gates are injection-immune precisely because they don't read prose. The per-gate keyword tuple becomes config (
keywords:). SITE_ROOTenv anchors all paths, as in v1.- Atomic state: every JSON write is write-temp-then-rename with a per-gate lockfile
data/gates/<gate>.lock(fixes audit R11). - Malformed proposals are killed, never bucketed. A proposal missing its
direction_keyfields,run_id, orprovenanceis excluded from all counting, ledgered asverdict/killwithreason: malformed:<missing-field>, and surfaced as a parse warning. The v1"unknown"grouping bucket — which let any two malformed proposals form phantom consensus, and any single one at threshold 1 — is deleted (fixes audit R3). - Every gate run mirrors its judgments to the ledger:
candidate-registeredon first sight,verdictper decision (LEDGER.md §3.1–3.2).
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)
- DISTINCT run_id × DISTINCT provenance fingerprint. Never file dates. N proposals from one run are one run; N proposals citing one source are one source (closes the poisoned-corroboration hole at the deterministic layer, where injection can't reach).
- Days appear only as a staleness ceiling: observations older than
cfg.day_ceiling(UTC, date-inclusive) are discarded before counting. No other day arithmetic exists in the library. - Epoch dedup precedes counting: at most one observation per
(direction_key, provenance_fingerprint, epoch)triple (Decision 4). A missingepochfield degrades that observation to one-per-UTC-day. - Consecutive means consecutive qualifying runs — no intervening qualifying run lacking the proposal — never calendar-day adjacency (fixes audit R6).
check_cooldown(gate, direction_key, epoch, cfg) -> CooldownStatus
Damping decoupled from thresholds — what makes threshold-1 building safe:
- A key that executed cannot re-fire, and its reversal key (
REV:<key>, resolved via the optionalreversals:map in ramp config) cannot fire, forcfg.cooldown_epochsepochs. - Ledger:
data/gates/cooldowns.json, entries{"<gate>": {"<key>": {"executed_epoch": 41, "cooldown_until_epoch": 43}}}. record_execution(gate, key, epoch, cfg)is exported for executors — the cooldown starts on completed execution, not on gate pass.- Suppression is not blockage: a cooldown-suppressed key returns
SUPPRESSED_BY_COOLDOWN, which does not feed the deadlock clock (fixes audit R8 — a working gate must not escalate to the arbiter because its own damping held).
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:
- Evaluates the active phase's
graduate_when:predicates — machine-checkable, over gate state (pass_streak,clean_merges,reviewed_clean) and declared metric files (data/metrics/*.json) — declared, not discovered. - Permitted predicate vocabulary: counts, streaks, rates, population-stability tests (
new_key_rate_below: {pct: 5, consecutive_cycles: 3}), named events (event: persona-lock-in), readiness flags (baselines_exist). Time-elapsed predicates are rejected at config load. - On criteria met: bump
state["phase"], append todata/gates/ramp-events.jsonl, emit a ledgeralarmatinfo. Demotion on regression uses the same path in reverse; both are auditable ramp events.
write_qualified(gate, result)
- Append, not overwrite: one JSON line per decision to
data/gates/qualified/<gate>.jsonl(fixes the v1 clobber race, audit R9 — a fast cadence can no longer overwrite a pass with a block before the executor reads it). A conveniencelatestpointer file may be regenerated per run; the JSONL is the record. - Atomic (temp + rename), lockfile-guarded.
- Every line carries: `{ts, run_id, epoch, phase, direction_key, status, reason, corroboration: {pairs, distinct_runs, distinct_provenance}, forced: bool}`.
statusdistinguishes, explicitly and machine-readably:PASSED(met the standard) ·BLOCKED(failed it, or stopped by filter/values/console-hold — blocker named inreason) ·SUPPRESSED_BY_COOLDOWN·CONSUMED. OnlyBLOCKED (no consensus)feeds the deadlock clock.
apply_console(gate, result) -> result
The human/arbiter console — the mechanism v1 described and never built:
- Reads
data/gates/console/<gate>.json:{"force_pass": ["<key>"], "hold": true, "phase_override": 0, "demote": true}. - The console path is writable only by the command center and ML —
data/gates/console/sits outside the worker-writable tree (workers/), enforced by filesystem permissions and verified by the self-knowledge sweep. Authority flows from where the file lives and who can write there, never from what a proposal says. - Force-passed keys enter qualified with
forced: trueand full ledger mirroring; single-shot entries are consumed (rewritten) after use.holdblocks everything;demote/phase_overrideadjust the ramp phase. Always available, never necessary.
Exit-code and state-file conventions (unified)
| Code | Meaning |
|---|---|
| 0 | PASSED (≥1 qualified) or clean no-candidates run |
| 3 | BLOCKED — no consensus (feeds deadlock clock) |
| 4 | BLOCKED — deterministic filter (budget/cap/DA); never feeds deadlock clock |
| 5 | Deadlock escalation emitted (arbiter-request artifact written, not just printed) |
| 2 | Gate 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:
- **Provenance must be attested by the ingesting sensor, never declared by the proposer.** A proposal's
provenance[]entries are valid only when theirattested_bysensor has a matching ingest record; self-declared provenance is stripped and the proposal is judged on what remains (usually: killed as provenance-less). - 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. - 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/keywordsis 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:
- Input: the worker's outbound action (proposal, draft, publish request, message).
- Check: line-by-line against the enumerated VALUES.md floor clause checks (VF1 proof-grade-illegality patterns, VF2/VF3 scam-shaped presentation or transaction patterns, operator-integrity: valence-preservation and perimeter). Each check is a string/structure match or a declared-metric comparison — no model judgment in the operating layer. **Quality/thin-content checks are NOT in this gate** — they live in the pipeline's acceptance standards and the gatekeeper's stage stamps (mission machinery,
seo/PIPELINE.md§3). The floor gate is few, firm, and light: worst-outcome prevention, tuned to rarely fire (ML, F1/F7). - On hit: block + log. The action stops; a
verdict/blockwithreason: values:<clause>is ledgered; tier-5 controls apply (CONSEQUENCE.md §2). - Unappealable in the operating layer. No worker, orienter, or executor can override a floor block. The paths forward are the escalation ladder (genuine ambiguity — noting VF1's posture: absent proof of illegality, ambiguity resolves toward action; only scam-shaped ambiguity stops) or amendment of VALUES.md by its owner. The console cannot force-pass a floor block. False-positive blocks are checklist defects — fix the checklist via gated update; never clear the block in-flight.
- The template ships in the worker anatomy; a worker without a wired values gate fails commissioning (CHANNELS.md §5).
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.