Skip to content

RUE-1406: consume prebuilt programs in the CLI corpus (ADR-0070 P2) - #2318

Merged
DorianListens merged 4 commits into
trunkfrom
claude/adr-70-phase-2-cli-98xtm9
Aug 12, 2026
Merged

RUE-1406: consume prebuilt programs in the CLI corpus (ADR-0070 P2)#2318
DorianListens merged 4 commits into
trunkfrom
claude/adr-70-phase-2-cli-98xtm9

Conversation

@DorianListens

@DorianListens DorianListens commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes RUE-1406. Part of ADR-0070 (docs/designs/0070-rue-program-build-actions.md), following RUE-1405.

What this does

The 64 CLI cases naming a checked-in root each compiled that root inside their own test execution, which never reaches the action cache. Those roots are rue_program build actions now, and the harness runs the prebuilt executable a case names.

  • Eight new rue_program targets for the CLI-named roots: harbor, jsonfmt, lattice, mosaic, rill, ruelex, first/stats, second/calculator. examples/meridian/main.rue is the ninth and is already a Phase 1 large-example program, so one artifact serves both the slow-tier scenarios and the six CLI ones — "many scenarios, one compile" reaching across two suites. examples/first/ holds three sibling roots, so first-stats names its one file instead of globbing the directory; every other root owns its directory and takes the directory-bounded glob.
  • A rue_program_staging rule collecting them into //:cli-staged-programs, a symlinked directory keyed by root path, so the harness's lookup key is the case's own source_path string with no name mangling on either side. //:cli-tests, //:cli-tests-slow and the four shards declare it through the same attrs.arg() / $(location ...) contract every other corpus input already uses.
  • A staged execution mode in rue-cli-tests. Cases keep their TOML form, their corpus actions, their names, labels and tiers.
  • cli.examples_meridian is no longer skipped. RUE-1083 disabled its six scenarios because each paid a full 80.7s compile of the same 36k-line root; they share one cached artifact now, so the corpus pays six runtime scenarios. That coverage was running nowhere.

No sharding change, no TOML migration, no decision on the per-file corpus redesign (RUE-1408). RUE-924's corpus-omission audit is untouched — target names, labels and tiers all carry over.

How the two modes stay honest

case_runs_prebuilt_program destructures Case exhaustively and stages a case only when it leaves the compile exactly as the rue_program action performed it — no argument override, no compiler environment beyond the standard library the program already declares, no runner-driven -O, and no assertion about a compile the harness does not run. Because the pattern names every field, a new Case field is a compile error here rather than a silent assumption that the new knob does not affect the compile.

Past that decision point the two modes are one shared path, run_case_program: same working directory, argv[0], stdin, environment, timeouts and expectations. ADR-0070's Consequences section names drift between the compile path and the staged path as this phase's cost; the exhaustive pattern and the shared runner are the two things holding it.

With RUE_CLI_STAGED_PROGRAMS unset — the //crates/rue-cli-tests:cli developer entry point, or any hand-run invocation — every case compiles exactly as before.

What deliberately keeps compiling in the harness

The 6 cross-target cli-test-fixtures cases (one consumer per (root, target) tuple), the repo-relative source_path fixture case (its subject is the TOML resolution mechanism), the one differential_opt calculator case (four compiles by design), the one-scenario wordfreq root, the ~4,050 inline-source cases, and the RUE-48 automatic example smokes.

//:release-smoke does not carry the staged directory either: it runs only the differential_opt filter, so declaring it there would add nine release-configured program compiles to a deliberately bounded lane for cases that can never consume them.

Positive warm-cache control

scripts/check-rue-program-warm-cache.sh now covers both consumer shapes ADR-0070 has. It cold-builds the canaries and the nine staged programs under a fresh nonce, then from a relocated checkout root runs the canaries' four scenarios and rebuilds //:cli-staged-programs, failing unless every rue_scan / rue_derive_manifest / rue_compile action was served by the remote cache.

The nonce did not create a cold namespace, and this job had never run. rue-program-warm landed with Phase 1 and its first-ever execution was dispatched from this branch (run 31615598448) — it failed in 2.6 minutes, and trunk failed the same way in 1.6 (run 31615608639):

FAIL: cold build was cache-served; the nonce did not create a cold namespace:
    build root//:meridian-canary (rue_scan meridian-canary)    cache
    build root//:caldera-canary  (rue_compile caldera-canary)  cache   ...

RUE_CACHE_PROBE_NONCE reaches Rust actions as an otherwise-unused --cfg (toolchains/rust/BUCK, RUE-1034). That re-keys the Rust actions, but an unused cfg does not change the compiler's emitted bytes, and rue_scan / rue_derive_manifest / rue_compile are keyed on the compiler artifact — so every canary action was served from the shared cache. It only looked correct in Phase 1 because nothing had populated those artifacts yet. The nine new programs here were genuinely cold for the same reason: nothing had ever built them.

rue_rules.bzl now carries the nonce into those three actions directly. Verified locally on //:jsonfmt:

