Orchestrata Ultimate › Gates › prolog-gate
Gate: prolog-gate
At a glance
| Purpose | Prove the Key Rules (no_relative_import, banned_import, bare_os_environ, env_mutation, infra_key_with_default, bare_toolchain) over a fact base extracted from the AST snapshot, and refuse to call a stale fact base green. |
| Where it runs | The scripts/gates.py battery (registered as prolog-gate), after the __ast__ refresh pre-step. |
| Trigger | A battery run whose fingerprint changed, or whose last cached verdict was not ok. |
| Exact command | python scripts/gate.py run (--snapshot, default plans/ast-ultimate.json; --repo-root, --workdir, --max-listed) |
| Blocks | Exit 1 on a violation; exit 2 INVALID when the fact base is stale, no rules loaded, or the snapshot is missing; 0 pass. |
| Ruling / ADR | ADR-0031 — every gate decision carries its proof tree; ADR-0032 — the gate adopts the admission-control contract; ADR-0025 — no dynamic predicate construction, no cut; ADR-0089 — bare toolchain names fail the gate. Rules cite RUL-003 and RUL-002 in gate_rules.pl. |
| Last recorded verdict | ok: false in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local; the cause is not recorded there). wt-MAP-PRECOMMIT has no .cache/gates.json. |
What it refuses
A Key-Rule violation in the tree — the rule set that used to live in the legacy
standards checker (relative imports, bare os.environ["X"] reads, banned imports)
plus infra_key_with_default — evaluated as Prolog rules over a fact base extracted
from the AST snapshot. It also refuses its own false greens: a stale fact base or
a run with no rules loaded exits 2 INVALID, never 0.
gate_rules.pl on wt-MAP-PRECOMMIT (read 2026-10-23) has six violation/4
rules, two more than an earlier version of this page listed:
| Rule | Refuses | Landed |
|---|---|---|
no_relative_import |
a relative import | with the gate |
banned_import |
an import of a library in .claude/banned-libraries.txt |
with the gate |
bare_os_environ |
an os.environ["X"] read (RUL-003) |
with the gate |
env_mutation |
an os.environ["X"] = v write (RUL-002) |
592a73da7, 2026-08-08: "Split env reads from env writes: the gate was advising a silent data loss" |
infra_key_with_default |
a required infra endpoint read with a literal fallback (RUL-003) | with the gate |
bare_toolchain |
a bare toolchain name at a spawn site (ADR-0089) | 34b9dc2b1, 2026-10-11 |
Every rule body carries \+ waived(File, Line): a reasoned # gate-ok: <reason>
covering the line suppresses the finding, and a bare # gate-ok: emits no waiver
fact (code_facts.pl).
Why it exists
From scripts/gate.py's docstring:
"
runevaluates the gate and exits 0 pass / 1 fail / 2 invalid — three outcomes, because 'the rules did not evaluate the code in front of them' is not a pass.explainandwhy-notanswer, for one location, why a rule DID or DID NOT fire.diffruns this layer and the legacy checker it replaces over one corpus and accounts for every disagreement — because a replacement that quietly finds LESS than what it replaces is a regression wearing a cleanup's clothes.
why-notis the question that matters most and the one no checker in this repo could previously answer. The tree-sitter relative-import checker was dead for its entire life, twice over, and nothing noticed — because nothing could ask it why it never fired."
The registry comment in scripts/gates.py adds the succession note: "It REPLACES the
standards entry that used to sit here — same three rules plus
infra_key_with_default, over an inferred fact base rather than a per-file walk,
with three outcomes instead of two."
Design authority: plans/prolog-gates-implementation-plan.md; contract:
plans/contracts/prolog-gates.contract.json.
How it runs
-
Command:
python scripts/gate.py runCompanion subcommands:python scripts/gate.py facts --stats # extract + report the fact base python scripts/gate.py explain no_relative_import src/pkg/a.py 7 python scripts/gate.py why-not bare_os_environ src/pkg/a.py 12 python scripts/gate.py diff # vs the legacy checker -
All
gate.pysubcommands and their exits (gate.py --helpand thecmd_*handlers, read through the AST toolkit on 2026-10-23;EXIT_OK = 0,EXIT_ERROR = 2):Subcommand What it does Exit factsExtract the fact base from the AST snapshot (REQ-GATE-001); --out,--stats.A report, not a verdict (the gates.pyregistry comment).runEvaluate the rules (the battery's prolog-gate).0 pass, 1 fail, 2 INVALID or snapshot missing (REQ-GATE-003). uncoveredList policy requirements with no enabled mechanism (REQ-GATE-011, RUL-014 as an inference). Always 0 on a successful query; the battery's uncovered-policies.diffCompare this layer with the legacy checkers it replaced. Exit 2 on every call today: LEGACY_RULE_MAPis{}because "every row of the replace-then-delete ledger is deleted as of P6", and the command refuses a vacuous comparison.explain RULE FILE LINEPrint the proof tree for one violation. 0 with a proof; 2 when the rule does not hold there ("Ask why-not instead") or the snapshot is missing. why-not RULE FILE LINEPrint the failing subgoal for a location that does not violate. 0 with failures listed; 2 when no clause even unifies (wrong rule id, or the rule holds — "try explain"). review-stagedThe commit-time review of staged files through commit_review.pl(--repo-root,--python,--timeout).0 pass, 1 fail, 2 when gitcannot read the index. WithPRECOMMIT_WALLS_FILEset it also writes per-file reviewer walls (ADR-0101 rule 2). -
When it fires: the
scripts/gates.pybattery (registered asprolog-gate).review-stagedruns from the git pre-commit hook instead; see review-staged. -
Inputs: fingerprint roots
("src", "tests", "scripts", ".claude/banned-libraries.txt", "pyproject.toml"). The registry comment explains why the set is wider than the oldstandards': "the rules and the fact ontology are .pl files under src; the ban registry is .claude/banned-libraries.txt; the ruff select/ignore lists that decide tool_rule_status come from pyproject.toml. Fingerprinting onlysrcwould let a package be un-banned, or a ruff rule turned off, with the gate cached as 'unchanged since last PASS' — the false-green this cache has already produced once for validate-ci." -
Caching: the verdict is cached in
.cache/gates.jsonagainst_fingerprint(roots): a sha256 over the relative path and bytes of every file under the directory roots whose suffix is.py,.toml,.pl,.json,.mdor.sh(_interesting()inscripts/gates.py), plus the two file roots. There is no per-gate suffix mask: this page previously quoted one, andgit log --all -S GATE_MASKS -- scripts/gates.pyfinds no commit that ever contained aGATE_MASKStable (checked 2026-10-23). The fact extractor "never opens a .py file to learn what a module defines"; structural facts come solely fromplans/ast-ultimate.json(refreshed first by__ast__), plus the five.plrulebase files (code_facts/freshness/gate_rules/gate_severity/explain) undersrc/orchestrata_ultimate/prolog/.
Typical failures and how to fix them
- Exit codes: 0 pass / 1 fail / 2 INVALID — the three-valued verdict is the point: stale facts or zero loaded rules are INVALID, not green.
- What a failure looks like (real recorded battery output,
.cache/gates.json, 2026-08-29 — an INVALID caused by a stale snapshot):23.5s | ...t describe the working tree: 2 file(s) across 2 finding(s). Re-run: uv run ast-sync (then scripts/build_runtime_callgraph.py reaugment) stale (2): scripts/gates.py (content_changed) src/orchestrata_ultimate/tools/__init__.py (not_in_snapshot) fact base: 413,299 facts across 23 predicates - Un-blocking: for INVALID, refresh the snapshot (
uv run ast-syncthenreaugment— or just re-runscripts/gates.py, whose__ast__pre-step does it); for a real violation, fix the code, then useexplain/why-notto verify the rule now behaves as expected. bare_toolchainon a spawn site. Name the interpreter throughsys.executableor the declared toolchain configuration (env/toolchain.envfor CC/CXX, per the rule's comment), not a literalpython/gcc. A program name used as test data rather than spawned takes a reasoned# gate-ok:line.env_mutationin a test. Pass the value through an explicitenv=seam instead of assigning toos.environ(the rule's own message).gate.py diffexits 2. Expected today: there is no legacy checker left to compare against. It is not a gate failure.- Last recorded state: the primary checkout's cache records
ok: false(2026-09-22); whether that was a FAIL or an INVALID is not stored there.
Wiring
- Invokers:
scripts/gates.py(battery); the engine lives insrc/orchestrata_ultimate/gates/(GateEngine,FactExtractor). - Sibling on the same engine: uncovered-policies queries the identical fact base.
- Tests:
tests/scripts/test_gate_cli.pyplus the engine suite undertests/gates/—test_fact_extractor.py(P1),test_freshness_inference.py(P2),test_gate_rules.py(P3, "with a negative corpus at least as large as the positive"),test_gate_explain.py(P4),test_differential_vs_legacy.py(P5),test_differential_coverage.py.
Related
- Gates master index
- Development workflow
- review-staged — the commit-time subcommand on the same CLI
- ADR-0031, ADR-0032, ADR-0025, ADR-0089
- RUL-003 — Env config from the active env profile
- RUL-002 — No mocks, stubs, fakes, skips or monkeypatch
- ast — the freshness this gate's INVALID outcome polices
- prolog-boundary — a different Prolog gate: query sites vs. declared predicates