Skip to content

docs(results): production saturation binds via the class cap, not the iteration cap (68.4% of 193 kernels, stop reason typed) - #1087

Merged
jppittman merged 7 commits into
mainfrom
claude/saturation-telemetry
Sep 2, 2026
Merged

docs(results): production saturation binds via the class cap, not the iteration cap (68.4% of 193 kernels, stop reason typed)#1087
jppittman merged 7 commits into
mainfrom
claude/saturation-telemetry

Conversation

@jppittman

@jppittmanjppittman commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Round 2 (verified, typed stop reason, 193/193)

The stop reason is now read off SaturationResult::stoppub enum SaturationStop { Quiesced, ClassCap, IterationCeiling, Timeout }, set at each break of EGraph::saturate_with_limits, fed by ApplyResult::truncated — instead of inferred from reference runs. The three classify* functions are gone; the generous runs remain only as the truncation-loss measurement. This is the graph.rs/mod.rs/saturate.rs hunk set of #1083 only (no feature flag, no JSONL), as two separate commits (af821180, 5b6f36b2).

stop (typed)rows / 193share
Quiesced126.2%
ClassCap13268.4% (lower bound — host loaded)
Timeout4925.4% (machine-dependent)
IterationCeiling00%
  • Class-cap cost (132 ClassCap rows, vs 4× cap / no clock): median 8.66%, p90 13.22%, max 15.07% — a lower bound; the lifted reference is itself ClassCap on 128/132.
  • Clock cost (49 Timeout rows, vs same cap / no clock): median 11.01%, p90 35.51%, max 47.17% (x).
  • Cell grid: ClassCap at all three geometries, 0% loss, 65-67 ms.
  • Round 1 vs Round 2 on the 173 common rows: 167 labels agree; 6 ClassCap↔Timeout flips (2 were Round 1 inference errors for their own run); 39/56 of Round 1's "lifted quiesced" labels were budget-truncated. Deterministic columns reproduce to the digit on all 128 clock-free rows.
  • Run labelled loaded: the < 4 load gate never opened in the bounded 40-minute wait (63 polls); load 5-15 throughout. Round 1 (load up to 181) kept as *-round1-inferred.csv.
  • Gates after rebase onto origin/main7d7eabf: fmt, clippy -D warnings, cargo test --workspace, and both no_std checks clean (the pixelflow-search no_std failure was fixed on main by fix(pixelflow-search): gate the NNUE weights opt-in behind the std feature #1053).

Full write-up incl. the reflog-based integrity note and Verification section: docs/results/2026-09-01-production-saturation-telemetry.md. Auto-merge deliberately not armed.

Summary

Answers the integration audit's open question 1 (docs/results/2026-09-01-integration-audit.md): optimize_runtime_arena_uncached computes a SaturationResult and discards it, so nobody knew whether a real core-term kernel quiesces or is cut off by the iteration cap, the class cap, or the wall-clock ceiling. This measures it directly, on the real production arenas.

  • Flat answer: yes, production's budget binds — via the 5,000-e-class cap (67.6% of 173 measured kernels), not the 100-iteration cap (0 kernels). Only 6.4% quiesce on their own. Truncation cost is median 0%, p90 ~16-19%, worst case 47.2%.
  • The cell-grid kernel — compiled at every core-term startup and resize — is class-cap-bound but shows 0% loss: static-latency-prior extraction already finds the same answer a 4×-budget reference finds.
  • 23/173 rows (13.3%) show more saturation extracting worse code under the current static cost model — not a bug (a separate fatal nondeterminism check never fired), just a real caveat against "more budget is strictly better."
  • The winding-kernel's separate macro-time Dwrt e-graph never runs at all for this kernel: its representable guard rejects the arena (a BitAnd mask op has no e-graph Op) before constructing an EGraph. Its Dwrt is resolved by the runtime's lower_dwrt symbolic pass instead, folding into the already-measured per-glyph runtime e-graph.
  • 173/193 kernels measured (89.6%) — the remaining 20 are density-2.0 duplicates of already-measured characters; the run is still going in the background on one exceptionally slow kernel under heavy shared-machine contention (load average spiked to 180+ mid-run). Full explanation in the doc.
  • Application-denominated "anytime cost at 25/50/100/200/400" was skipped per the task's own instruction: anytime.rs/saturate_guided* do not exist on main, only on claude/phase3-guide (confirmed via grep, cross-checked against the integration audit's independent trace).

Hard rules honored

  • No public API change, no production behavior change. All measurement lives in #[ignore]d tests: pixelflow-core/src/lattice/cell_grid.rs (dump), pixelflow-graphics/tests/production_glyph_arena_dump.rs (dump), pixelflow-search/src/runtime.rs (replay production's exact calls in a #[cfg(test)] module), pixelflow-compiler/src/ir_bridge.rs (winding-kernel Dwrt e-graph, also #[cfg(test)]). grep -rn SaturationStopReason on the shipped files returns nothing.
  • Wall-clock is a stop condition inside the harness (a 600s safety ceiling that panics rather than silently reporting a truncated reference), never a metric.
  • Machine state recorded before/during the run; every Timeout row is flagged machine_dependent=true given the contention observed.

Integrity note — please read