Condition Result
No nonce, across the rules change no-op rebuild — ordinary action keys unchanged
Fresh nonce all three rue_* actions execute locally, cold
Same nonce twice no re-execution — the warm half can still take cache hits

The timeout stays at 60. The earlier 60 → 90 raise is reverted. Run 31615598448 measured the nine staged programs cold at ~78s of wall time (whole cold build 2m24s including the compiler; meridian, the long pole, ~30-40s) — the existing headroom already covers it. Two caveats worth stating: that run never achieved a cold namespace for the canaries, so it under-measures a fully cold build; and meridian at ~30-40s is well under the 80.7s this description quotes elsewhere, which is compiler performance work landing since those numbers were recorded, not a change from this PR.

Verification

The container this was authored in has no working buck2 (dotslash is unavailable and github.com is blocked), so the suite was not run locally — CI is the authority here. What was checked:

  • The new predicate, the staging helper and their four unit tests were extracted into a scratch crate against the pinned 1.92.0 toolchain: compiles, clippy-clean under the repository's allow list, 4/4 tests pass. Adding a field to Case was confirmed to produce E0027, so the drift guard fires as intended.
  • rustfmt --check clean on crates/rue-cli-tests/src/main.rs with the repository config.
  • bash -n on the warm-cache script; syntax-parsed BUCK, rue_rules.bzl and corpus.bzl.
  • Case inventory counted directly against the tree: 73 cases name a checked-in root, 64 stage, matching the ADR's accounting.
  • Verified no example root imports outside its own directory, so no rue_program's scan can read outside its declared srcs.

Staging is asserted, not assumed

staged_program returned None when a case's executable was absent and the case then compiled its own root with nothing reported. Absence had to be tolerated, because it is also the deliberate exclusion for roots no rue_program owns — so a renamed root, an edited source_path, or a case that grew a compile-touching field would have put its cases back to compiling with nothing red. That is the coverage-shaped failure RUE-924 describes, in a PR that cites RUE-924.

rue_program_staging now writes staged-roots.txt into the directory, and the harness validates it against the complete unfiltered corpus before libtest2 applies filters — the same way validate_contract_metadata already runs, so a shard checks the whole graph rather than its own slice. A listed root must exist and must have at least one case consuming it. The reverse stays legal: an eligible case whose root is absent from the manifest compiles, which is what the repo-relative source_path fixture and the one-scenario wordfreq root need.

Verified against the real staging directory and the full 1,850-case inventory:

Scenario Result
Real staging directory cli-tests: 64 case(s) run prebuilt rue_program executables — the 64 this description claims, now asserted at runtime
Staged root no case consumes fails: staged program(s) no case consumes: examples/ghost/main.rue
Listed root with no executable fails: staged program '…' is listed in staged-roots.txt but absent from '…'
RUE_CLI_STAGED_PROGRAMS at a directory with no manifest fails: its staged-roots.txt is unreadable
No env var (developer entry point) silent, every case compiles, unchanged

cli.examples_mosaic::mosaic_prints_reference passes both ways — 0.18s staged against 2.09s compiling.

Shard weights, refreshed from measurement

The six restored meridian scenarios had no weights at all — they had never run — so shard balance and the derived corpus deadlines sized them from default_ms (1059 ms). This branch's CI run is the first whose inventory includes them, so its four cli-shard-*-case-timings artifacts were fed back through scripts/generate-cli-shard-weights.py:

python3 scripts/generate-cli-shard-weights.py \
  --timings linux-x64=shard0.jsonl --timings linux-x64=shard1.jsonl \
  --timings linux-x64=shard2.jsonl --timings linux-x64=shard3.jsonl

Meridian measures 11-31 ms per scenario, so the placeholder was conservative rather than dangerous — the risk was over-weighting, not a missed deadline.

The same data measures this phase on linux-x64:

Case Before After
cli.examples_lattice::lattice_prints_reference 79.3s 0.01s
cli.examples_lattice::lattice_runs_cross_oracle_selftest 78.6s 0.25s
cli.examples_harbor::harbor_validates_and_simulates_scenario 58.2s 0.01s
cli.examples::lattice::main (RUE-48 smoke, still compiles) 70.9s 12.79s

36 cases drop by more than three seconds, and a linux-x64 corpus pass sheds 1,051 seconds.

common, default_ms, and the linux-arm64 and macos maps are untouched — the generator replaces a whole map per platform, and required CI only shards the CLI corpus on linux-x64. Those platforms keep falling back to common (platforms[p]commondefault_ms) until a run on each supplies timings, which is the remaining half of this follow-up.

Known follow-up

linux-arm64 and macos shard weights still carry the old compile-dominated costs, because required CI only shards the CLI corpus on linux-x64 and the generator replaces one platform map at a time. Both fall back to common, so nothing breaks; refreshing them needs a full corpus pass on each platform.

