Orchestrata Ultimate › Gates › ast (AST refresh)
Gate: __ast__ (AST refresh)
At a glance
| Purpose | Refuse to evaluate any battery gate against a stale plans/ast-ultimate.json, and refuse a refresh that leaves runtime_calls empty. |
| Where it runs | The scripts/gates.py battery only, as the automatic pre-step of main(). |
| Trigger | Every python scripts/gates.py run whose src/ tests/ scripts/ fingerprint differs from the cached __ast__ entry (always under --all); skipped with --no-fix-ast. |
| Exact command | uv run ast-sync, then python scripts/build_runtime_callgraph.py reaugment, then a runtime_calls count check (refresh_ast(), read through the AST toolkit). |
| Blocks | A failed step or a zero runtime_calls count prints [gates] FAIL ast-refresh … and the battery returns 1 before any gate runs. |
| Ruling / ADR | None names the pre-step. The session-time snapshot path is ADR-0097. |
| Last recorded verdict | ok: true under __ast__ in the primary checkout's .cache/gates.json (file last written 2026-09-22 10:01 local). wt-MAP-PRECOMMIT has no .cache/gates.json. |
__ast__ is the one registry name in .cache/gates.json that is not an entry in
the GATES dict — it is the automatic pre-step scripts/gates.py::main() runs
before any gate, and it is cached and verdict-tracked exactly like the others. It is
listed here because the brief's registry census counts 24 cached names, and this is
the 24th.
What it refuses
Running the battery against a stale plans/ast-ultimate.json snapshot — and, just as
important, refusing any refresh that silently destroys the runtime call graph the
RTM depends on.
Why it exists
From scripts/gates.py's module docstring (self-healing behavior #3):
"AST staleness:
plans/ast-ultimate.jsongoes stale on every source change, anduv run ast-syncZEROESruntime_callsunlessbuild_runtime_callgraph.py reaugmentruns after it. That two-step ordering was documented and still forgotten repeatedly;--fix-astdoes both, in order, and verifies the augmentation landed."
And from refresh_ast()'s own docstring:
"Both steps, in this order, always.
ast-syncrebuildsplans/ast-ultimate.jsonfrom source and drops everyruntime_callsentry;build_runtime_callgraph.py reaugmentputs them back from the recorded runtime capture. Running the first without the second silently destroys the runtime call graph the RTM depends on — measured twice, and the reason this is one function rather than two commands to remember."
Staleness is called "the single most frequent gate failure" in the same file, and the refresh runs before the gates "so validate-ci sees a fresh snapshot rather than failing and demanding a second invocation."
How it runs
- Command (what the pre-step executes):
followed by a verification that at least one function in the refreshed snapshot carriesuv run ast-sync python scripts/build_runtime_callgraph.py reaugmentruntime_calls— a zero count is a hard failure:"ast-sync ran but runtime_calls is still empty after reaugment — the runtime call graph was destroyed, not refreshed"
- When it fires: at the top of every
python scripts/gates.pyrun, unless--no-fix-astis passed. Under--allthe refresh is forced along with everything else (the cache is empty by construction). - Inputs / caching: fingerprinted over
("src", "tests", "scripts")with the default suffix set — "Every tree the snapshot covers (ast_pipeline.SOURCE_TREES), not just src: fingerprinting one of three trees means a change under tests/ or scripts/ leaves the cached snapshot looking fresh, and the gate then answers from a fact base that predates the change." The verdict is stored under__ast__in.cache/gates.json.
Typical failures and how to fix them
- A failed
ast-sync, a failedreaugment, or a zeroruntime_callscount prints[gates] FAIL ast-refresh …and the whole battery returns 1 immediately — no gate runs against the stale snapshot. - On success it prints
AST refreshed; <N> functions carry runtime_callsand the battery proceeds. - Un-blocking: run the two commands above by hand in that order (or just re-run
scripts/gates.py, which does it for you); ifreaugmentfails, the recorded runtime capture itself needs regenerating (scripts/build_runtime_callgraph.py). - Docstring drift in the script. The module docstring's usage block still lists
python scripts/gates.py --fix-ast("also refresh a stale AST snapshot"), butmain()'s parser defines only--no-fix-astand refreshes by default (read through the AST toolkit, 2026-10-23). The refresh is on unless you pass--no-fix-ast. - Trap: the refresh calls a plain
uv run.refresh_ast()runs["uv", "run", "ast-sync"]without--no-sync, andautoformat()runsuv run ruffanduv run blackthe same way. Memoryuv-run-flips-venv-editable(2026-10-22) records that a plainuv runin a checkout without direnv re-syncs the venv editable, which the pre-push source-provenance gate then refuses. Whether the battery's calls flip the venv was not measured.
Wiring
- Invoker:
scripts/gates.py::main()(the only invoker of the cached pre-step); the same two commands are what a human runs after any source change per CLAUDE.md. - Downstream dependents: the prolog-gate
and uncovered-policies
fact extractor reads the snapshot exclusively;
validate-ci's Gate 10 checks "AST outputs exist and are not stale"; the whole AST toolkit answers from it. - Tests:
tests/scripts/test_gates.py,tests/scripts/test_gates_coverage.py(the battery),tests/scripts/test_build_runtime_callgraph.py(the reaugment half),tests/scripts/test_ast_freshness_pin.py(the session-start freshness pin).
Related
- Gates master index
- Development workflow
- freshness-guard — the test runner's own stale-input check
- prolog-gate — exits 2 INVALID on a stale fact base rather than passing
- source-provenance — the gate an editable venv trips
- ADR-0097 — the resident AST daemon publishes the snapshot
- The session hooks
ast-freshness-pin.sh/ast-incremental-refresh.shon the session-hooks family page