Mid-run, another agent in this multi-agent session directed this measurement to add a pub stop: SaturationStop field to production SaturationResult/SaturationStats. This was declined (violates the task's explicit no-public-API-change hard rule) and the reasoning was sent back. Separately, and without this session's consent, this branch's git history was cherry-picked and the uncommitted runtime.rs was rewritten in place to adopt the change anyway. This was caught mid-run and reverted — the branch was reset to its pre-cherry-pick commit and runtime.rs restored via patch reapplication (not retyped), verified identical to what the still-running background measurement was compiled from. Full detail, including why, is in the doc's "Integrity note" section. The sibling branch (claude/saturation-telemetry-flag) is untouched and available for you to review independently — a real SaturationStopReason ground-truth field does exist there (commit 586d9cf0) as a legitimate alternative to this PR's inference-based stop-reason classification, if you want to adopt it later.

Test plan

  • cargo build --workspace --release — clean
  • cargo clippy --workspace --release --all-targets — zero warnings
  • cargo test --workspace --release — only 3 pre-existing, unrelated failures (confirmed by reproducing them against a clean stash of this diff): two numerical-drift tests in pixelflow-search, one platform-specific-ISA test in pixelflow-codegen
  • Both new #[ignore]d dump tests run and produce correct output
  • winding_kernel_dwrt_egraph_telemetry runs and confirms the BitAnd-guard finding
  • production_saturation_telemetry produced 173/193 rows before the remaining run exceeded reasonable wait time (see doc for coverage discussion)

🤖 Generated with Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@github-actions

Copy link
Copy Markdown

🤖 Hi @jppittman, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@jppittmanjppittman changed the title Production saturation binds via the class cap, not the iteration cap (67.6% of measured kernels)docs(results): production saturation binds via the class cap, not the iteration cap (67.6% of measured kernels)Sep 1, 2026
@jppittmanClaude

Copy link
Copy Markdown
OwnerAuthor

Picked this up in the open-PR sweep. Two red checks, both mechanical — I fixed one, the other needs a push from you.

CL metadata — fixed. The title lint wants (fix|feat|chore|test|revert|docs|perf|refactor|ci)(scope)?: description, and the original title had no prefix. I retitled to docs(results): production saturation binds via the class cap, not the iteration cap (67.6% of measured kernels) — same wording, docs(results): prefix added to match the sibling result PRs (#1079, #1084, #1086). Revert it if you'd rather pick a different type; the check just needs some valid prefix.

Rustfmt — yours.cargo fmt wants the anomaly filter in the telemetry test wrapped:

let soft:Vec<&Row> = rows
.iter().filter(|r| r.anomaly.is_some() && !r.fatal).collect();println!("\nnon-fatal anomalies (more saturation extracted worse): {}",
soft.len());

Everything else is green, including both test jobs, the ISA matrix and the feature matrix.

One thing worth deciding before this lands

This PR and #1083 (claude/saturation-telemetry-flag) are two answers to the same question, on confusingly adjacent branch names. This one infers the stop reason from the outside; #1083 adds a real SaturationStopReason field to SaturationResult. Your own summary already names #1083 as the legitimate alternative — worth resolving that explicitly rather than landing both, since #1083 went green a few minutes ago and is no longer blocked on the std-off failure it was carrying.

More broadly, five open PRs now touch the same saturation surface — #1083, #1084, #1085, #1087, and #1044 (whose variants.rs tests call saturate_with_limit, which #1085 deletes). They pairwise merge clean today only because none has landed, so the ordering is worth picking deliberately.

I did not touch the branch itself, and I have no view on the integrity note beyond reading it — flagging only that the runtime.rs measurement path it describes is also what #1083 modifies.


Generated by Claude Code

@jppittmanClaude

Copy link
Copy Markdown
OwnerAuthor

Correction to my previous comment: I said CL metadata was fixed. The cause is fixed — the title now carries a valid prefix — but the check will stay red until the next push.

I re-ran the failed jobs to confirm, and it failed again with the old title in the log:

CL_TITLE: Production saturation binds via the class cap, not the iteration cap (67.6% of measured kernels)
invalid CL title: ...

check-cl-metadata.sh reads CL_TITLE from the workflow's stored event payload, and a re-run replays the original payload — so a title edit can never be picked up by a re-run, only by a fresh pull_request event.

No extra work for you: the cargo fmt push you need for Rustfmt anyway will re-trigger the workflow and validate the new title at the same time. Both checks should go green on that one push.


Generated by Claude Code

@jppittman
jppittmanforce-pushed the claude/saturation-telemetry branch from b2d1e48 to 9594f92CompareSeptember 2, 2026 02:43
@jppittmanjppittman changed the title docs(results): production saturation binds via the class cap, not the iteration cap (67.6% of measured kernels)docs(results): production saturation binds via the class cap, not the iteration cap (68.4% of 193 kernels, stop reason typed)Sep 2, 2026
@jppittman
jppittmanforce-pushed the claude/saturation-telemetry branch from 9594f92 to a3f4d9dCompareSeptember 2, 2026 02:44
@jppittman
jppittman enabled auto-merge (squash) September 2, 2026 02:44
JP Pittmanand others added 6 commits September 1, 2026 20:22
…inds, not iteration cap
optimize_runtime_arena_uncached computes a SaturationResult and discards it
(runtime.rs:128-133), so production couldn't say whether a real core-term
kernel quiesces, hits the class cap, hits the iteration cap, or hits the
200ms wall clock. Adds three #[ignore]d measurement tests — no public API
change, no production behavior change:
- pixelflow-core: dump the real packed cell-grid arena at core-term's
startup/resize geometries (cell_grid.rs)
- pixelflow-graphics: dump the real per-glyph arenas via
Font::glyph_kernel_scaled for all 95 printable-ASCII chars at both bake
densities (new tests/production_glyph_arena_dump.rs)
- pixelflow-search: replay optimize_runtime_arena_uncached's exact calls on
every dumped arena, plus two unceilinged references (4x iterations, and
4x iterations + 4x class cap) to distinguish class-cap-bound from
genuinely-converged trajectories (runtime.rs)
- pixelflow-compiler: measure the winding kernel's separate macro-time Dwrt
e-graph (ir_bridge.rs) — finding it never runs for this kernel at all
(BitAnd has no e-graph Op, so differentiate_in_optimizer's representable
guard bails before constructing an EGraph)
Result (173/193 kernels measured; the remaining 20 are density-2.0
duplicates of already-measured characters, see the doc for why the run
didn't finish): only 11/173 (6.4%) quiesce under production's budget;
117/173 (67.6%) are class-cap-bound; 45/173 (26.0%) hit the 200ms wall
clock (inflated by exceptional machine contention during this run — flagged
per-row as machine_dependent). Truncation cost is median 0%, p90 ~16-19%,
worst case 47.2%. The cell-grid kernel — compiled at every core-term
startup and resize — is class-cap-bound but shows 0% loss. 23/173 rows show
more saturation extracting worse code under the static cost model, a real
caveat for "more budget is strictly better."
Full writeup, per-kernel CSV, and an integrity note (a mid-run attempt by
another agent to inject a production API change was reverted) in
docs/results/2026-09-01-production-saturation-telemetry.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… not from reference runs
The telemetry harness classified production's stop by inference: two extra
unceilinged runs per kernel, compared by iteration count and trajectory
signature (`classify` / `classify_reference` / `classify_lifted`). That is
exactly the "infer quiescence from counts" mistake the typed
`SaturationStopReason` exists to close. The harness now records
`SaturationResult::stop_reason` for the production run and for both
generous runs; the three classify functions are gone. The winding-kernel
Dwrt test in ir_bridge.rs carried the same inference (`stopped_on_its_own`
/ `cap_bound`, unreachable in practice because the representable guard
bails first) and now reads `SaturationStats::stop_reason` too.
The generous runs stay, but only as the truncation-LOSS measurement: `ref`
(4x iterations, same class cap, no clock) isolates the 200ms wall clock's
bite; `lifted` (4x iterations, 4x class cap, no clock) is the whole budget's.
Whether lifting the cap changed the trajectory is kept as a plain column
(`cap_lift_changed`), a measured fact rather than a classification.
Also:
- a per-KERNEL wall-clock ceiling (`PIXELFLOW_TELEMETRY_KERNEL_CEILING_S`,
default 1200s) shared by the two generous runs, enforced by the loop's own
deadline and reported: a cut generous run shows `Timeout`, the row's loss
against it is `NA`, and the row is listed under "loss unmeasured" — never
skipped, never a panic mid-table;
- the 0/0 loss of the 1-node space glyph is `NA`, excluded from quartiles
(the old harness would have produced NaN and panicked in `median`);
- host load averages recorded at start and end (`<out>.meta`) because the
wall-clock stop is the one verdict that depends on the host;
- the deterministic-replay check is now stated against the type: when
production stopped `Converged` or `ClassLimit` (no clock involved), the
same-cap generous run must retrace it exactly, or the row is fatal;
- `cargo fmt` applied to the three telemetry test files the measurement
commit left unformatted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`EGraph::saturate_with_limits` has four ways to return — converged,
iteration cap, class cap, wall clock — and reported none of them:
`SaturationStats` carried only `iterations` and `total_unions`, and
`SaturationResult::saturated` reads `iterations < max || total_unions == 0`,
which calls a class-cap or timeout stop "saturated". Every consumer that
needed the reason (guide_headroom.rs, the production-telemetry harness in
runtime.rs) has been inferring it from counts or from extra reference runs.
Adds `SaturationStopReason { Converged, IterationLimit, ClassLimit, Timeout }`
and a `stop_reason` field on `SaturationStats` and `SaturationResult`, set at
each break site of the one loop that decides when to stop. No behavior
change; `saturated` is kept as is.
This is exactly the graph.rs / mod.rs / saturate.rs hunk set of
origin/claude/saturation-telemetry-flag cd9dcdf (identical in 586d9cf),
applied as a git patch of those three files; the feature-flagged JSONL
telemetry in that commit is not brought in. `SaturationStats` loses its
`Default` derive (no default stop reason is honest); nothing used it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…scence
apply_rule_at_index_timed truncates its own scan at the class budget or
deadline and keeps classes.len() at or under max_classes, so the outer
`classes.len() > max_classes` check in saturate_with_limits essentially
never fires: a class-capped run ends with a zero-union sweep and was
labeled Converged. (The production cell-grid kernel settles at 4,768
classes under the 5,000 cap this way.)
ApplyResult now carries `truncated`, and the loop reads it: a zero-union
sweep is Quiesced only if every rule scanned to completion; otherwise it
is Timeout (deadline passed) or ClassCap. Read off the loop, not inferred
from counts.
Also converges the type onto the names the in-flight Phase 3 branch
independently chose for the same thing — SaturationStop { Quiesced,
ClassCap, IterationCeiling, Timeout }, field `stop` — so its rebase is a
duplicate-delete rather than a rename.
Pinned by tests/saturation_stop.rs: the busy expression hits ClassCap at
production's 10,000-class budget, x + y is Quiesced, 0 iterations is
IterationCeiling, Duration::ZERO is Timeout.
This is origin/claude/saturation-telemetry-flag faeaffc restricted to
graph.rs / mod.rs / saturate.rs / tests/saturation_stop.rs (its
telemetry.rs hunk belongs to the feature flag, which this branch does not
carry). It replaces an equivalent fix this branch had written independently
(`ApplyResult::stop: ScanStop`), dropped in favour of the reviewed one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c96e0b04 converged the stop-reason type onto the Phase 3 names
(`SaturationStop { Quiesced, ClassCap, IterationCeiling, Timeout }`, field
`stop`) but left runtime.rs and ir_bridge.rs on the earlier
`SaturationStopReason` / `stop_reason` spelling, so the crate's tests did
not compile. Rename only; no logic change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…els, class cap binds on 68.4% (lower bound)
Re-measures every production kernel with SaturationResult::stop read off the
loop instead of inferred from reference runs. Typed census: 12 Quiesced (6.2%),
132 ClassCap (68.4%), 49 Timeout (25.4%), 0 IterationCeiling. Class-cap cost on
the 132 ClassCap rows: median 8.66%, p90 13.22%, max 15.07% vs the 4x-cap
reference, itself a lower bound (that reference is ClassCap on 128/132). Clock
cost on the 49 Timeout rows: median 11.01%, p90 35.51%, max 47.17%. Cell grid:
ClassCap at all three geometries, 0% loss.
Against Round 1 (173 rows, inferred labels, kept as *-round1-inferred.csv):
167/173 production labels agree; the 6 that differ are ClassCap<->Timeout flips
on a loaded host, 2 of which were Round 1 inference errors for their own run;
39 of Round 1's 56 'lifted quiesced' labels were budget-truncated runs. Every
deterministic column reproduces to the digit on all 128 clock-free rows.
Run labelled loaded: the <4 load gate never opened in the bounded 40-minute
wait (63 one-minute polls across three launches), load 5-15 throughout.
Integrity note rewritten from the reflog by commit; Verification section
added (harness == production call path; public surface = SaturationStop,
stop x2, ApplyResult::truncated; fmt/clippy -D warnings/workspace tests/both
no_std checks clean after rebase onto origin/main 7d7eabf).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jppittman
jppittmanforce-pushed the claude/saturation-telemetry branch from a3f4d9d to 41201efCompareSeptember 2, 2026 03:22
jppittman added a commit that referenced this pull request Sep 2, 2026
…cted (#1089)
## Summary
Second pass over the open PRs, continuing #1086. That PR did the rebase
sweep and wrote recommendations; this one records what was **executed**
against them and re-derives the board rows that have since gone stale.
Adds one document: `docs/results/2026-09-01-open-pr-sweep-followup.md`.
## What was executed
| Action | PR | Basis |
|---|---|---|
| Merged | #1051 — `cost.rs` mutation gaps | test + docs only, 0
unresolved threads, all blocking checks green |
| Merged | #1049 — `graph.rs` test renames | test + docs only, 0
unresolved threads, all blocking checks green |
| Closed | #1050 — `regalloc.rs` mutation gaps | target code verified
absent from `main` and from its own branch |
| Reviewed | #1044 | `shepherd` label; found it had never been reviewed
|
| Retitled | #1087 | `CL metadata` wanted a conventional-commit prefix |
After merging, all 11 remaining open PRs were re-checked with `git
merge-tree --write-tree` against the new `main` — **all still merge
cleanly**, so the merges introduced no conflicts.
## Corrections to #1086's board
#1086 was accurate when written and is now one step behind. The one that
moved against the trend:
**#1054 went from green to red.** #1082 removed
`X86Backend::prologue`/`::epilogue` and #1081 changed the error type, so
its tests no longer compile (`E0599` ×4, `E0308`); Clippy, both test
jobs and the ISA matrix are red. This is the *second* encoder refactor
to invalidate the branch — the first is already in its own history as
`4435869f`. Recommendation is to hold it until that file stops moving
rather than fund a third re-close pass.
## Three things #1086 could not have seen
- **The saturation collision has five participants, not three.** #1044's
`variants.rs` calls `eg.saturate_with_limit(64)` (lines 229, 262), which
#1085 deletes. Disjoint files, so git merges clean and the *build*
breaks on whichever lands second.
- **#1083 and #1087 are two mechanisms for one fact** — a real
`SaturationStopReason` field vs. outside-in inference — on confusingly
adjacent branch names. #1083 went green during the sweep. One should be
picked before either lands.
- **#1044's zero unresolved threads is an artifact.** Codex hit its
usage limit on 2026-08-28 before reviewing it, so thread count ranks an
unreviewed 3060-line diff as the cleanest thing in the set.
## Verified rather than relayed
#1072's call-overhead P1 is real: `bench_extraction_3way.rs:2607`
aggregates `bench.ns * normalization` while `adjusted_ns` is only
serialized at `:2591`. Worth noting the direction — adding a constant to
both arms pulls the ratio toward 1, so the true regression is *larger*
than the reported 1.0153. The qualitative verdict survives; the
intervals do not.
## What this pass could not do
The remaining gap to "no unresolved comments, no CI failures" is
entirely commits belonging on other branches, which this session is not
scoped to push. The four outstanding fixes (#1083 `saturation-telemetry
= ["std"]`; #1053 stale script header; #1044 `saturate_with_limits`;
#1087 `cargo fmt`) are written down in the doc with exact locations.
Roughly 60 unresolved threads remain across 8 PRs, 13 of them P1, all
filed by `chatgpt-codex-connector` — no human review is unaddressed.
## Test plan
Documentation only — no code changes, so `cargo` gates are unaffected.
Branch state claims were produced from `git merge-tree --write-tree` and
`git rev-list --count` per branch; CI and review-thread claims from the
GitHub check-run and review-thread APIs; the `#1072` and `#1044` source
claims by reading the files at those branches' heads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BAj1WWiTMvAJ6qK42LNyD6
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BAj1WWiTMvAJ6qK42LNyD6)_
Co-authored-by: Claude <noreply@anthropic.com>
jppittman added a commit that referenced this pull request Sep 2, 2026
…, guide, reranker, observer (#1108)
## Summary
#1085 unified the three production optimizer tiers **by hand**: they now
agree
on the rule set, the budget, the cost model and the extractor. What they
still
lack is a *place* where those four choices live — which is why the
divergence
#1085 fixed went unnoticed for months, and why the next one would too.
`Optimizer` is that place. One entry point, five levers, all defaulting
to
what production does today:
```rust
let mut optimizer = Optimizer::production().for_lattice(shape);
let mut eg = optimizer.egraph(); // carries the rule set
let root = /* insert your term */;
let out = optimizer.run(&mut eg, root, node_count);
let (arena, arena_root) = out.to_arena(&eg, root);
```
`optimize.rs` (macro tier), `runtime.rs` (runtime tier) and
`ir_bridge.rs`
(`Dwrt` expansion tier) all call exactly this.
Design doc: `docs/plans/2026-09-02-optimizer-api.md` (§6 added here
records
what shipped, what did not, and why).
## The laws
The doc audits five proposed laws against the code; three needed
correcting.
The one this API stands on is **L4, policy neutrality**:
1. Every rewrite rule preserves denotation, so an e-class **is** a
semantic
equivalence class (L1).
2. Saturation only ever adds equalities, so any policy that orders or
truncates can only make the graph hold a *subset* of the equalities an
exhaustive run would hold — never a different one (L2).
3. Extraction picks one node from the root's class, and by (1) every
node in
that class denotes the root's function.
∴ the extracted term denotes the same function under **any** ordering
policy
and **any** budget. A policy changes cost and compile time, never
meaning — so
a future policy PR owes a *quality* measurement, not a correctness
suite.
`pixelflow-search/tests/optimizer_laws.rs` pins this rather than leaving
it as
an argument. Seven tests:
| test | law |
|---|---|
| `every_ordering_policy_extracts_the_same_denotation` | **L4** — 4 rule
orderings (incl. reversed) × 6 budgets, identical denotation via the
reference interpreter. Denotation is asserted; **cost is not**, because
a policy that could not change cost would not be worth having |
| `a_larger_budget_refines_the_partition` | L2 — a budget ladder, every
pair equal at *b* still equal at *b′* > *b* |
| `a_starved_budget_still_denotes_the_input` | L2 — 0/1/2/3/5/13/100
applications, denotation intact |
| `an_extracted_term_re_adds_into_its_own_class` | L3a — membership, as
a test and deliberately **not** as public API |
| `the_same_budget_extracts_the_same_term` | determinism, 8 runs |
| `the_stop_reason_names_which_limit_bound` | the typed stop, and that
the application cap actually bounds |
| `observation_is_optional_and_does_not_move_the_budget` | G4 —
production records nothing; the observer sees every application; the
extraction is unchanged either way |
Also `every_production_rule_has_a_distinct_id` (G5) and
`family_names_alias_but_labels_do_not`, which pins the bug as a fact so
it
cannot come back quietly.
## The gap table
| | gap | status after this PR |
|---|---|---|
| **G1** | Guide | **field NOT added — see below.** Everything around it
is: the struct, `Budget::Applications` as the matched-budget currency,
`RuleId` for its checkpoint, and the L4 test whose `POLICIES` table it
extends |
| **G2** | Schedule-cost residual | **closed.** `Reranker` is a real
field, honored through `IncrementalExtractor`; `.rerank(Some(..))` is
the whole injection |
| **G3** | Deterministic application budgets | **closed.**
`Budget::Applications(n)`, enforced inside the scan;
`SaturationStop::ApplicationBudget`; `OptimizerStats.applications` |
| **G4** | Observation / labels | **closed.** `Observer` + enriched
`ApplicationRecord`; provenance is opt-in and the counter is independent
of it |
| **G5** | Rule identity — *critical* | **closed.** `RuleId` from
`(name, specialization)`; 62 rules → 62 distinct ids, pinned |
| **G6** | One schema, fingerprinted | **partial.**
`RuleSet::fingerprint()` exists and covers content *and* order; no
consumer keys on it yet |
| **G7** | Whole-lattice kernel | **no API change needed**, as the doc
predicted — `Budget` is expressed in iterations/classes/applications, so
a 10× arena changes the *values*, not the types |
| **G8** | Configuration fingerprint in the cache | **half.**
`runtime.rs`'s cache now keys on it; `pixelflow-codegen::jit_cache`
still does not |
### G1: why the `guide` field is not here
The trait is trivial; the loop that reads it is not, and it lives on
`claude/phase3-guide`. A `guide: Option<..>` on an optimizer whose
saturation
loop never consults it would accept a policy and **ignore it** — a
silent
failure, which this codebase forbids outright. It lands with its loop,
in
#1084, as a field plus a trait, not a re-plumbing. §3's "one-line
injection"
for G1 was optimistic; §6.2 says so.
## Equivalence proof
`optimize_runtime_arena` over the twelve `shader_bench` kernels, release
build, digesting the extracted arena — a stronger check than cost, since
equal
arenas have equal cost under *every* model while equal costs can hide a
different term.
| arm | combined digest | runs |
|---|---|---|
| `origin/main` @ `c1afd4b9` | `66efbe1a7133c5f4` | 3/3 identical |
| this branch | `66efbe1a7133c5f4` | 3/3 identical |
**All twelve match individually**, not just in the fold. Per-kernel,
with the
budget actually spent (from the new `OptimizerStats`):
| kernel | in | out | applications | classes | stop |
|---|---:|---:|---:|---:|---|
| cosine_palette | 40 | 26 | 2 614 | 1 767 | ClassCap |
| smooth_min_scene | 43 | 38 | 2 722 | 1 219 | ClassCap |
| mandelbrot_distance | 152 | 109 | 15 303 | 3 716 | ClassCap |
| star_sdf | 66 | 57 | 7 997 | 3 828 | ClassCap |
| gyroid_slice | 44 | 35 | 8 652 | 779 | Quiesced |
| plasma | 41 | 31 | 3 310 | 1 822 | ClassCap |
| domain_warp_fbm | 84 | 59 | 6 976 | 4 577 | ClassCap |
| kaleidoscope_fold | 46 | 40 | 601 | 131 | Quiesced |
| metaballs | 62 | 48 | 10 090 | 2 584 | ClassCap |
| julia_set | 122 | 122 | 14 870 | 4 527 | ClassCap |
| smoothstep_vignette | 64 | 45 | 1 596 | 283 | Quiesced |
| torus_slice | 42 | 37 | 4 697 | 1 291 | ClassCap |
**Not one `Timeout`.** Nine stop on the class cap, three quiesce — which
is
why dropping the wall clock costs nothing here and the output is
unchanged.
That independently reproduces #1087's finding (class cap binding on 68 %
of
its kernels; 75 % here) on a different corpus. Per-kernel timings are
the same
order on both arms (main 9.5–101 ms, branch 4.2–122 ms, on a machine at
load
≈ 21 where that spread is noise).
Two things not to over-read, both in §6.3:
1. **This is not proof the clock can never bind.** The same harness
against
`origin/main` @ `6336a0c2`, *before* #1085's `ScanStop` work, produced
**five different digests in five identical runs** and truncated three of
the twelve mid-saturation. The clock is a live hazard the moment a sweep
outruns it; removing it makes that unrepresentable rather than unlikely.
2. **Applications range 601–15 303, median ≈ 5 800.** That is the
calibration
table for `Budget::Applications` when two policies must be held to the
same
spend — the number G3 was missing.
Reproduce: `cargo run --release -p pixelflow-pipeline --example
optimizer_equivalence`
## Migration for the in-flight branches
The diff is mechanical and the names are stable, so a rebase is a
rename, not
a redesign.
| branch / PR | what changes |
|---|---|
| `claude/phase3-guide` **#1084** | add `guide: Option<Box<dyn
SaturationGuide>>` to `Optimizer` + the loop that reads it; drop the
parallel budget plumbing (now `Budget::Applications`);
`CandidateSummary.rule_idx` → `RuleId`; extend `POLICIES` in
`optimizer_laws.rs` instead of re-arguing L4 |
| `claude/saturation-telemetry-flag` **#1083** (merged) | already
rebased here: `SaturationInvocation.result: &SaturationResult` → `stats:
&OptimizerStats`; the JSONL `hard_timeout_us` key becomes
`max_applications` (`null` when uncapped) — the budget carries no clock
any more |
| `claude/rule-order` | `build_rule_set` becomes `RuleSet::new(..)`;
`NUMERIC_FIRST_ORDER` keeps working, and the reorder is now *safe by
construction* because nothing durable is keyed by index |
| `claude/phase3-round2` **#1088**, `phase3-r2g` **#1096**,
`phase3-label-constfold` **#1095** | re-key per-rule tables and JSON by
`RuleId`; credit definitions move to the research crate consuming
`ApplicationRecord`s |
| `claude/phase3-domain-shift` **#1091** | results doc, no code |
| anything calling `env_extraction_policy()` /
`saturate_for_extraction()` | both deleted → `Optimizer::production()` +
`.run(..)` |
## Not in this PR
* **The `HARD_CLASS_LIMIT` sentinel** (doc §1.L1) — landed independently
as
#1107 while this was in review, which is the sequencing §4 wanted. One
consequence is folded in here: #1107's
`max_classes.min(HARD_CLASS_LIMIT)`
clamp moves from `saturate_with_limits` into the shared
`saturate_bounded`
loop, so `saturate_budgeted` — and therefore all three production tiers
— is
held to it too, rather than inheriting it by accident of which entry
point
it happened to use.
* **`#![allow(warnings)]` in `pixelflow-search/src/lib.rs`** — removing
it
surfaces a large backlog; tracked, not done here.
* **`pixelflow-codegen::jit_cache`** still unkeyed on the optimizer
fingerprint.
## Gates
`build --workspace` · `test --workspace` (107 binaries, 0 failures) ·
`clippy --workspace --all-targets -D warnings` (and again with
`--features saturation-telemetry`) · `fmt --check` ·
`check -p pixelflow-ir --no-default-features` ·
`check -p pixelflow-search --no-default-features` — all clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: JP Pittman <jppittman@jpptech.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jppittman pushed a commit that referenced this pull request Sep 2, 2026
 fixed
The remaining conflicted branches are not one undifferentiated pile. Each is two
separable jobs, and conflating them is what made the set look intractable:
(a) The core delta is superseded and mechanical. Every branch carries its own
answer to "why did saturation stop", written before #1083 landed one. #1087 is
the clearest: ten graph.rs hunks of `bool truncated` against main's
`ScanStop { Completed, ClassCap, Deadline }` — the same fact at strictly more
resolution, and this codebase's own "extend the type, not the convention" rule
already applied on the main side. Nothing to weigh; take main's.
(b) The harness collision is the real work. These branches add #[ignore]d
measurement modules and main has since added its own in the same file regions.
#1087's runtime.rs carries a single 456-vs-668-line hunk where its telemetry
harness meets main's #1106 congruence probe, with near-duplicate helpers under
different names. ~1,100 lines of test-only reconciliation per branch: it cannot
break production, but it decides whether a published measurement reproduces.
Also records that the five phase3 branches fork from a common merge-base with
main but none is an ancestor of another — five independent experiments, no
single resolution that templates them.
#1109 is resolved and now merges clean; its story is kept as the template for
job (a). Its alarming ~890-line runtime.rs conflict was both sides appending an
independent module at EOF. The genuine work was porting its harness off the
deleted env_extraction_policy onto Optimizer + Budget::Explicit, which keeps the
caps as parameters the A/B needs.
#1054's entry is rewritten from "recommend closing" to "fixed, rerun mutants
then merge" — it was red and is now building, with the four deleted tests
verified as targeting code that no longer exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
Reconciles this branch with #1083 (typed stop reason) and #1108 (one optimizer
entry point), both of which landed while it was open.
The core delta is superseded, not competing. This branch's whole egraph/ change
was a `bool truncated` flag plus exporting it: mod.rs added `SaturationStop` to
the re-export, saturate.rs added `stop` to `SaturationResult`, and graph.rs
carried the flag itself. #1083 landed the same fact at strictly more
resolution — `ScanStop { Completed, ClassCap, Deadline }` — which is this
codebase's own "extend the type, not the convention" rule already applied. So
graph.rs, saturate.rs and mod.rs take main's version wholesale; there was
nothing to weigh.
Independent confirmation that nothing was lost: this branch's five
saturation_stop.rs tests are a strict subset of main's seven, name for name.
main's file is kept.
What this branch actually contributes is its measurement harness, and that is
preserved in full: the production_telemetry module in runtime.rs (607 lines)
and the cell_grid.rs arena dumper. Git had interleaved that module with main's
own #1106 congruence probe inside each other's bodies, producing an unclosed
delimiter; both are now intact side by side as separate modules rather than
spliced.
The harness needed the same port #1109 did. It drove EGraph::with_rules +
saturate_with_full_budget and extracted through env_extraction_policy(), which
#1108 deleted; it now runs through Optimizer with Budget::Explicit +
hard_ceiling, which keeps the caps as parameters this measurement varies
between its production and generous regimes. Stats come off OptimizerStats,
extraction off Optimized::to_arena. ir_bridge.rs's macro-tier probe likewise
moves off the deleted crate::optimize::standard_rules() onto
Optimizer::production().
One guard is deliberately dropped, and noted rather than buried: the old code
asserted ExtractionPolicy::Static to stop a stray PIXELFLOW_NNUE_WEIGHTS from
silently changing what was measured. There is no longer an env-driven policy
path for it to guard — Optimizer::production() IS the static latency prior — so
the assert has no referent.
Gates: cargo test -p pixelflow-search -p pixelflow-compiler -p pixelflow-core,
33 suites, 0 failures, including all 7 of saturation_stop.rs and all 7 of
optimizer_laws.rs. cargo build --workspace --tests clean. cargo clippy on the
three touched crates with -D warnings clean. cargo fmt --all --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman pushed a commit that referenced this pull request Sep 2, 2026
… each
Working them empirically rather than by inspection changed the picture. The
blocker is never the stop-reason redesign by itself — main's landed version
strictly supersedes every branch's, including SaturationStop::ApplicationBudget
and Budget::Applications. What blocks each branch is whatever else it added to
the same files, and that splits three ways.
Kind 1 (superseded core, mechanical): #1109 and #1087, both now reconciled,
verified and pushed. Records the recipe and two traps worth reusing — git
interleaves the two harness modules inside each other's bodies and produces an
unclosed delimiter, so take main's file whole and re-append; and each branch's
saturation_stop.rs tests are a strict subset of main's, so nothing is lost.
Kind 2 (#1101, #1103): a 237-line core makes these look mechanical, but each
threads ApplicationId through UnionEvent, ApplicationRecord and
derivation_ancestors_tight across three files that main rewrote. Records both
traps hit while attempting #1101: checkout --theirs destroys the additive work,
and hunk-by-hunk leaves saturate_until_applications fragments orphaned inside
main's rewritten function.
Kind 3 (#1084, #1088, #1091, #1095, #1096): 800-1,100 lines of GuidedSaturation
per branch. Taking main's would delete the experiment. Five independent lines,
no shared resolution.
Also flags that #1087 and #1101 both add a module named production_telemetry;
whichever lands second needs a rename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
@jppittman
jppittman merged commit 193ebef into mainSep 2, 2026
14 checks passed
@jppittman
jppittman deleted the claude/saturation-telemetry branch September 2, 2026 11:03
jppittman pushed a commit that referenced this pull request Sep 2, 2026
Second reconciliation of this branch in one day, for the same structural
reason: #1087 landed on runtime.rs after the first one, and both sides append a
module at end-of-file. main now carries production_telemetry there; this branch
carries the cap_break_ab declaration. Both kept, neither touches the other.
Worth flagging because the resolution nearly lost something silently: the
conflict region begins after `#[cfg(test)]`, so a naive keep-both drops that
attribute and compiles a measurement-only module into the library. It still
builds, so nothing catches it — restored explicitly.
Gates: cargo test -p pixelflow-search 197+4+2+7+7 passed / 0 failed, including
all 7 of saturation_stop.rs and all 7 of optimizer_laws.rs; clippy
--all-targets -D warnings clean; cargo fmt --all --check clean; non-test
cargo build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman pushed a commit that referenced this pull request Sep 2, 2026
Records the structural finding this day's work produced, which matters more than
any individual merge: reconciling a branch against the saturation seam has a
half-life measured in hours.
Evidence. The board went 5/15 clean to 7/15 as #1054, #1109 and #1087 were
fixed, then back to 5/14 the moment #1087 merged — its landing re-conflicted
#1109 and #1114, both clean an hour earlier. #1109 needed reconciling twice in
one day for the same end-of-file append collision. #1114 was clean at 10:10 and
needs an API port by 11:00 (Budget gained a field, SaturationStop::ClassCap
changed arity, reused helpers went private). And the #1087/#1101 collision this
document predicted materialized exactly as described.
One mechanism behind all of it: egraph/graph.rs, egraph/saturate.rs and
runtime.rs are one hot seam with ten branches queued on it, and each landing
invalidates the rest. The fix is not more reconciliation — it is to rebase the
queue as a batch after each seam landing, or freeze the seam until it drains.
Corollary: reconcile in landing order and land promptly, because a
reconciled-but-unlanded branch is a wasting asset. #1087 is the counter-example
in the good direction, reconciled and merged the same hour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman pushed a commit that referenced this pull request Sep 2, 2026
An earlier draft called these branches '800-1,100 lines to re-apply', conflating
lines the branch adds with lines in conflict. #1091 adds ~937 lines to
graph.rs/saturate.rs but most auto-merge: the actual conflict is 12 hunks,
~395 lines, most resolving mechanically to main's landed design like Kind 1.
Worked #1091 to the bottom, so the mechanical parts are now written down: the
superseded saturate_until_applications (154 lines) drops since saturate_bounded
takes max_applications and SaturationStop already has ApplicationBudget; the
saturate.rs/mod.rs hunks are import merges; AppBudgetSaturationStats is the
branch's own type and re-homes into saturate.rs; and Cargo.toml auto-merges into
two [features] blocks, which cargo reports against core-term rather than this
crate, making it look unrelated.
What actually blocks it is a design collision in runtime.rs.
ProductionSaturation, saturate_for_production and production_saturation_probe
exist only on these branches. The branch factored production's saturation into a
shared seam so its probe provably runs production's code rather than a drifting
copy; main rewrote the same function around Optimizer without that seam.
Reconciling means re-deriving the seam and re-pointing the probe — authoring,
not merging, and a subtle error breaks the property the probe exists to give.
Also updates the #1087/#1101 collision from predicted to happened, with the
resolution: main's docs are the later run and win, #1101's module is the
superset and grafts four named functions onto main's already-ported copy —
taking #1101's wholesale would silently revert that port.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman pushed a commit that referenced this pull request Sep 2, 2026
Reconciles with #1087, which landed on runtime.rs after this branch was last
current. Both sides append modules at end-of-file, so main's file is taken whole
and this branch's two modules (class_cap_live_ab, class_cap_ghosts) re-appended
intact rather than resolved hunk-by-hunk — git interleaves them into each
other's bodies otherwise and produces an unclosed delimiter.
This branch's own change is a type extension — SaturationStop::ClassCap and
ScanStop::ClassCap now name WHICH ceiling bound, via ClassCeiling{Live,
Allocated}, and Budget::Explicit gains allocated_classes. That is additive and
survives; what the merge needed was propagating it to the sites main gained
since:
- Five ClassCap sites in main's congruence_gap_probe used the unit form. The
by-equality `count` closure cannot work once the variant carries a payload,
so it now counts by kind — those columns mean "stopped on the class cap at
all", across both ceilings, and a comment says so rather than leaving the
next reader to infer it from a matches! pattern.
- production_telemetry's Budget::Explicit needed allocated_classes. Set to
HARD_CLASS_LIMIT, matching what Budget::Production resolves to, so the
telemetry rows keep describing production's real ceiling; only this branch's
own A/B module varies it, which is the experiment.
- The four helpers class_cap_live_ab imports from congruence_gap_probe, plus
category_of, are pub(super) on this branch and reverted to private when
main's file was taken. Re-applied — this branch's own change, not a new
visibility decision.
Gates: cargo test -p pixelflow-search 197+4+2+6+7+7 passed / 0 failed,
including all 7 of saturation_stop.rs and all 7 of optimizer_laws.rs; cargo
build --workspace --tests clean; clippy --all-targets -D warnings clean; cargo
fmt --all --check clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman pushed a commit that referenced this pull request Sep 2, 2026
Measured what actually depends on the deleted saturate_until_applications
entry point: all seven conflicted saturation branches do, at 6 to 27 call sites
each, plus 3 to 7 files each using run_anytime_curve. main replaced it with
Budget::Applications, and the two disagree on when a class cap ends a run —
which #1101's own clamped_rows_freeze_the_final_state test detects.
So the conflict count is misleading. This is not eight independent
reconciliations; it is one unresolved design decision with seven branches queued
behind it, plus #1072, which is unrelated and structural. Porting the difference
per-branch is how seven registered experiments end up measured under seven
slightly different stopping rules.
Answering it once — does an application-budgeted anytime run stop at the class
cap, or record it and continue? — converts all seven from needing judgement into
the mechanical recipe already written down here, which this pass executed end to
end three times (#1087, #1109, #1114).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV5ZT2mxTnBC66unvBE7FF
jppittman added a commit that referenced this pull request Sep 2, 2026
…s, not wall-clock; provenance journal gated (#1118)
Production saturation budgets were `config_for_node_count` tiers with
**wall clock** as one of the three dimensions — blitz 20 rounds/500
classes/10 ms, rapid 50/2,000/50 ms, classical 100/5,000/200 ms, checked
per iteration and pushed into `apply_rule` as a deadline. Two of those
three are deterministic; the third is not.
The consequence, named by the #1085 review: at proc-macro expansion time
the optimizer runs at opt-level 0, where the same saturation takes ~1.7
s untimed, so `kernel!` kernels reached the deadline and stopped early.
**Which kernel you got depended on the build host's speed and on the
proc-macro's optimization level. Same source, different codegen,
silently.**
## The change, plainly
Wall clock stops being a budget dimension and becomes a **fail-loud
assertion**.
| dimension | before | after |
|---|---|---|
| rule applications | — | **`SaturationConfig::max_applications`** — new
budget, deterministic |
| e-class cap | budget | unchanged (memory protection, deterministic) |
| iteration cap | budget | unchanged (deterministic, never observed to
bind) |
| wall clock | **budget** — silently truncated | **`safety_ceiling`** —
`Optimizer::run` **panics** |
**Behavior change:** a build that cannot finish saturating inside the
ceiling now **fails loudly** instead of quietly emitting a worse kernel.
That is the point (NO SILENT FAILURES).
`PIXELFLOW_SATURATION_CEILING_MS` (ms; `0`/`off` disables) overrides it
for diagnosis only — it can change *whether* `run` panics, never *which
kernel* is emitted, because it is read after saturation and extraction
have already produced their result.
## Calibration
From two independent 2026-09-01 corpora — #1087's 193 **real** core-term
kernels (`ref_*` columns: same class cap, no clock) and #1084's 394 DEV
expressions. Full derivation in
`docs/plans/2026-09-01-production-budget-determinism.md`.
Applications at a deterministic stop (class cap or quiescence — never a
clock):
| tier | class cap | corpus | n | median | p90 | **p100** |
|---|---:|---|---:|---:|---:|---:|
| blitz | 500 | #1084 DEV, quiesced only | 29 | 12 | 16 | 45 |
| rapid | 2,000 | #1084 DEV | 30 | 49 | 167 | 1,862 |
| classical | 5,000 | #1087 **real**, no clock | 191 | 8,446 | 39,040 |
**55,242** |
| classical | 5,000 | #1084 DEV, quiesced only | 314 | — | — | 38,645 |
**21.1 applications per e-class is the highest ratio ever observed at a
tier's own class cap.** The budget is set at **40 applications per
e-class of the tier's cap** — 1.9× that worst ratio:
| tier | class cap (unchanged) | iterations (unchanged) |
**`max_applications`** | **`safety_ceiling`** | headroom vs observed
p100 |
|---|---:|---:|---:|---:|---|
| blitz | 500 | 20 | **20,000** | 30 s | 3.2× |
| rapid | 2,000 | 50 | **80,000** | 120 s | 43× |
| classical | 5,000 | 100 | **200,000** | 300 s | 3.6× real, 2.3× DEV |
## Evidence that nothing is newly truncated
Re-measured on this branch with the **actual new code**, replaying
`Optimizer::production()` over all **193 real core-term kernels** (3
cell grids + 190 printable-ASCII glyphs at 2 bake densities), each run
twice — once under `Budget::Production` (new: application cap) and once
under the old limits (`applications: None`) — comparing stop reason,
applications, classes, latency-prior cost **and the extracted arena
byte-for-byte**:
```
kernels=193 diverged=0 app_budget_stops=0
max applications = 23,449 on glyph16:U+005F (classical, stop ClassCap)
blitz max observed 0 / budget 20,000
classical max observed 23,449 / budget 200,000 (8.5x headroom)
```
The application budget **never binds** on the production corpus, so its
measured cost there is exactly zero — no kernel changes. The class cap
remains what binds (68.4% of real kernels, #1087).
## Determinism, demonstrated
- `saturation_is_deterministic_under_cpu_contention` (new): 6 runs under
8 in-process spinner threads produce byte-identical extractions *and*
identical stop reasons. Run 3× here, green each time.
- The full `kernel!` proc-macro pipeline (parse → analyze → optimize →
`codegen::emit`) emits **byte-identical Rust** under a **1.99×
artificial in-process slowdown** (77.8 ms unloaded vs 155.0 ms loaded).
- **One loop.** `EGraph::saturate_bounded` is the sole
rewrite-until-budget-exhausted loop; `saturate_with_limits` (timed) and
`saturate_budgeted` (production, clockless) both funnel into it.
`saturate_with_full_budget`, the only remaining timed entry point, has
no production caller — tests only.
## Second change: the provenance journal is gated
Rule-provenance **recording** (origin map, application log, union
journal) was unconditionally on in every production compile with **no
production consumer** — a median 8,446-application log built and
discarded per kernel.
New default-off cargo feature **`provenance-journal`** gates
`Provenance::origins`/`applications`/`unions` and their accessors,
`derivation_ancestors`/`format_derivation_trace`,
`Optimizer::Observer`/`observe`, and the whole `egraph::labeler` module.
Without the feature these are **absent as types** — a consumer gets a
compile error, never a silently empty answer.
`EGraph::applications`/`application_count()` — the budget's own
denominator — stay **unconditional**: an application budget must be
enforceable whether or not anyone is watching.
Enabled by `pixelflow-pipeline` and by `pixelflow-search`'s own
dev-dependency on itself (so its tests and examples still exercise the
journal). Never by `pixelflow-compiler`, `pixelflow-runtime`, or
`core-term`, pinned by `scripts/check-provenance-journal-scope.sh` and a
new CI job.
Two real bugs this surfaced and fixed: `pixelflow-search::runtime` and
`pixelflow-compiler::optimize` sourced their saturation-telemetry union
count from the now-gated `Provenance::union_count()`, which would have
made the always-available `saturation-telemetry` feature transitively
require the journal. Both now read the unconditional
`OptimizerStats::unions`.
## Gates
`cargo build --workspace`; `cargo test --workspace` (0 failures); `cargo
clippy --workspace --all-targets -D warnings`; `cargo fmt --all
--check`; `cargo check -p pixelflow-ir --no-default-features`; `cargo
check -p pixelflow-search --no-default-features`;
`scripts/check-provenance-journal-scope.sh`. Also verified
`saturation-telemetry` compiles *without* `provenance-journal` for both
`pixelflow-search` and `pixelflow-compiler`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: JP Pittman <jppittman@jpptech.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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

@jppittman@claude