claude and others added 4 commits August 12, 2026 14:31
The 64 CLI cases naming a checked-in root each compiled that root inside
their own test execution, which never reaches the action cache. The nine
roots are `rue_program` build actions now — eight new, plus Phase 1's
`examples/meridian/main.rue`, so one artifact serves both the slow-tier
large-example scenarios and the six CLI ones — collected by a new
`rue_program_staging` rule into `//:cli-staged-programs` and declared by
//:cli-tests, //:cli-tests-slow and the four shards through the same
`$(location ...)` env contract every other corpus input already uses.

The cases keep their TOML form, their corpus actions, their names, labels
and tiers; RUE-924's corpus-omission audit is untouched. What changes is
that the harness runs the staged executable instead of compiling. It
decides structurally rather than from a list: `case_runs_prebuilt_program`
destructures `Case` exhaustively and stages only a case that leaves the
compile exactly as the `rue_program` action performed it, so a new case
field is a compile error rather than a silent assumption. Everything past
that point — working directory, argv, stdin, environment, timeouts,
expectations — is one shared path, `run_case_program`, which is what keeps
the two modes from drifting. With RUE_CLI_STAGED_PROGRAMS unset, as at the
`//crates/rue-cli-tests:cli` developer entry point, every case compiles.

Staying compile-in-harness, all deliberate: the six cross-target
cli-test-fixtures cases, the repo-relative `source_path` fixture whose
subject is the TOML mechanism itself, the one differential_opt calculator
case, the one-scenario wordfreq root, and the RUE-48 automatic example
smokes. //:release-smoke does not carry the staged directory either — it
runs only the differential_opt filter, so it would pay nine
release-configured compiles for cases that can never consume them.

`cli.examples_meridian` is no longer skipped. RUE-1083 disabled it because
each of its six cases paid a full 80.7s compile of the same 36k-line root;
they share one cached artifact now, so the corpus pays six runtime
scenarios. That coverage was running nowhere.

Phase 2 also repeats Phase 1's positive warm-cache check for CLI roots:
check-rue-program-warm-cache.sh cold-builds the staged programs alongside
the canaries and asserts every scan/derive/compile action is cache-served
when the staged directory is rebuilt from a relocated checkout root.

The checked shard weights still carry the old compile-dominated costs for
these cases; they rebalance from the next run's measured timings.
The warm-cache control asserts its first build executes rue_scan,
rue_derive_manifest and rue_compile cold. It could not: the probe nonce
reaches Rust actions as an otherwise-unused --cfg, which re-keys the Rust
actions but leaves the compiler's emitted bytes identical, and these actions
are keyed on the compiler artifact. Every canary action was served from the
shared cache and the job failed its own cold-namespace assertion the first
time it ran, on trunk as well as here.

Carry the nonce into the three actions directly. With no nonce set,
_probe_env returns its argument untouched and action keys are unchanged --
verified by rebuilding //:jsonfmt across the change as a no-op; a fresh nonce
re-executes all three actions locally, and a repeated nonce does not.

Also restore the job's 60-minute timeout. The nine staged CLI programs add
about 78 seconds of wall time to the cold half, meridian's ~30-40s among it,
so the existing headroom already covers them.
A case whose prebuilt executable was missing fell back to compiling its own
root, silently. Absence had to be tolerated, because it is also the deliberate
exclusion for roots no rue_program owns -- so a renamed root, an edited
source_path, or a case that grew a compile-touching field would have left its
cases compiling again with nothing red, which is the coverage-shaped failure
RUE-924 describes.

Have the staging directory declare its inventory in staged-roots.txt, and
validate it against the complete unfiltered corpus before libtest2 applies
filters, the way the contract metadata is already validated. A root listed in
the manifest must exist and must have at least one case that consumes it; the
reverse direction stays legal, so the source_path fixture and the wordfreq
root keep compiling in the harness. The run reports how many cases take the
staged path, so a lane that loses staging is visible in its log rather than
only in its wall clock.
The six restored meridian scenarios had no weights at all -- they had never
run -- so shard balance and the derived corpus deadlines were sizing them from
default_ms. Regenerated platforms["linux-x64"] from the four cli-shard
case-timings artifacts of this branch's CI run, which is the first run whose
inventory includes them: 11-31ms each, well under the 1059ms placeholder, so
the placeholder was conservative rather than dangerous.

The same data measures this phase: 36 cases drop by more than three seconds
and a linux-x64 corpus pass sheds 1051s, with the lattice scenarios going from
about 79s each to hundredths of a second. The automatic RUE-48 smokes still
compile their roots and still cost what they cost.

common, default_ms, and the linux-arm64 and macos maps are untouched; those
platforms keep falling back to common until a run on each supplies timings.
@DorianListens
DorianListens added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@DorianListens
DorianListens added this pull request to the merge queue Aug 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 12, 2026
@DorianListens
DorianListens added this pull request to the merge queue Aug 12, 2026
Merged via the queue into trunk with commit e88953d Aug 12, 2026
23 checks passed
@DorianListens
DorianListens deleted the claude/adr-70-phase-2-cli-98xtm9 branch August 12, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants