Ripple gates -- the dispatch-surface CI gate set (#T1063)¶
What this is¶
When you register a new public op or edit a ToolEntry, the change
ripples to a fixed family of CI gates. That list used to live only in a
private memory file, so each build brief hand-transcribed a subset and dropped
gates -- rc385 (quaternion_log / quaternion_slerp) ate two CI-red rounds on
the worked-example gate family alone. This directory now carries the list in
the repo, as one runnable command:
The gate set is the committed manifest tools/ripple_gates.txt (one pytest
target per line; # comments and blank lines ignored). ripple_check.py reads
that manifest and runs exactly those gates -- the FAST dispatch-surface subset,
never the full ~10k-test suite. A meta-test,
tests/test_ripple_manifest_covers_known_gates.py, guarantees the manifest can
never silently shrink below the known families.
When to run it¶
Run it before pushing any rc that registers/edits an op or a ToolEntry --
i.e. any change that moves describe()["tools"]["total"], the C tool registry,
the carrier / rosetta / MCP surfaces, the worked-example ledger, or the emitted
docstrings. If in doubt, run it: it is fast.
The two-step story: regen, THEN verify¶
A dispatch-surface change usually needs the generated tree regenerated first,
in this exact order (the second step is easy to forget -- it is not a codegen
step and regen_all.py does not run it, but the executed-example ledger gate
reds without it):
python3 tools/regen_all.py # rebuild every generated file + verify idempotence
python3 tools/run_worked_examples.py --only-stale # refresh the executed-example ledger
ripple_check.py --regen runs both, in order, and then runs the gates -- one
command for the whole "regen, then verify" story:
Other modes: --regen-only (preamble only), --list (print the resolved
targets), --manifest PATH (alternate manifest).
After --regen, glance at git status: run_worked_examples.py executes
every snippet, so it refreshes tests/worked_examples_result.ndjson and can
leave an incidental scratch artifact from a snippet that writes a file. On a
CRLF (Windows) checkout the executor's LF writes also show as whole-file EOL
churn -- run the preamble on the WSL2 / LF checkout that build agents use, and
those are non-events.
The gate set¶
Grouped by family (the manifest is the SSOT; this table is the human-readable mirror):
| Family | Gate(s) | What a new op ripples to |
|---|---|---|
| C tool registry | test_tool_registry_c_rc184, test_tool_schema_ops_c_rc185, test_invoke_tool_c_rc188 |
the compiled-in C tool registry + schema + invoke path |
| carrier schema | test_carrier_schema_rc205 |
the carrier-registry generated table |
| rosetta | test_rosetta_transitive_standalone, test_rosetta_completeness |
standalone-C reachability + classification ledger (slowest gate ~30s) |
| c-claims | test_c_claim_resolution_rc300 |
the generated C-claims resolution table |
| MCP | test_mcpb_emit (tool list == advertised introspection), test_mcp_marshal_c_rc187, test_mcp_sse_c_rc194, test_mcp_stdio_c_rc186 |
a new op appears in the MCP surface (bundle + C marshalling over the SSE / stdio transports) |
| MCP coercer + signature drift | test_mcp.py::test_all_param_types_json_coercible, test_mcp.py::test_schema_signature_alignment_no_drift (by NODE-ID) |
the novel param-type axis: a new op declaring a param TYPE with no _PARAM_COERCERS handler, or declared params drifting from the signature (the rc273 / rc328 failure class). test_mcpb_emit does NOT cover coercion and the C-marshalling gates test the wire, not the Python coercer registry -- so these two carry the axis. They run pure (~sub-second, no server / no fixture). The rest of test_mcp.py (socket SSE server + subprocess round-trips) stays EXCLUDED -- as a whole file it hangs a fast runner (>90s, no clean exit) |
| regen graph | test_regen_all_rc346 |
the codegen dependency graph + idempotence |
| worked-example family | test_worked_examples_strict_zero_rc353 (registry-only, strict-zero), test_worked_examples_execute_rc354 (executed ledger -- needs run_worked_examples.py --only-stale) |
the two DIFFERENT-regen worked-example gates that bit rc385 |
| count-pin | test_registry_smoke_rc127, test_rc15_describe_resolve |
representative describe()["tools"]["total"] pins (the full blast radius is ~55 files -- see note) |
| class-TOML op-ref | test_class_catalog_oprefs_resolve_930 |
the third generated C table (srmech_class_registry.c) op refs |
| ref-notation | test_ref_notation_emitted_rc348 |
bare-#NNN autolink guard on emitted artifacts |
| JPL audit | test_jpl_audit |
C Power-of-Ten ratchet (any C-touching op) |
| version pin | test_signal_processing_scaffolding |
the single hard version-literal gate |
| non_compute / annex | test_non_compute_ratchet_rc170, test_annex_ratchet_rc177, test_annex_ratchet_rc183 |
the non_compute / annex classification ratchets |
| self-hosting import ban | test_selfhosting_import_ban |
the ONE table-driven ban list (#T1073): a new op-rc's new test file reaching for stdlib fractions (#845 / #870) — cost rc386 a full CI-red round because the runner omitted it — and a new package module reaching for numpy / math / decimal, or bypassing the srmech._json / srmech._toml front doors. Absorbed test_no_stdlib_fractions_import + test_no_stdlib_math_import + test_numpy_carrier_ratchet at rc405. Pure-Python, no native .so |
| decode-aware population pin | test_namespace_prefix_decode_aware_rc361 |
a new cascade op bumps the srmech.cascade DECODED-channel population (the ratchet's population half, invisible to a text grep). rc387 had to run it manually (100 → 102). Pure-Python (reads regen'd artifacts + decodes their byte arrays), ~seconds; no native .so |
Note on the count-pin blast radius¶
A tool-count bump ripples to ~55 test files that assert
describe()["tools"]["total"] == N. Running all 55 here would blur into the full
suite, so the manifest carries two cheap describe()-based representatives; if
they red on a count change, the fix is to update all the count-pins (CI runs
the full suite and will confirm). The point of including them here is early
detection: you learn the count moved before you push.
Why a standalone runner (not a pytest -m ripple marker)¶
The gate list lives in ONE committed file the runner reads. A marker would
require either editing the ~15 gate test files (churn, and it collides with
concurrently-running rcs editing those same files) or a conftest
pytest_collection_modifyitems hook (this package ships no pytest config today,
and editing the shared conftest.py is exactly the collision risk we want to
avoid). A standalone tools/ripple_check.py reading tools/ripple_gates.txt is
fully non-invasive: new files only, no edits to any existing gate test.