From 6aa823d29a356de02e8784bebd015752004c14d1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 08:33:12 +0000 Subject: [PATCH 01/11] Regen round cost instrument: trace_mark gains a process-tree CPU clock and an armed ledger, the regen host stamps its phases, and claim_executor --regen-round-cost prices one convergence round through gunbc.regen_round_cost Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- dag/gunbc/regen_round_cost.dag | 227 +++++++++++ .../claim/regen_round_cost_witness_test.dag | 118 ++++++ src/v1/runtime_rust.dag | 113 ++++-- src/v1/stage0/src/bin/claim_executor.rs | 36 ++ src/v1/stage0/src/cli_run.rs | 11 + src/v1/stage0/src/required_regen_host.rs | 353 +++++++++++++++++- src/v1/stage0/src/v1_rt.rs | 116 ++++-- 7 files changed, 904 insertions(+), 70 deletions(-) create mode 100644 dag/gunbc/regen_round_cost.dag create mode 100644 dag/test/claim/regen_round_cost_witness_test.dag diff --git a/dag/gunbc/regen_round_cost.dag b/dag/gunbc/regen_round_cost.dag new file mode 100644 index 00000000000..d5ec1926404 --- /dev/null +++ b/dag/gunbc/regen_round_cost.dag @@ -0,0 +1,227 @@ +module gunbc.regen_round_cost + +import std.types { String, Int, List, Bool } + +// ONE REGEN ROUND, PRICED BY PHASE, ON TWO CLOCKS -- the instrument behind +// `claim_executor --regen-round-cost`. A round is the convergence recipe that +// gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_repair_steps prints +// at every stranded author: build the seed, emit the mirrors from it, install what drifted, +// rebuild from the installed seed, and read what changed. Every lane in the self-host program +// pays that recipe on every edit inside the seed closure, and until this carrier existed the +// only figures for it were wall times copied into prose ("~18 min a round") that named no +// producer, no host and no tree -- exactly the transcription DESIGN section 6 forbids. +// +// WHY THIS IS AN EXTENSION OF v1_rt trace_mark AND NOT A SECOND INSTRUMENT. compile.dag already +// stamps compile.frontend / compile.normalize / compile.reconcile / compile.analyses / +// compile.emit through trace_mark, and those marks already print a wall elapsed. What they did +// not have was a CPU clock beside the wall one (std.measure measure_clock_basis_note: two clocks +// on one occurrence, neither substitutes) or any way for a consumer to READ them rather than +// scrape stderr. So the runtime gained a process-tree CPU reading at every mark and an armed +// ledger, the regen host gained marks at its own phase boundaries, and this module folds the +// ledger into the receipt. One producer of every figure, read in one place. +// +// CPU IS THE PROCESS TREE, STATED SO THE NUMBER IS NOT MISREAD. The seed build and the rustfmt +// normalization run in children; the compile runs in this process. A self-only clock would +// price the seed build at zero and a children-only clock would price the emit at zero, so the +// reading is utime+stime+cutime+cstime from the process's own accounting, and a child's CPU +// lands on the mark that reaped it. A phase whose wall is large and whose CPU is near zero is +// therefore waiting, not computing -- the discrimination cli_run's floor heartbeat exists for. +// +// WHERE THE READING CANNOT BE TAKEN IT IS UNREADABLE, NEVER ZERO. CpuUnreadable is the arm for a +// host without process accounting; a fabricated 0 would read as "free", which is the direction +// that looks survivable (gunbc.observation_seed_render seed_heartbeat_unreadable_cause is the +// same refusal one instrument over). + +type RegenRoundPhase + = SeedBuild + | CorpusLoad + | CorpusReload + | Frontend + | Normalize + | Reconcile + | Analyses + | Emit + | MirrorWrite + | CandidateVerify + | Adjudicate + | HandVerify + | Digest + | Install + | RebuildFromInstalled + | Diff + +type CpuReading + = CpuMs { ms: Int } + | CpuUnreadable + +// A drained ledger row. `label` is the trace_mark label with its `.done` suffix removed (a plain +// mark keeps its label whole); `wall_ms` is the elapsed since the matching `.begin`, or since the +// previous mark when no begin was opened, which is the contract trace_mark already had. +type TraceMark { + label: String + wall_ms: Int + cpu: CpuReading +} + +type RegenRoundPhaseAttribution + = Attributed { phase: RegenRoundPhase } + | Unattributed + +type RegenRoundCostReceipt { + producer: String + host: String + tree: String + tree_dirty: Bool + seed_build_compiled_crates: Int + rebuild_compiled_crates: Int + marks: List + changed_paths: List +} + +fn regen_round_phase_tag(phase: RegenRoundPhase) -> String { + match phase { + SeedBuild => "seed_build" + CorpusLoad => "corpus_load" + CorpusReload => "corpus_reload" + Frontend => "compile.frontend" + Normalize => "compile.normalize" + Reconcile => "compile.reconcile" + Analyses => "compile.analyses" + Emit => "compile.emit" + MirrorWrite => "mirror_write" + CandidateVerify => "candidate_verify" + Adjudicate => "adjudicate" + HandVerify => "hand_verify" + Digest => "digest" + Install => "install" + RebuildFromInstalled => "rebuild_from_installed" + Diff => "diff" + } +} + +// THE LABEL EACH PHASE IS STAMPED UNDER, and the one place both ends read it from. The compile +// labels are compile.dag's existing marks verbatim; the regen.* and round.* labels are the marks +// required_regen_host stamps. A mark under any other label is reported as Unattributed rather +// than dropped -- a sub-mark like compile.emit.dag.collect.done is real time and hiding it +// would make the phase lines sum to less than the round. +fn regen_round_phase_label(phase: RegenRoundPhase) -> String { + match phase { + SeedBuild => "round.seed_build" + CorpusLoad => "regen.corpus_load" + CorpusReload => "regen.corpus_reload" + Frontend => "compile.frontend" + Normalize => "compile.normalize" + Reconcile => "compile.reconcile" + Analyses => "compile.analyses" + Emit => "compile.emit" + MirrorWrite => "regen.mirror_write" + CandidateVerify => "regen.candidate_verify" + Adjudicate => "regen.adjudicate" + HandVerify => "regen.hand_verify" + Digest => "regen.digest" + Install => "round.install" + RebuildFromInstalled => "round.rebuild_from_installed" + Diff => "round.diff" + } +} + +fn regen_round_phases() -> List { + [SeedBuild, CorpusLoad, CorpusReload, Frontend, Normalize, Reconcile, Analyses, Emit, MirrorWrite, CandidateVerify, Adjudicate, HandVerify, Digest, Install, RebuildFromInstalled, Diff] +} + +fn regen_round_phase_of_label(label: String) -> RegenRoundPhaseAttribution { + fold(regen_round_phases(), init: Unattributed, f: fn(acc, phase) { + match acc { + Attributed { phase: p } => Attributed { phase: p } + Unattributed => + if regen_round_phase_label(phase: phase) == label { + Attributed { phase: phase } + } else { + Unattributed + } + } + }) +} + +fn regen_round_cpu_text(cpu: CpuReading) -> String { + match cpu { + CpuMs { ms: m } => to_string(m) + CpuUnreadable => "na" + } +} + +fn regen_round_mark_line(mark: TraceMark) -> String { + let subject = match regen_round_phase_of_label(label: mark.label) { + Attributed { phase: p } => concat("phase=", regen_round_phase_tag(phase: p)) + Unattributed => concat("unattributed label=", mark.label) + } + concat( + concat("regen-round-cost: ", subject), + concat(concat(" wall_ms=", to_string(mark.wall_ms)), concat(" cpu_ms=", regen_round_cpu_text(cpu: mark.cpu))) + ) +} + +fn regen_round_wall_total_ms(marks: List) -> Int { + fold(marks, init: 0, f: fn(acc, mark) { acc + mark.wall_ms }) +} + +// The CPU total is readable only when every row is; one unreadable row makes the sum a +// fabrication and the total says so instead of summing the rows it could read. +fn regen_round_cpu_total(marks: List) -> CpuReading { + fold(marks, init: CpuMs { ms: 0 }, f: fn(acc, mark) { + match acc { + CpuUnreadable => CpuUnreadable + CpuMs { ms: sum } => + match mark.cpu { + CpuUnreadable => CpuUnreadable + CpuMs { ms: m } => CpuMs { ms: sum + m } + } + } + }) +} + +fn regen_round_bool_text(b: Bool) -> String { + if b { "true" } else { "false" } +} + +fn regen_round_header_line(receipt: RegenRoundCostReceipt) -> String { + concat( + concat(concat("regen-round-cost: producer=", receipt.producer), concat(" host=", receipt.host)), + concat( + concat(concat(" tree=", receipt.tree), concat(" tree_dirty=", regen_round_bool_text(b: receipt.tree_dirty))), + concat( + concat(" seed_build_compiled_crates=", to_string(receipt.seed_build_compiled_crates)), + concat(" rebuild_compiled_crates=", to_string(receipt.rebuild_compiled_crates)) + ) + ) + ) +} + +fn regen_round_total_line(receipt: RegenRoundCostReceipt) -> String { + concat( + concat("regen-round-cost: total wall_ms=", to_string(regen_round_wall_total_ms(marks: receipt.marks))), + concat(" cpu_ms=", regen_round_cpu_text(cpu: regen_round_cpu_total(marks: receipt.marks))) + ) +} + +fn regen_round_changed_paths_line(receipt: RegenRoundCostReceipt) -> String { + let listed = fold(receipt.changed_paths, init: "", f: fn(acc, path) { + if acc == "" { path } else { concat(concat(acc, ", "), path) } + }) + concat( + concat("regen-round-cost: changed_paths=", to_string(count(receipt.changed_paths))), + concat(concat(" [", listed), "]") + ) +} + +// The whole receipt as the lines the host prints, in ledger order. Rendered here and read by +// the host through the interpreter, so there is no Rust mirror of this format to drift. +fn regen_round_cost_render(receipt: RegenRoundCostReceipt) -> String { + let mark_lines = fold(receipt.marks, init: "", f: fn(acc, mark) { + concat(concat(acc, regen_round_mark_line(mark: mark)), "\n") + }) + concat( + concat(concat(regen_round_header_line(receipt: receipt), "\n"), mark_lines), + concat(concat(regen_round_total_line(receipt: receipt), "\n"), concat(regen_round_changed_paths_line(receipt: receipt), "\n")) + ) +} diff --git a/dag/test/claim/regen_round_cost_witness_test.dag b/dag/test/claim/regen_round_cost_witness_test.dag new file mode 100644 index 00000000000..01a5a3e0b02 --- /dev/null +++ b/dag/test/claim/regen_round_cost_witness_test.dag @@ -0,0 +1,118 @@ +module test.claim.regen_round_cost_witness + +import std.types { Bool, String, List } +import gunbc.regen_round_cost { + RegenRoundCostReceipt, TraceMark, CpuReading, CpuMs, CpuUnreadable, + RegenRoundPhase, Emit, SeedBuild, RegenRoundPhaseAttribution, Attributed, Unattributed, + regen_round_phase_of_label, regen_round_phase_tag, regen_round_phase_label, regen_round_phases, + regen_round_cpu_total, regen_round_wall_total_ms, regen_round_mark_line, regen_round_cost_render +} + +// WHAT THIS WITNESS COVERS: the fold from a drained trace ledger to the receipt lines the host +// prints -- attribution of a label to a phase, the two totals, and the rendered text -- held +// against fixture rows so the format authority is exercised without a seed build, an emit or a +// host. WHAT IT DOES NOT COVER: that the seed stamps the labels this module names at the phase +// boundaries it claims. That half executes only in a real `claim_executor --regen-round-cost` +// run, whose receipt is the evidence; a mark the seed stamps under a label this module does not +// know renders as `unattributed`, so the disagreement is visible in the receipt rather than +// silent. + +fn a_mark(label: String, wall_ms: Int, cpu_ms: Int) -> TraceMark { + TraceMark { label: label, wall_ms: wall_ms, cpu: CpuMs { ms: cpu_ms } } +} + +fn a_receipt(marks: List) -> RegenRoundCostReceipt { + RegenRoundCostReceipt { + producer: "claim_executor --regen-round-cost", + host: "srv1", + tree: "2a11b317d2caf3c37d1d38a4421e8e0c06188925", + tree_dirty: false, + seed_build_compiled_crates: 0, + rebuild_compiled_crates: 2, + marks: marks, + changed_paths: ["v1_rt.rs", "v1_compiler_runtime_rust.rs"] + } +} + +fn attributed_to(attribution: RegenRoundPhaseAttribution, phase: RegenRoundPhase) -> Bool { + match attribution { + Attributed { phase: p } => regen_round_phase_tag(phase: p) == regen_round_phase_tag(phase: phase) + Unattributed => false + } +} + +fn is_unattributed(attribution: RegenRoundPhaseAttribution) -> Bool { + match attribution { + Attributed { phase: p } => false + Unattributed => true + } +} + +// POSITIVE CONTROL: the compile.dag mark that carries the emit lands on the Emit phase. +test fn a_compile_emit_label_attributes_to_the_emit_phase() -> Bool { + attributed_to(attribution: regen_round_phase_of_label(label: "compile.emit"), phase: Emit) +} + +// EVERY PHASE ROUND-TRIPS THROUGH ITS OWN LABEL. A phase whose label attributes to a different +// phase, or to nothing, would price one phase's time under another's name. +test fn a_every_phase_label_attributes_back_to_its_phase() -> Bool { + fold(regen_round_phases(), init: true, f: fn(acc, phase) { + acc && attributed_to(attribution: regen_round_phase_of_label(label: regen_round_phase_label(phase: phase)), phase: phase) + }) +} + +// THE DISCRIMINATING RED for attribution: a sub-mark under the emit is NOT the emit. A prefix +// match would fold compile.emit.dag.collect into the Emit line and count its time twice. +test fn w_RED_an_emit_sub_mark_is_unattributed_not_folded_into_emit() -> Bool { + is_unattributed(attribution: regen_round_phase_of_label(label: "compile.emit.dag.collect")) +} + +test fn a_wall_total_is_the_sum_of_the_marks() -> Bool { + regen_round_wall_total_ms(marks: [a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), a_mark(label: "compile.emit", wall_ms: 300000, cpu_ms: 299000)]) == 301500 +} + +// THE DISCRIMINATING RED for the CPU total: one unreadable row makes the total unreadable. A +// total that summed the readable rows would report a smaller CPU than the round spent. +test fn w_RED_one_unreadable_cpu_row_makes_the_cpu_total_unreadable() -> Bool { + let total = regen_round_cpu_total(marks: [ + a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), + TraceMark { label: "compile.emit", wall_ms: 300000, cpu: CpuUnreadable } + ]) + match total { + CpuUnreadable => true + CpuMs { ms: m } => false + } +} + +test fn a_all_readable_cpu_rows_sum() -> Bool { + match regen_round_cpu_total(marks: [a_mark(label: "a", wall_ms: 1, cpu_ms: 2), a_mark(label: "b", wall_ms: 3, cpu_ms: 4)]) { + CpuMs { ms: m } => m == 6 + CpuUnreadable => false + } +} + +test fn a_mark_line_names_the_phase_and_both_clocks() -> Bool { + regen_round_mark_line(mark: a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000)) + == "regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000" +} + +test fn a_mark_line_for_an_unknown_label_says_unattributed_and_keeps_the_label() -> Bool { + regen_round_mark_line(mark: a_mark(label: "compile.emit.dag.serialize", wall_ms: 7, cpu_ms: 7)) + == "regen-round-cost: unattributed label=compile.emit.dag.serialize wall_ms=7 cpu_ms=7" +} + +// THE RENDERED RECEIPT, BYTE FOR BYTE: provenance header, one line per mark in ledger order, the +// totals, and the changed set by identity. +test fn a_receipt_renders_provenance_marks_totals_and_changed_paths() -> Bool { + let text = regen_round_cost_render(receipt: a_receipt(marks: [ + a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), + a_mark(label: "compile.emit", wall_ms: 300000, cpu_ms: 299000) + ])) + text == concat( + "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=false seed_build_compiled_crates=0 rebuild_compiled_crates=2\n", + concat( + "regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\nregen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=299000\n", + "regen-round-cost: total wall_ms=301500 cpu_ms=308000\nregen-round-cost: changed_paths=2 [v1_rt.rs, v1_compiler_runtime_rust.rs]\n" + ) + ) +} diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index ce5622ce922..e7cc153d655 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -668,6 +668,48 @@ fn rt_hash_ops() -> String { " format!(\"\{glyph\} \{phase\} done in \{\}\", obs_human_duration(elapsed_ms))\n", "\}\n", "\n", + "/// One drained trace-ledger row: the mark's label with any `.done` suffix removed, the wall\n", + "/// elapsed the mark printed, and the process-tree CPU spent since the mark it closes.\n", + "/// Authority: `gunbc.regen_round_cost` `TraceMark`; `cpu_ms == None` is its `CpuUnreadable`.\n", + "#[derive(Clone, Debug)]\n", + "pub struct TraceLedgerRow \{\n", + " pub label: String,\n", + " pub wall_ms: u64,\n", + " pub cpu_ms: Option,\n", + "\}\n", + "\n", + "fn trace_ledger_state() -> &'static std::sync::Mutex>> \{\n", + " static LEDGER: std::sync::OnceLock>>> =\n", + " std::sync::OnceLock::new();\n", + " LEDGER.get_or_init(|| std::sync::Mutex::new(None))\n", + "\}\n", + "\n", + "/// Start recording marks. Until armed, `trace_mark` records nothing, so a program that never\n", + "/// drains the ledger never grows it.\n", + "pub fn trace_ledger_arm() \{\n", + " if let Ok(mut ledger) = trace_ledger_state().lock() \{\n", + " *ledger = Some(Vec::new());\n", + " \}\n", + "\}\n", + "\n", + "/// Take every row recorded since `trace_ledger_arm` and disarm; `None` when never armed.\n", + "pub fn trace_ledger_drain() -> Option> \{\n", + " trace_ledger_state().lock().ok().and_then(|mut ledger| ledger.take())\n", + "\}\n", + "\n", + "/// Process-tree CPU milliseconds — utime + stime + cutime + cstime from `/proc/self/stat` —\n", + "/// or `None` where that accounting is unreadable. A child's CPU lands once it is reaped, so a\n", + "/// mark that closes over a waited child carries that child's CPU; a fabricated 0 would read as\n", + "/// free, which is the direction that looks survivable.\n", + "pub fn trace_process_tree_cpu_ms() -> Option \{\n", + " let stat = std::fs::read_to_string(\"/proc/self/stat\").ok()?;\n", + " let after_comm = stat.rsplit(')').next()?;\n", + " let fields: Vec<&str> = after_comm.split_whitespace().collect();\n", + " let tick = |i: usize| fields.get(i).and_then(|v| v.parse::().ok());\n", + " let ticks = tick(11)? + tick(12)? + tick(13)? + tick(14)?;\n", + " Some(ticks * 1000 / 100)\n", + "\}\n", + "\n", "/// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep\n", "/// CostAccount (std.realization_measurement). Wired (gantt flip): projects Begin/Concluded\n", "/// on a PhaseSegment via the observation mirrors of `ci_event_line`. Segment wall on\n", @@ -688,51 +730,58 @@ fn rt_hash_ops() -> String { " use std::sync::\{Mutex, OnceLock\};\n", " use std::time::Instant;\n", " static TRACE_T0: OnceLock = OnceLock::new();\n", - " static LAST_MARK: OnceLock> = OnceLock::new();\n", - " static OPENS: OnceLock>> = OnceLock::new();\n", + " static LAST_MARK: OnceLock)>> = OnceLock::new();\n", + " static OPENS: OnceLock)>>> =\n", + " OnceLock::new();\n", " let t0 = TRACE_T0.get_or_init(Instant::now);\n", - " let last = LAST_MARK.get_or_init(|| Mutex::new(*t0));\n", + " let last = LAST_MARK.get_or_init(|| Mutex::new((*t0, trace_process_tree_cpu_ms())));\n", " let opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));\n", " let emoji = obs_phase_emoji();\n", " let now = Instant::now();\n", + " let cpu_now = trace_process_tree_cpu_ms();\n", " let _ = GANTT_CENSUS_MARKER;\n", " if let Some(phase) = label.strip_suffix(\".begin\") \{\n", " if let Ok(mut map) = opens.lock() \{\n", - " map.insert(phase.to_string(), now);\n", + " map.insert(phase.to_string(), (now, cpu_now));\n", " \}\n", " if let Ok(mut l) = last.lock() \{\n", - " *l = now;\n", + " *l = (now, cpu_now);\n", " \}\n", " eprintln!(\"\{\}\", render_phase_begin_line_mirror(phase, emoji));\n", - " \} else if let Some(phase) = label.strip_suffix(\".done\") \{\n", - " let elapsed_ms = \{\n", - " let started = opens.lock().ok().and_then(|mut m| m.remove(phase));\n", - " match started \{\n", - " Some(t) => now.saturating_duration_since(t).as_millis() as u64,\n", - " None => \{\n", - " let prev = last.lock().map(|l| *l).unwrap_or(*t0);\n", - " now.saturating_duration_since(prev).as_millis() as u64\n", - " \}\n", - " \}\n", - " \};\n", - " if let Ok(mut l) = last.lock() \{\n", - " *l = now;\n", - " \}\n", - " eprintln!(\n", - " \"\{\}\",\n", - " render_phase_concluded_line_mirror(phase, elapsed_ms, emoji)\n", - " );\n", - " \} else \{\n", - " let prev = last.lock().map(|l| *l).unwrap_or(*t0);\n", - " let elapsed_ms = now.saturating_duration_since(prev).as_millis() as u64;\n", - " if let Ok(mut l) = last.lock() \{\n", - " *l = now;\n", + " return;\n", + " \}\n", + " let (phase, opened) = match label.strip_suffix(\".done\") \{\n", + " Some(phase) => (\n", + " phase.to_string(),\n", + " opens.lock().ok().and_then(|mut m| m.remove(phase)),\n", + " ),\n", + " None => (label.clone(), None),\n", + " \};\n", + " let (since, since_cpu) = match opened \{\n", + " Some(open) => open,\n", + " None => last.lock().map(|l| *l).unwrap_or((*t0, None)),\n", + " \};\n", + " let elapsed_ms = now.saturating_duration_since(since).as_millis() as u64;\n", + " let cpu_ms = match (cpu_now, since_cpu) \{\n", + " (Some(now_ms), Some(since_ms)) => Some(now_ms.saturating_sub(since_ms)),\n", + " _ => None,\n", + " \};\n", + " if let Ok(mut l) = last.lock() \{\n", + " *l = (now, cpu_now);\n", + " \}\n", + " if let Ok(mut ledger) = trace_ledger_state().lock() \{\n", + " if let Some(rows) = ledger.as_mut() \{\n", + " rows.push(TraceLedgerRow \{\n", + " label: phase.clone(),\n", + " wall_ms: elapsed_ms,\n", + " cpu_ms,\n", + " \});\n", " \}\n", - " eprintln!(\n", - " \"\{\}\",\n", - " render_phase_concluded_line_mirror(&label, elapsed_ms, emoji)\n", - " );\n", " \}\n", + " eprintln!(\n", + " \"\{\}\",\n", + " render_phase_concluded_line_mirror(&phase, elapsed_ms, emoji)\n", + " );\n", "\}\n", "\n", "/// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash`\n", diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 4081576e50b..f2745cfe1c1 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -162,6 +162,7 @@ fn run() -> Result { let mut emit_partition_crates_mode = false; let mut emit_partition_crates_write = false; let mut required_regen_fixed_point_mode = false; + let mut regen_round_cost_mode = false; let mut regen_candidate_dir = "target/stage0-regen-candidate".to_string(); let mut regen_receipt_path = "target/stage0-regen-receipt.json".to_string(); @@ -226,6 +227,13 @@ fn run() -> Result { "--required-regen-fixed-point" => { required_regen_fixed_point_mode = true; } + // ONE PRICED REGEN ROUND: seed build, the same `--required-regen` emit, install of + // what drifted, rebuild from the installed seed, diff — every phase on two clocks, + // rendered by `gunbc.regen_round_cost`. It installs into src/v1/stage0/src, which + // is what a round IS; the receipt names the tree and every installed path. + "--regen-round-cost" => { + regen_round_cost_mode = true; + } "--regen-candidate-dir" => { i += 1; regen_candidate_dir = require_value(&args, i, "--regen-candidate-dir")?; @@ -834,6 +842,34 @@ fn run() -> Result { // outside `//:required` by construction there -- `gunbc.discovery_census` derives that // aggregate from discovered witness sites, and no fold feeds the instrument population into it. + if regen_round_cost_mode { + return match v1_compiler::cli_run::run_regen_round_cost( + ®en_candidate_dir, + ®en_receipt_path, + &source_roots, + ) { + Ok(outcome) => { + eprint!("{}", outcome.rendered); + eprintln!( + "regen-round-cost: receipt={}", + outcome.receipt_path.display() + ); + for failure in &outcome.round_failures { + eprintln!("regen-round-cost: FAIL {failure}"); + } + if outcome.round_failures.is_empty() { + Ok(ExitCode::SUCCESS) + } else { + Err(ExitCode::from(1)) + } + } + Err(e) => { + eprintln!("regen-round-cost: refused: {e}"); + Err(ExitCode::from(1)) + } + }; + } + if required_regen_mode { return match v1_compiler::cli_run::run_required_regen( ®en_candidate_dir, diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 0d2ac5f7176..5472d0808d1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -38267,6 +38267,17 @@ pub fn run_required_regen_fixed_point( required_regen_host::run_required_regen_fixed_point(receipt_rel, pass1_digest) } +pub use required_regen_host::RegenRoundCostOutcome; + +/// One priced regen round — see `required_regen_host::run_regen_round_cost`. +pub fn run_regen_round_cost( + candidate_dir_rel: &str, + receipt_rel: &str, + source_roots: &[String], +) -> Result { + required_regen_host::run_regen_round_cost(candidate_dir_rel, receipt_rel, source_roots) +} + /// The emitted generated surface, keyed by basename, off the SAME `measure_generated_surface` /// producer the regen path uses -- so the bytes a behavioural receipt compiles are the bytes /// regen compared. A second emit here would be a second producer of the candidate itself. diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index ef28fcb5bba..f2ec167622e 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -278,7 +278,13 @@ pub fn run_required_regen( let formatter = ResolvedFormatter::admit()?; let commit_sha = git_head_sha(&workspace)?; + // PHASE MARKS. Every boundary below is stamped through `v1_rt::trace_mark`, the same + // instrument compile.dag already uses for its stages, so `--regen-round-cost` reads one + // ledger for the whole round rather than a second set of clocks kept beside the first. + // The labels are `gunbc.regen_round_cost` `regen_round_phase_label`, verbatim. + v1_rt::trace_mark("regen.corpus_load.begin".to_string()); let sources = super::regen_input_sources(&workspace)?; + v1_rt::trace_mark("regen.corpus_load.done".to_string()); let authority_digest = authority_digest_from_sources(&sources)?; // PRODUCTION, THEN ADJUDICATION -- and the order is the whole repair. @@ -326,21 +332,24 @@ pub fn run_required_regen( .map_err(|e| format!("remove {}: {e}", candidate_dir.display()))?; } let fresh_src = candidate_dir.join("src"); + v1_rt::trace_mark("regen.mirror_write.begin".to_string()); write_emitted_tree(&formatter, &fresh_src, &emitted)?; copy_hand_maintained_support(&stage0_src, &fresh_src)?; + v1_rt::trace_mark("regen.mirror_write.done".to_string()); // Verified against what EMIT produced, not against what is committed. Those two populations // are equal on a clean tree and differ in precisely the case this ordering exists to serve, so // checking the committed population here would re-impose the refusal one line after the write // and fail the producer on the run that needs it. The invariant a producer owes is that its // own product landed whole. + v1_rt::trace_mark("regen.candidate_verify.begin".to_string()); verify_candidate_tree(&fresh_src, &emitted_basenames)?; + v1_rt::trace_mark("regen.candidate_verify.done".to_string()); - let (committed_basenames, sync) = match adjudicate_generated_surface( - &formatter, - &stage0_src, - &emitted, - &emitted_basenames, - )? { + v1_rt::trace_mark("regen.adjudicate.begin".to_string()); + let adjudicated = + adjudicate_generated_surface(&formatter, &stage0_src, &emitted, &emitted_basenames)?; + v1_rt::trace_mark("regen.adjudicate.done".to_string()); + let (committed_basenames, sync) = match adjudicated { GeneratedSurfaceAdjudicated::Refused { reason } => { return regen_refusal_outcome( &workspace, @@ -357,11 +366,15 @@ pub fn run_required_regen( GeneratedSurfaceAdjudicated::Measured { committed, sync } => (committed, sync), }; + v1_rt::trace_mark("regen.hand_verify.begin".to_string()); let hand = verify_hand_maintained(&formatter, &emitted, &stage0_src, &candidate_dir)?; + v1_rt::trace_mark("regen.hand_verify.done".to_string()); + v1_rt::trace_mark("regen.digest.begin".to_string()); let committed_digest = tree_digest_for_basenames(&formatter, &stage0_src, &committed_basenames, "committed")?; let candidate_digest = tree_digest_from_map(&formatter, &emitted, &committed_basenames)?; + v1_rt::trace_mark("regen.digest.done".to_string()); let first_generation_equal = sync.matches && hand.unverifiable.is_empty(); let changed_paths = sync.drifted_paths.clone(); @@ -769,7 +782,12 @@ fn is_declared_divergent(file_name: &str) -> bool { } fn compile_stage0(workspace: &Path) -> Result, String> { + // A SECOND READ OF THE SAME CLOSURE. `run_required_regen` already read these sources for + // the authority digest; this is the same walk again, and it is stamped under its own label + // so the receipt prices the duplication instead of folding it into the frontend. + v1_rt::trace_mark("regen.corpus_reload.begin".to_string()); let sources = super::regen_input_sources(workspace)?; + v1_rt::trace_mark("regen.corpus_reload.done".to_string()); let source_files: Vec> = sources .into_iter() .map(|(path, content)| Rc::new(SourceFile { path, content })) @@ -2110,3 +2128,326 @@ mod tests { ); } } + +// --------------------------------------------------------------------------------------------- +// ONE REGEN ROUND, PRICED — `claim_executor --regen-round-cost`. +// +// The round is the convergence recipe `gunbc.generated_artifact_merge_driver` +// `generated_artifact_merge_driver_repair_steps` prints: build the seed, emit from it, install +// what drifted, rebuild from the installed seed. This driver runs exactly that sequence ONCE, +// through the same `run_required_regen` the required phase runs (never a second emit path), +// with `v1_rt::trace_mark` armed so every phase boundary lands in one ledger, and renders the +// receipt through `gunbc.regen_round_cost` `regen_round_cost_render` via the interpreter — there +// is no Rust copy of the receipt format to drift from the model. +// +// IT MUTATES THE TREE, AND SAYS SO. Installing the drifted mirrors into src/v1/stage0/src is +// the step an author performs by hand today, and the round's rebuild is only a measurement of +// "rebuild from the installed seed" if the seed was actually installed. A read-only variant +// would price a round nobody runs. The receipt names the tree (HEAD sha, dirty flag) and lists +// every installed path by identity, so what the round changed is on the record. +// +// THE SEED THAT EMITS IS THE SEED THAT WAS BUILT, OR THE RUN REFUSES. The emit runs in THIS +// process, whose binary is the product of the seed build only if that build was a no-op on the +// running executable. The executable's bytes are hashed before and after the build; a changed +// hash means the round measured a stale seed's emit, and the only honest answer is to stop and +// say so rather than report a candidate the built seed never produced. +// --------------------------------------------------------------------------------------------- + +pub struct RegenRoundCostOutcome { + /// The rendered receipt, already printed by the caller's contract to stderr. + pub rendered: String, + /// Where the same bytes were written, so a remote run can bring them back by path. + pub receipt_path: PathBuf, + /// Anything that stopped the round short of install/rebuild/diff, or that the regen + /// reported as a failure. Non-empty means the round is not a clean round. + pub round_failures: Vec, +} + +pub const REGEN_ROUND_COST_RECEIPT_REL: &str = "target/stage0-regen-round-cost.txt"; +const REGEN_ROUND_COST_PRODUCER: &str = "claim_executor --regen-round-cost"; +const REGEN_ROUND_COST_ENTRY_UNDER_ROOT: &str = "gunbc/regen_round_cost.dag"; + +struct CargoBuildObservation { + compiled_crates: u64, +} + +fn seed_cargo_build(workspace: &Path, label: &str) -> Result { + v1_rt::trace_mark(format!("{label}.begin")); + let output = Command::new("cargo") + .args(["build", "--release", "--bin", "claim_executor"]) + .current_dir(workspace) + .output() + .map_err(|e| format!("spawn cargo build ({label}): {e}"))?; + v1_rt::trace_mark(format!("{label}.done")); + let stderr = String::from_utf8_lossy(&output.stderr); + if !output.status.success() { + let tail: Vec<&str> = stderr.lines().rev().take(40).collect::>(); + let tail: Vec<&str> = tail.into_iter().rev().collect(); + return Err(format!( + "refusal: cargo build ({label}) failed with {} — last lines:\n{}", + output.status, + tail.join("\n") + )); + } + let compiled_crates = stderr + .lines() + .filter(|l| l.trim_start().starts_with("Compiling ")) + .count() as u64; + Ok(CargoBuildObservation { compiled_crates }) +} + +fn current_exe_digest() -> Result { + let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?; + let bytes = fs::read(&exe).map_err(|e| format!("read {}: {e}", exe.display()))?; + Ok(v1_rt::bytes_identity_hash(&bytes)) +} + +fn git_tree_dirty(workspace: &Path) -> Result { + let output = Command::new("git") + .args(["status", "--porcelain", "--untracked-files=no"]) + .current_dir(workspace) + .output() + .map_err(|e| format!("git status: {e}"))?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).to_string()); + } + Ok(!output.stdout.is_empty()) +} + +fn git_changed_stage0_paths(workspace: &Path) -> Result, String> { + let output = Command::new("git") + .args(["diff", "--name-only", "--", "src/v1/stage0/src"]) + .current_dir(workspace) + .output() + .map_err(|e| format!("git diff --name-only: {e}"))?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).to_string()); + } + Ok(String::from_utf8_lossy(&output.stdout) + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect()) +} + +fn host_name() -> String { + // `/proc/sys/kernel/hostname` first (no libc buffer sizing), then the POSIX call; a host + // that answers neither is reported as unreadable rather than as an empty name. + if let Ok(name) = fs::read_to_string("/proc/sys/kernel/hostname") { + let trimmed = name.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + let mut buf = [0u8; 256]; + // SAFETY: `buf` is a live, fully-owned buffer; `gethostname` writes at most `buf.len()` + // bytes into it and nothing else. + let rc = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf.len()) }; + if rc == 0 { + let end = buf.iter().position(|b| *b == 0).unwrap_or(buf.len()); + let name = String::from_utf8_lossy(&buf[..end]).to_string(); + if !name.is_empty() { + return name; + } + } + "unreadable".to_string() +} + +/// Install the candidate's drifted mirrors into the committed seed — the manual step of the +/// recipe, performed from the SAME candidate tree the regen just wrote and judged. +fn install_candidate_paths( + candidate_src: &Path, + stage0_src: &Path, + basenames: &[String], +) -> Result<(), String> { + for basename in basenames { + let from = candidate_src.join(basename); + let to = stage0_src.join(basename); + fs::copy(&from, &to) + .map_err(|e| format!("install {} -> {}: {e}", from.display(), to.display()))?; + } + Ok(()) +} + +fn round_cost_entry(source_roots: &[String]) -> Result { + source_roots + .iter() + .map(|root| Path::new(root).join(REGEN_ROUND_COST_ENTRY_UNDER_ROOT)) + .find(|candidate| candidate.is_file()) + .map(|found| found.to_string_lossy().into_owned()) + .ok_or_else(|| { + format!( + "refusal: {REGEN_ROUND_COST_ENTRY_UNDER_ROOT} is not under any declared source \ + root {source_roots:?}, so the receipt has no renderer" + ) + }) +} + +fn render_round_cost_receipt( + source_roots: &[String], + host: &str, + tree: &str, + tree_dirty: bool, + seed_build_compiled_crates: u64, + rebuild_compiled_crates: u64, + marks: &[v1_rt::TraceLedgerRow], + changed_paths: &[String], +) -> Result { + use crate::v1_interpreter::{self, str_value, ExecutionMode, Value}; + let entry = round_cost_entry(source_roots)?; + let index = super::process_shared_index(source_roots); + let (graph, indices) = super::resolve_entry_with_index_for_discovery_corpus(&index, &entry) + .map_err(|e| { + format!("refusal: {entry} did not resolve, so the receipt cannot render: {e}") + })?; + let ctx = super::make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let cpu_value = |cpu_ms: Option| match cpu_ms { + Some(ms) => Value::Variant { + type_name: ctx.sym("CpuReading"), + variant_name: ctx.sym("CpuMs"), + fields: Rc::new(vec![(ctx.sym("ms"), Value::Int(ms as i64))]), + }, + None => Value::Variant { + type_name: ctx.sym("CpuReading"), + variant_name: ctx.sym("CpuUnreadable"), + fields: Rc::new(vec![]), + }, + }; + let mark_values: Vec = marks + .iter() + .map(|row| Value::Record { + type_name: ctx.sym("TraceMark"), + fields: Rc::new(vec![ + (ctx.sym("label"), str_value(row.label.clone())), + (ctx.sym("wall_ms"), Value::Int(row.wall_ms as i64)), + (ctx.sym("cpu"), cpu_value(row.cpu_ms)), + ]), + }) + .collect(); + let path_values: Vec = changed_paths.iter().map(str_value).collect(); + let receipt = Value::Record { + type_name: ctx.sym("RegenRoundCostReceipt"), + fields: Rc::new(vec![ + (ctx.sym("producer"), str_value(REGEN_ROUND_COST_PRODUCER)), + (ctx.sym("host"), str_value(host)), + (ctx.sym("tree"), str_value(tree)), + (ctx.sym("tree_dirty"), Value::Bool(tree_dirty)), + ( + ctx.sym("seed_build_compiled_crates"), + Value::Int(seed_build_compiled_crates as i64), + ), + ( + ctx.sym("rebuild_compiled_crates"), + Value::Int(rebuild_compiled_crates as i64), + ), + (ctx.sym("marks"), Value::List(Rc::new(mark_values.into()))), + ( + ctx.sym("changed_paths"), + Value::List(Rc::new(path_values.into())), + ), + ]), + }; + let args = vec![(Some("receipt".to_string()), receipt)]; + let rendered = v1_interpreter::with_active_context(&ctx, || { + v1_interpreter::run_in_context_with_args(&ctx, "regen_round_cost_render", &args, false) + }) + .map_err(|e| format!("refusal: regen_round_cost_render did not render: {e}"))?; + match rendered { + Value::Str(s) => Ok(s.to_string()), + other => Err(format!( + "refusal: regen_round_cost_render returned {} where a String was expected", + other.type_label_public() + )), + } +} + +pub fn run_regen_round_cost( + candidate_dir_rel: &str, + receipt_rel: &str, + source_roots: &[String], +) -> Result { + let workspace = workspace_root(); + let stage0_src = workspace.join("src/v1/stage0/src"); + let candidate_src = workspace.join(candidate_dir_rel).join("src"); + let host = host_name(); + let tree = git_head_sha(&workspace)?; + let tree_dirty = git_tree_dirty(&workspace)?; + let exe_before = current_exe_digest()?; + + v1_rt::trace_ledger_arm(); + let seed_build = seed_cargo_build(&workspace, "round.seed_build")?; + let exe_after = current_exe_digest()?; + if exe_before != exe_after { + return Err(format!( + "refusal: the seed build replaced the running executable ({exe_before} -> \ + {exe_after}), so an emit from this process would measure a seed the build did not \ + produce. Re-run {REGEN_ROUND_COST_PRODUCER} so the emitting seed is the built one." + )); + } + + let mut round_failures = Vec::new(); + let regen = run_required_regen(candidate_dir_rel, receipt_rel)?; + for failure in ®en.failures { + round_failures.push(format!("regen: {failure}")); + } + // The install set is the regen's own drift answer — every generated path whose committed + // bytes differ from the candidate's. A refused regen has no such answer and the round stops + // at the emit: no install, no rebuild, no diff, and the receipt says which phases ran. + let drifted: Option> = match ®en.receipt { + RegenReceipt::FirstGeneration { changed_paths, .. } => Some(changed_paths.clone()), + RegenReceipt::Refused { reason, .. } => { + round_failures.push(format!("regen refused, round stopped at emit: {reason}")); + None + } + RegenReceipt::FixedPoint { .. } => { + round_failures.push( + "regen returned a fixed-point receipt from a first-generation pass".to_string(), + ); + None + } + }; + + let mut rebuild_compiled_crates = 0; + let mut changed_paths: Vec = Vec::new(); + if let Some(drifted) = drifted { + v1_rt::trace_mark("round.install.begin".to_string()); + install_candidate_paths(&candidate_src, &stage0_src, &drifted)?; + v1_rt::trace_mark("round.install.done".to_string()); + let rebuild = seed_cargo_build(&workspace, "round.rebuild_from_installed")?; + rebuild_compiled_crates = rebuild.compiled_crates; + v1_rt::trace_mark("round.diff.begin".to_string()); + changed_paths = git_changed_stage0_paths(&workspace)?; + v1_rt::trace_mark("round.diff.done".to_string()); + } + + // The runtime's `Vec` is the persistent vector its emitted programs use; the receipt + // renderer takes a slice, so the rows are collected once here. + let marks: std::vec::Vec = v1_rt::trace_ledger_drain() + .ok_or_else(|| { + "refusal: the trace ledger was not armed, so no phase was recorded".to_string() + })? + .iter() + .cloned() + .collect(); + let rendered = render_round_cost_receipt( + source_roots, + &host, + &tree, + tree_dirty, + seed_build.compiled_crates, + rebuild_compiled_crates, + &marks, + &changed_paths, + )?; + let receipt_path = workspace.join(REGEN_ROUND_COST_RECEIPT_REL); + if let Some(parent) = receipt_path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; + } + fs::write(&receipt_path, &rendered) + .map_err(|e| format!("write {}: {e}", receipt_path.display()))?; + Ok(RegenRoundCostOutcome { + rendered, + receipt_path, + round_failures, + }) +} diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index 30f3c637d47..bc3a4021888 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1006,6 +1006,51 @@ pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: b format!("{glyph} {phase} done in {}", obs_human_duration(elapsed_ms)) } +/// One drained trace-ledger row: the mark's label with any `.done` suffix removed, the wall +/// elapsed the mark printed, and the process-tree CPU spent since the mark it closes. +/// Authority: `gunbc.regen_round_cost` `TraceMark`; `cpu_ms == None` is its `CpuUnreadable`. +#[derive(Clone, Debug)] +pub struct TraceLedgerRow { + pub label: String, + pub wall_ms: u64, + pub cpu_ms: Option, +} + +fn trace_ledger_state() -> &'static std::sync::Mutex>> { + static LEDGER: std::sync::OnceLock>>> = + std::sync::OnceLock::new(); + LEDGER.get_or_init(|| std::sync::Mutex::new(None)) +} + +/// Start recording marks. Until armed, `trace_mark` records nothing, so a program that never +/// drains the ledger never grows it. +pub fn trace_ledger_arm() { + if let Ok(mut ledger) = trace_ledger_state().lock() { + *ledger = Some(Vec::new()); + } +} + +/// Take every row recorded since `trace_ledger_arm` and disarm; `None` when never armed. +pub fn trace_ledger_drain() -> Option> { + trace_ledger_state() + .lock() + .ok() + .and_then(|mut ledger| ledger.take()) +} + +/// Process-tree CPU milliseconds — utime + stime + cutime + cstime from `/proc/self/stat` — +/// or `None` where that accounting is unreadable. A child's CPU lands once it is reaped, so a +/// mark that closes over a waited child carries that child's CPU; a fabricated 0 would read as +/// free, which is the direction that looks survivable. +pub fn trace_process_tree_cpu_ms() -> Option { + let stat = std::fs::read_to_string("/proc/self/stat").ok()?; + let after_comm = stat.rsplit(')').next()?; + let fields: Vec<&str> = after_comm.split_whitespace().collect(); + let tick = |i: usize| fields.get(i).and_then(|v| v.parse::().ok()); + let ticks = tick(11)? + tick(12)? + tick(13)? + tick(14)?; + Some(ticks * 1000 / 100) +} + /// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep /// CostAccount (std.realization_measurement). Wired (gantt flip): projects Begin/Concluded /// on a PhaseSegment via the observation mirrors of `ci_event_line`. Segment wall on @@ -1026,51 +1071,58 @@ pub fn trace_mark(label: String) { use std::sync::{Mutex, OnceLock}; use std::time::Instant; static TRACE_T0: OnceLock = OnceLock::new(); - static LAST_MARK: OnceLock> = OnceLock::new(); - static OPENS: OnceLock>> = OnceLock::new(); + static LAST_MARK: OnceLock)>> = OnceLock::new(); + static OPENS: OnceLock)>>> = + OnceLock::new(); let t0 = TRACE_T0.get_or_init(Instant::now); - let last = LAST_MARK.get_or_init(|| Mutex::new(*t0)); + let last = LAST_MARK.get_or_init(|| Mutex::new((*t0, trace_process_tree_cpu_ms()))); let opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new())); let emoji = obs_phase_emoji(); let now = Instant::now(); + let cpu_now = trace_process_tree_cpu_ms(); let _ = GANTT_CENSUS_MARKER; if let Some(phase) = label.strip_suffix(".begin") { if let Ok(mut map) = opens.lock() { - map.insert(phase.to_string(), now); + map.insert(phase.to_string(), (now, cpu_now)); } if let Ok(mut l) = last.lock() { - *l = now; + *l = (now, cpu_now); } eprintln!("{}", render_phase_begin_line_mirror(phase, emoji)); - } else if let Some(phase) = label.strip_suffix(".done") { - let elapsed_ms = { - let started = opens.lock().ok().and_then(|mut m| m.remove(phase)); - match started { - Some(t) => now.saturating_duration_since(t).as_millis() as u64, - None => { - let prev = last.lock().map(|l| *l).unwrap_or(*t0); - now.saturating_duration_since(prev).as_millis() as u64 - } - } - }; - if let Ok(mut l) = last.lock() { - *l = now; - } - eprintln!( - "{}", - render_phase_concluded_line_mirror(phase, elapsed_ms, emoji) - ); - } else { - let prev = last.lock().map(|l| *l).unwrap_or(*t0); - let elapsed_ms = now.saturating_duration_since(prev).as_millis() as u64; - if let Ok(mut l) = last.lock() { - *l = now; + return; + } + let (phase, opened) = match label.strip_suffix(".done") { + Some(phase) => ( + phase.to_string(), + opens.lock().ok().and_then(|mut m| m.remove(phase)), + ), + None => (label.clone(), None), + }; + let (since, since_cpu) = match opened { + Some(open) => open, + None => last.lock().map(|l| *l).unwrap_or((*t0, None)), + }; + let elapsed_ms = now.saturating_duration_since(since).as_millis() as u64; + let cpu_ms = match (cpu_now, since_cpu) { + (Some(now_ms), Some(since_ms)) => Some(now_ms.saturating_sub(since_ms)), + _ => None, + }; + if let Ok(mut l) = last.lock() { + *l = (now, cpu_now); + } + if let Ok(mut ledger) = trace_ledger_state().lock() { + if let Some(rows) = ledger.as_mut() { + rows.push(TraceLedgerRow { + label: phase.clone(), + wall_ms: elapsed_ms, + cpu_ms, + }); } - eprintln!( - "{}", - render_phase_concluded_line_mirror(&label, elapsed_ms, emoji) - ); } + eprintln!( + "{}", + render_phase_concluded_line_mirror(&phase, elapsed_ms, emoji) + ); } /// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash` From 79aff1910eea1273abca15eefe46dc1e82af1eef Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 08:42:50 +0000 Subject: [PATCH 02/11] Regen round cost: seed-to-model lockstep unit tests for the receipt renderer and the process-tree clock Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- src/v1/stage0/src/required_regen_host.rs | 62 ++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index f2ec167622e..6081d74d592 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2451,3 +2451,65 @@ pub fn run_regen_round_cost( round_failures, }) } + +#[cfg(test)] +mod regen_round_cost_tests { + use super::*; + + /// THE SEED-TO-MODEL LOCKSTEP the .dag witness says it cannot hold: the host builds the + /// receipt Value with these field and variant names, and the model's renderer either + /// accepts them or refuses. A renamed field on either side reds here, not in a + /// forty-minute round. The expected text is the same fixture the .dag witness asserts. + #[test] + fn host_built_receipt_renders_through_the_model() { + let roots = vec![workspace_root().join("dag").to_string_lossy().into_owned()]; + let marks = vec![ + v1_rt::TraceLedgerRow { + label: "round.seed_build".to_string(), + wall_ms: 1500, + cpu_ms: Some(9000), + }, + v1_rt::TraceLedgerRow { + label: "compile.emit".to_string(), + wall_ms: 300000, + cpu_ms: None, + }, + ]; + let rendered = render_round_cost_receipt( + &roots, + "srv1", + "2a11b317d2caf3c37d1d38a4421e8e0c06188925", + true, + 0, + 2, + &marks, + &["v1_rt.rs".to_string()], + ) + .expect("the model renders a host-built receipt"); + assert_eq!( + rendered, + "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 \ + tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=true \ + seed_build_compiled_crates=0 rebuild_compiled_crates=2\n\ + regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\n\ + regen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=na\n\ + regen-round-cost: total wall_ms=301500 cpu_ms=na\n\ + regen-round-cost: changed_paths=1 [v1_rt.rs]\n" + ); + } + + /// The process-tree clock reads on this host, and it is monotone across work. + #[test] + fn process_tree_cpu_reads_and_does_not_go_backwards() { + let before = v1_rt::trace_process_tree_cpu_ms(); + let mut sink = 0u64; + for i in 0..2_000_000u64 { + sink = sink.wrapping_mul(31).wrapping_add(i); + } + assert_ne!(sink, 1); + let after = v1_rt::trace_process_tree_cpu_ms(); + if let (Some(b), Some(a)) = (before, after) { + assert!(a >= b, "cpu went backwards: {b} -> {a}"); + } + } +} From 0fe2c517d2dac1acf83e307a8d429817d08815b7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 08:50:05 +0000 Subject: [PATCH 03/11] regen-round-cost: digest the seed binary at its on-disk path so a replaced executable reaches the typed refusal Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- src/v1/stage0/src/required_regen_host.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 6081d74d592..6e7367df049 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2196,9 +2196,15 @@ fn seed_cargo_build(workspace: &Path, label: &str) -> Result (deleted)`; the digest is +/// taken from the path itself so the comparison is "what is installed there now" against "what +/// was installed there before the build", which is the question the refusal asks. fn current_exe_digest() -> Result { let exe = std::env::current_exe().map_err(|e| format!("current_exe: {e}"))?; - let bytes = fs::read(&exe).map_err(|e| format!("read {}: {e}", exe.display()))?; + let shown = exe.to_string_lossy().into_owned(); + let on_disk = PathBuf::from(shown.strip_suffix(" (deleted)").unwrap_or(&shown)); + let bytes = fs::read(&on_disk).map_err(|e| format!("read {}: {e}", on_disk.display()))?; Ok(v1_rt::bytes_identity_hash(&bytes)) } From 75bc50072fc79f19512c617da61e4fac13dae86e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 09:07:02 +0000 Subject: [PATCH 04/11] regen-round-cost: a failed rebuild-from-installed is a round failure carried on the receipt, not a refusal of the measurement; regen the runtime template mirror (v1_compiler_runtime_rust.rs) from round 1 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- src/v1/stage0/src/required_regen_host.rs | 13 +++++++++++-- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 6e7367df049..c7d1342f173 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2419,8 +2419,17 @@ pub fn run_regen_round_cost( v1_rt::trace_mark("round.install.begin".to_string()); install_candidate_paths(&candidate_src, &stage0_src, &drifted)?; v1_rt::trace_mark("round.install.done".to_string()); - let rebuild = seed_cargo_build(&workspace, "round.rebuild_from_installed")?; - rebuild_compiled_crates = rebuild.compiled_crates; + // A REBUILD THAT FAILS IS A MEASURED FACT ABOUT THE ROUND, NOT A REFUSAL OF THE + // MEASUREMENT: every phase before it was priced, and the receipt is the only record of + // that. Measured on 2026-08-30 (srv1 and BuildBuddy, same tree): the emitting seed was + // built with the OLD runtime template baked into its emitter mirror, so its emit + // installed the old `v1_rt.rs` over the hand-synced one and the rebuild refused -- + // exactly the two-round bootstrap the merge-driver recipe's step 3 exists for. The + // receipt still renders; the failure is carried beside it and fails the exit code. + match seed_cargo_build(&workspace, "round.rebuild_from_installed") { + Ok(rebuild) => rebuild_compiled_crates = rebuild.compiled_crates, + Err(failure) => round_failures.push(format!("rebuild_from_installed: {failure}")), + } v1_rt::trace_mark("round.diff.begin".to_string()); changed_paths = git_changed_stage0_paths(&workspace)?; v1_rt::trace_mark("round.diff.done".to_string()); diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index a6e3c928605..ea61bb56130 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -94,7 +94,7 @@ pub fn rt_filesystem() -> String { } pub fn rt_hash_ops() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;\n".to_string(), "const FNV1A64_PRIME: u64 = 0x100000001b3;\n".to_string()), "\n".to_string()), "fn fnv1a64(bytes: &[u8]) -> u64 {\n".to_string()), " let mut hash = FNV1A64_OFFSET;\n".to_string()), " for b in bytes {\n".to_string()), " hash ^= *b as u64;\n".to_string()), " hash = hash.wrapping_mul(FNV1A64_PRIME);\n".to_string()), " }\n".to_string()), " hash\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub type Hash = String;\n".to_string()), "\n".to_string()), "const HASH_DIGEST_LEN: usize = 16;\n".to_string()), "\n".to_string()), "pub fn is_hash_digest(s: &str) -> bool {\n".to_string()), " s.len() == HASH_DIGEST_LEN && s.bytes().all(|b| b.is_ascii_hexdigit())\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn expect_hash_digest(s: &str, arg: &str) {\n".to_string()), " if !is_hash_digest(s) {\n".to_string()), " panic!(\"{} must be a 16-char hex Hash digest\", arg);\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the\n".to_string()), "/// raw gantt key=value shape (process-absolute millis and rss-mib fields) dissolves into the observation projection.\n".to_string()), "#[allow(dead_code)]\n".to_string()), "pub const GANTT_CENSUS_MARKER: &str = \"[gantt]\";\n".to_string()), "\n".to_string()), "/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds`.\n".to_string()), "const OBS_MINUTE_SWITCH_SECONDS: u64 = 90;\n".to_string()), "\n".to_string()), "pub fn obs_human_duration(ms: u64) -> String {\n".to_string()), " if ms < 1_000 {\n".to_string()), " format!(\"{ms}ms\")\n".to_string()), " } else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 {\n".to_string()), " format!(\"{} seconds\", ms / 1_000)\n".to_string()), " } else {\n".to_string()), " format!(\"{} minutes\", ms / 60_000)\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Mirror of `gunbc.observation_ci_render.ci_human_elapsed`.\n".to_string()), "pub fn obs_human_elapsed(wall_nanos: u128) -> String {\n".to_string()), " if wall_nanos < 1_000_000 {\n".to_string()), " format!(\"{wall_nanos}ns\")\n".to_string()), " } else {\n".to_string()), " obs_human_duration((wall_nanos / 1_000_000) as u64)\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn obs_phase_emoji() -> bool {\n".to_string()), " std::env::var(\"GITHUB_ACTIONS\").as_deref() == Ok(\"true\")\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_begin_line` —\n".to_string()), "/// `ci_event_line(Begin) ∘ ci_render_line`. Justified divergence from the\n".to_string()), "/// interpreter seed boundary used by floor prelude marks: `trace_mark` runs\n".to_string()), "/// inside compile itself (hand-synced path and interpreter intrinsic), and\n".to_string()), "/// calling back into the interpreter to render would recurse. Proven\n".to_string()), "/// byte-equal to the `.dag` oracle by the seed RED.\n".to_string()), "pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String {\n".to_string()), " let glyph = if emoji { \"🔄\" } else { \"◐\" };\n".to_string()), " format!(\"{glyph} started {phase}\")\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_concluded_line` —\n".to_string()), "/// `ci_event_line(Concluded{Done}) ∘ ci_render_line`. Same justified-divergence\n".to_string()), "/// note as `render_phase_begin_line_mirror`.\n".to_string()), "pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String {\n".to_string()), " let glyph = if emoji { \"✅\" } else { \"✓\" };\n".to_string()), " format!(\"{glyph} {phase} done in {}\", obs_human_duration(elapsed_ms))\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep\n".to_string()), "/// CostAccount (std.realization_measurement). Wired (gantt flip): projects Begin/Concluded\n".to_string()), "/// on a PhaseSegment via the observation mirrors of `ci_event_line`. Segment wall on\n".to_string()), "/// Concluded (matched `.begin`→`.done`, else delta since last mark) — never the old\n".to_string()), "/// process-absolute millis field. RSS stays on heartbeat/measurement (`ci_event_line` does\n".to_string()), "/// not project `event.rss` — explicit divergence from the deleted rss-mib field).\n".to_string()), "///\n".to_string()), "/// **Dissolution trigger (DESIGN §6):** delete this fn, the `trace_mark` registry row in\n".to_string()), "/// `04_method.dag` (+ hand-synced twin `v1_compiler_infer_method.rs`), the nine\n".to_string()), "/// `trace_mark(...)` marks in `compile.dag` (+ hand-synced `v1_compiler_compile.rs`), and\n".to_string()), "/// the interpreter arm in `v1_interpreter.rs` when realization_measurement_loop **Phase 0**\n".to_string()), "/// (`docs/plans/realization-measurement-loop.md`) lands a `.dag` `PerformanceReceipt`\n".to_string()), "/// per-stage carrier that a floor witness consumes by execution (the same retirement event\n".to_string()), "/// as `phase_profile.rs` / `GUNBC_FLOOR_GANTT`, per `ci-floor-fractal-gantt (plan doc deleted 2026-08-28)`\n".to_string()), "/// § dissolution). Receipt = that witness green with these marks deleted and stage walls\n".to_string()), "/// still attributable from the model path.\n".to_string()), "pub fn trace_mark(label: String) {\n".to_string()), " use std::sync::{Mutex, OnceLock};\n".to_string()), " use std::time::Instant;\n".to_string()), " static TRACE_T0: OnceLock = OnceLock::new();\n".to_string()), " static LAST_MARK: OnceLock> = OnceLock::new();\n".to_string()), " static OPENS: OnceLock>> = OnceLock::new();\n".to_string()), " let t0 = TRACE_T0.get_or_init(Instant::now);\n".to_string()), " let last = LAST_MARK.get_or_init(|| Mutex::new(*t0));\n".to_string()), " let opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));\n".to_string()), " let emoji = obs_phase_emoji();\n".to_string()), " let now = Instant::now();\n".to_string()), " let _ = GANTT_CENSUS_MARKER;\n".to_string()), " if let Some(phase) = label.strip_suffix(\".begin\") {\n".to_string()), " if let Ok(mut map) = opens.lock() {\n".to_string()), " map.insert(phase.to_string(), now);\n".to_string()), " }\n".to_string()), " if let Ok(mut l) = last.lock() {\n".to_string()), " *l = now;\n".to_string()), " }\n".to_string()), " eprintln!(\"{}\", render_phase_begin_line_mirror(phase, emoji));\n".to_string()), " } else if let Some(phase) = label.strip_suffix(\".done\") {\n".to_string()), " let elapsed_ms = {\n".to_string()), " let started = opens.lock().ok().and_then(|mut m| m.remove(phase));\n".to_string()), " match started {\n".to_string()), " Some(t) => now.saturating_duration_since(t).as_millis() as u64,\n".to_string()), " None => {\n".to_string()), " let prev = last.lock().map(|l| *l).unwrap_or(*t0);\n".to_string()), " now.saturating_duration_since(prev).as_millis() as u64\n".to_string()), " }\n".to_string()), " }\n".to_string()), " };\n".to_string()), " if let Ok(mut l) = last.lock() {\n".to_string()), " *l = now;\n".to_string()), " }\n".to_string()), " eprintln!(\n".to_string()), " \"{}\",\n".to_string()), " render_phase_concluded_line_mirror(phase, elapsed_ms, emoji)\n".to_string()), " );\n".to_string()), " } else {\n".to_string()), " let prev = last.lock().map(|l| *l).unwrap_or(*t0);\n".to_string()), " let elapsed_ms = now.saturating_duration_since(prev).as_millis() as u64;\n".to_string()), " if let Ok(mut l) = last.lock() {\n".to_string()), " *l = now;\n".to_string()), " }\n".to_string()), " eprintln!(\n".to_string()), " \"{}\",\n".to_string()), " render_phase_concluded_line_mirror(&label, elapsed_ms, emoji)\n".to_string()), " );\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash`\n".to_string()), "/// is the `String` projection of this. Use this directly for arbitrary binary content\n".to_string()), "/// (e.g. an executable or serialized payload): routing bytes through `String`/\n".to_string()), "/// `from_utf8_lossy` first collapses every invalid UTF-8 sequence to U+FFFD, so distinct\n".to_string()), "/// byte sequences would hash equal — a §5 silent-collision fail-open for content-addressing.\n".to_string()), "pub fn bytes_identity_hash(bytes: &[u8]) -> Hash {\n".to_string()), " format!(\"{:016x}\", fnv1a64(bytes))\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub fn atom_identity_hash(s: String) -> Hash {\n".to_string()), " bytes_identity_hash(s.as_bytes())\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub fn hash_combine(a: Hash, b: Hash) -> Hash {\n".to_string()), " expect_hash_digest(&a, \"a\");\n".to_string()), " expect_hash_digest(&b, \"b\");\n".to_string()), " let mut bytes = a.into_bytes();\n".to_string()), " bytes.push(0);\n".to_string()), " bytes.extend_from_slice(b.as_bytes());\n".to_string()), " format!(\"{:016x}\", fnv1a64(&bytes))\n".to_string()), "}\n".to_string()), "\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;\n".to_string(), "const FNV1A64_PRIME: u64 = 0x100000001b3;\n".to_string()), "\n".to_string()), "fn fnv1a64(bytes: &[u8]) -> u64 {\n".to_string()), " let mut hash = FNV1A64_OFFSET;\n".to_string()), " for b in bytes {\n".to_string()), " hash ^= *b as u64;\n".to_string()), " hash = hash.wrapping_mul(FNV1A64_PRIME);\n".to_string()), " }\n".to_string()), " hash\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub type Hash = String;\n".to_string()), "\n".to_string()), "const HASH_DIGEST_LEN: usize = 16;\n".to_string()), "\n".to_string()), "pub fn is_hash_digest(s: &str) -> bool {\n".to_string()), " s.len() == HASH_DIGEST_LEN && s.bytes().all(|b| b.is_ascii_hexdigit())\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn expect_hash_digest(s: &str, arg: &str) {\n".to_string()), " if !is_hash_digest(s) {\n".to_string()), " panic!(\"{} must be a 16-char hex Hash digest\", arg);\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the\n".to_string()), "/// raw gantt key=value shape (process-absolute millis and rss-mib fields) dissolves into the observation projection.\n".to_string()), "#[allow(dead_code)]\n".to_string()), "pub const GANTT_CENSUS_MARKER: &str = \"[gantt]\";\n".to_string()), "\n".to_string()), "/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds`.\n".to_string()), "const OBS_MINUTE_SWITCH_SECONDS: u64 = 90;\n".to_string()), "\n".to_string()), "pub fn obs_human_duration(ms: u64) -> String {\n".to_string()), " if ms < 1_000 {\n".to_string()), " format!(\"{ms}ms\")\n".to_string()), " } else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 {\n".to_string()), " format!(\"{} seconds\", ms / 1_000)\n".to_string()), " } else {\n".to_string()), " format!(\"{} minutes\", ms / 60_000)\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Mirror of `gunbc.observation_ci_render.ci_human_elapsed`.\n".to_string()), "pub fn obs_human_elapsed(wall_nanos: u128) -> String {\n".to_string()), " if wall_nanos < 1_000_000 {\n".to_string()), " format!(\"{wall_nanos}ns\")\n".to_string()), " } else {\n".to_string()), " obs_human_duration((wall_nanos / 1_000_000) as u64)\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn obs_phase_emoji() -> bool {\n".to_string()), " std::env::var(\"GITHUB_ACTIONS\").as_deref() == Ok(\"true\")\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_begin_line` —\n".to_string()), "/// `ci_event_line(Begin) ∘ ci_render_line`. Justified divergence from the\n".to_string()), "/// interpreter seed boundary used by floor prelude marks: `trace_mark` runs\n".to_string()), "/// inside compile itself (hand-synced path and interpreter intrinsic), and\n".to_string()), "/// calling back into the interpreter to render would recurse. Proven\n".to_string()), "/// byte-equal to the `.dag` oracle by the seed RED.\n".to_string()), "pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String {\n".to_string()), " let glyph = if emoji { \"🔄\" } else { \"◐\" };\n".to_string()), " format!(\"{glyph} started {phase}\")\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_concluded_line` —\n".to_string()), "/// `ci_event_line(Concluded{Done}) ∘ ci_render_line`. Same justified-divergence\n".to_string()), "/// note as `render_phase_begin_line_mirror`.\n".to_string()), "pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String {\n".to_string()), " let glyph = if emoji { \"✅\" } else { \"✓\" };\n".to_string()), " format!(\"{glyph} {phase} done in {}\", obs_human_duration(elapsed_ms))\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// One drained trace-ledger row: the mark's label with any `.done` suffix removed, the wall\n".to_string()), "/// elapsed the mark printed, and the process-tree CPU spent since the mark it closes.\n".to_string()), "/// Authority: `gunbc.regen_round_cost` `TraceMark`; `cpu_ms == None` is its `CpuUnreadable`.\n".to_string()), "#[derive(Clone, Debug)]\n".to_string()), "pub struct TraceLedgerRow {\n".to_string()), " pub label: String,\n".to_string()), " pub wall_ms: u64,\n".to_string()), " pub cpu_ms: Option,\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn trace_ledger_state() -> &'static std::sync::Mutex>> {\n".to_string()), " static LEDGER: std::sync::OnceLock>>> =\n".to_string()), " std::sync::OnceLock::new();\n".to_string()), " LEDGER.get_or_init(|| std::sync::Mutex::new(None))\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Start recording marks. Until armed, `trace_mark` records nothing, so a program that never\n".to_string()), "/// drains the ledger never grows it.\n".to_string()), "pub fn trace_ledger_arm() {\n".to_string()), " if let Ok(mut ledger) = trace_ledger_state().lock() {\n".to_string()), " *ledger = Some(Vec::new());\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Take every row recorded since `trace_ledger_arm` and disarm; `None` when never armed.\n".to_string()), "pub fn trace_ledger_drain() -> Option> {\n".to_string()), " trace_ledger_state().lock().ok().and_then(|mut ledger| ledger.take())\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Process-tree CPU milliseconds — utime + stime + cutime + cstime from `/proc/self/stat` —\n".to_string()), "/// or `None` where that accounting is unreadable. A child's CPU lands once it is reaped, so a\n".to_string()), "/// mark that closes over a waited child carries that child's CPU; a fabricated 0 would read as\n".to_string()), "/// free, which is the direction that looks survivable.\n".to_string()), "pub fn trace_process_tree_cpu_ms() -> Option {\n".to_string()), " let stat = std::fs::read_to_string(\"/proc/self/stat\").ok()?;\n".to_string()), " let after_comm = stat.rsplit(')').next()?;\n".to_string()), " let fields: Vec<&str> = after_comm.split_whitespace().collect();\n".to_string()), " let tick = |i: usize| fields.get(i).and_then(|v| v.parse::().ok());\n".to_string()), " let ticks = tick(11)? + tick(12)? + tick(13)? + tick(14)?;\n".to_string()), " Some(ticks * 1000 / 100)\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep\n".to_string()), "/// CostAccount (std.realization_measurement). Wired (gantt flip): projects Begin/Concluded\n".to_string()), "/// on a PhaseSegment via the observation mirrors of `ci_event_line`. Segment wall on\n".to_string()), "/// Concluded (matched `.begin`→`.done`, else delta since last mark) — never the old\n".to_string()), "/// process-absolute millis field. RSS stays on heartbeat/measurement (`ci_event_line` does\n".to_string()), "/// not project `event.rss` — explicit divergence from the deleted rss-mib field).\n".to_string()), "///\n".to_string()), "/// **Dissolution trigger (DESIGN §6):** delete this fn, the `trace_mark` registry row in\n".to_string()), "/// `04_method.dag` (+ hand-synced twin `v1_compiler_infer_method.rs`), the nine\n".to_string()), "/// `trace_mark(...)` marks in `compile.dag` (+ hand-synced `v1_compiler_compile.rs`), and\n".to_string()), "/// the interpreter arm in `v1_interpreter.rs` when realization_measurement_loop **Phase 0**\n".to_string()), "/// (`docs/plans/realization-measurement-loop.md`) lands a `.dag` `PerformanceReceipt`\n".to_string()), "/// per-stage carrier that a floor witness consumes by execution (the same retirement event\n".to_string()), "/// as `phase_profile.rs` / `GUNBC_FLOOR_GANTT`, per `ci-floor-fractal-gantt (plan doc deleted 2026-08-28)`\n".to_string()), "/// § dissolution). Receipt = that witness green with these marks deleted and stage walls\n".to_string()), "/// still attributable from the model path.\n".to_string()), "pub fn trace_mark(label: String) {\n".to_string()), " use std::sync::{Mutex, OnceLock};\n".to_string()), " use std::time::Instant;\n".to_string()), " static TRACE_T0: OnceLock = OnceLock::new();\n".to_string()), " static LAST_MARK: OnceLock)>> = OnceLock::new();\n".to_string()), " static OPENS: OnceLock)>>> =\n".to_string()), " OnceLock::new();\n".to_string()), " let t0 = TRACE_T0.get_or_init(Instant::now);\n".to_string()), " let last = LAST_MARK.get_or_init(|| Mutex::new((*t0, trace_process_tree_cpu_ms())));\n".to_string()), " let opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));\n".to_string()), " let emoji = obs_phase_emoji();\n".to_string()), " let now = Instant::now();\n".to_string()), " let cpu_now = trace_process_tree_cpu_ms();\n".to_string()), " let _ = GANTT_CENSUS_MARKER;\n".to_string()), " if let Some(phase) = label.strip_suffix(\".begin\") {\n".to_string()), " if let Ok(mut map) = opens.lock() {\n".to_string()), " map.insert(phase.to_string(), (now, cpu_now));\n".to_string()), " }\n".to_string()), " if let Ok(mut l) = last.lock() {\n".to_string()), " *l = (now, cpu_now);\n".to_string()), " }\n".to_string()), " eprintln!(\"{}\", render_phase_begin_line_mirror(phase, emoji));\n".to_string()), " return;\n".to_string()), " }\n".to_string()), " let (phase, opened) = match label.strip_suffix(\".done\") {\n".to_string()), " Some(phase) => (\n".to_string()), " phase.to_string(),\n".to_string()), " opens.lock().ok().and_then(|mut m| m.remove(phase)),\n".to_string()), " ),\n".to_string()), " None => (label.clone(), None),\n".to_string()), " };\n".to_string()), " let (since, since_cpu) = match opened {\n".to_string()), " Some(open) => open,\n".to_string()), " None => last.lock().map(|l| *l).unwrap_or((*t0, None)),\n".to_string()), " };\n".to_string()), " let elapsed_ms = now.saturating_duration_since(since).as_millis() as u64;\n".to_string()), " let cpu_ms = match (cpu_now, since_cpu) {\n".to_string()), " (Some(now_ms), Some(since_ms)) => Some(now_ms.saturating_sub(since_ms)),\n".to_string()), " _ => None,\n".to_string()), " };\n".to_string()), " if let Ok(mut l) = last.lock() {\n".to_string()), " *l = (now, cpu_now);\n".to_string()), " }\n".to_string()), " if let Ok(mut ledger) = trace_ledger_state().lock() {\n".to_string()), " if let Some(rows) = ledger.as_mut() {\n".to_string()), " rows.push(TraceLedgerRow {\n".to_string()), " label: phase.clone(),\n".to_string()), " wall_ms: elapsed_ms,\n".to_string()), " cpu_ms,\n".to_string()), " });\n".to_string()), " }\n".to_string()), " }\n".to_string()), " eprintln!(\n".to_string()), " \"{}\",\n".to_string()), " render_phase_concluded_line_mirror(&phase, elapsed_ms, emoji)\n".to_string()), " );\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash`\n".to_string()), "/// is the `String` projection of this. Use this directly for arbitrary binary content\n".to_string()), "/// (e.g. an executable or serialized payload): routing bytes through `String`/\n".to_string()), "/// `from_utf8_lossy` first collapses every invalid UTF-8 sequence to U+FFFD, so distinct\n".to_string()), "/// byte sequences would hash equal — a §5 silent-collision fail-open for content-addressing.\n".to_string()), "pub fn bytes_identity_hash(bytes: &[u8]) -> Hash {\n".to_string()), " format!(\"{:016x}\", fnv1a64(bytes))\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub fn atom_identity_hash(s: String) -> Hash {\n".to_string()), " bytes_identity_hash(s.as_bytes())\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub fn hash_combine(a: Hash, b: Hash) -> Hash {\n".to_string()), " expect_hash_digest(&a, \"a\");\n".to_string()), " expect_hash_digest(&b, \"b\");\n".to_string()), " let mut bytes = a.into_bytes();\n".to_string()), " bytes.push(0);\n".to_string()), " bytes.extend_from_slice(b.as_bytes());\n".to_string()), " format!(\"{:016x}\", fnv1a64(&bytes))\n".to_string()), "}\n".to_string()), "\n".to_string()) } pub fn rt_resolution_silent_pick_telemetry() -> String { From 2d8c82d116045380c67582c15bea8151243d53e4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 09:23:01 +0000 Subject: [PATCH 05/11] resolve cache keys the RUNNING compiler image via /proc/self/exe, not the start path a rebuild may have re-linked Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- src/v1/stage0/src/resolved_graph_cache.rs | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/v1/stage0/src/resolved_graph_cache.rs b/src/v1/stage0/src/resolved_graph_cache.rs index 5a86aec614a..a91b50f89bb 100644 --- a/src/v1/stage0/src/resolved_graph_cache.rs +++ b/src/v1/stage0/src/resolved_graph_cache.rs @@ -262,12 +262,24 @@ pub fn transform_content_digest() -> Hash { static DIGEST: OnceLock = OnceLock::new(); DIGEST .get_or_init(|| { - let exe = std::env::current_exe().unwrap_or_else(|e| { - panic!( - "resolve cache: cannot locate compiler executable to content-address \ - the transform: {e}" - ) - }); + // THE RUNNING IMAGE, NOT THE PATH IT WAS STARTED FROM. On Linux `current_exe()` + // is the start path, and once a cargo build has re-linked that file it reads + // ` (deleted)` -- unreadable, while the mapped image is still exactly the + // compiler executing this line. Measured 2026-08-30 (BuildBuddy, regen round + // cost): the in-round seed build re-linked a byte-identical binary and this read + // panicked on the stale path. `/proc/self/exe` resolves to the mapped image even + // after the directory entry is replaced, so it is the reading that names THIS + // transform; elsewhere the start path is the only spelling available. + let exe = if cfg!(target_os = "linux") { + std::path::PathBuf::from("/proc/self/exe") + } else { + std::env::current_exe().unwrap_or_else(|e| { + panic!( + "resolve cache: cannot locate compiler executable to content-address \ + the transform: {e}" + ) + }) + }; let bytes = fs::read(&exe).unwrap_or_else(|e| { panic!( "resolve cache: cannot read compiler executable {:?} to content-address \ From 677988a26ddb4d8c73ac3351045d8c17aa5ea660 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 09:23:43 +0000 Subject: [PATCH 06/11] PLANTED (to be reverted): single-module edit in std.content_hash for the affected-set measurement Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- dag/std/content_hash.dag | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dag/std/content_hash.dag b/dag/std/content_hash.dag index 357974e77ce..67658ef8e77 100644 --- a/dag/std/content_hash.dag +++ b/dag/std/content_hash.dag @@ -8,6 +8,8 @@ data content_hash_family_grounding_note: String = "ContentHash is grounded on ha data content_hash_cross_family_eq_v1_seed_bridge_note: String = "Seed-retained hand-Rust backstop (reviews 45323/45331/45353/45359; same wall-after-grounding class as numeric-tower CrossRepresentationEquality in src/v1/stage0/src/v1_interpreter.rs cross_representation_numeric_straddle). cross_family_content_hash_straddle refuses bare `==`/`!=` when both operands are ContentHash values (Value::Variant type_name ContentHash) with mismatched family tags (Fnv1a64 vs Sha256Hash vs Sha1Hash vs Sha512Hash), because the substrate still derives PartialEq on coproducts and a cross-variant compare would fabricate `false` (DESIGN §5). Scoped by coproduct type_name, not variant-name homonyms alone (review 45353). DELETE SCAFFOLD — not a second digest concept; R0 safety backstop only until Phase B of feature:content-hash-family-grounded lands. CENSUS SHRINK — eval_binop Eq/Ne path in v1_interpreter.rs only; Value::eq stays infallible for map keys (same split as numeric tower). WITNESS — UNWITNESSED as of 2026-08-11 (gunbc#8146, review 51178). The witness WAS src/v1/tests/src/cross_representation_equality_test.rs cross_family_content_hash_bare_eq_refuses and cross_family_content_hash_homonym_variant_names_do_not_trigger_guard (discriminating RED if guard removed or over-broad); that module was deleted by the v1 Rust test-suite cutover. Two facts keep this from reading as a regression this PR caused: the suite was gated by cargo test --no-run, so those assertions had not executed since that gate landed — the receipt was already nominal — and the guard itself is untouched and still live in v1_interpreter.rs eval_binop. What is gone is the proof, not the backstop. DISSOLVE-ON: re-establish both assertions as a floor witness through compile_dag_diagnostic_census (naming the expected refusal, since a bare false cannot separate the guard firing from an unrelated earlier failure), OR land Phase B, which makes union-level == a typecheck refusal and retires the R0 backstop and this row together. EXPLICIT DEFERRAL — dissolves when Phase B makes ContentHash union-level `==`/`!=` a typecheck refusal (substrate coproduct eq-suppression) or when union carriers re-ground to family-specific types zero reachable sites; LANE model↔realization fork (DESIGN open thread, same row as CrossRepresentationEquality); ROADMAP docs/plans/compiler-guarantee-recovery-gap-analysis.md Cross-representation `==` full class (R0 backstop today, R3 primitive-grounding ceiling) plus DESIGN §3 dissolve-on feature:content-hash-family-grounded Phase B." +data regen_affected_set_probe_note: String = "PLANTED SINGLE-MODULE EDIT for the affected-set scoping measurement (gunbc.regen_round_cost): one unreferenced data row in one std module, so the regen drift set names exactly the mirrors whose bytes depend on this module." + type HashFamily = | Fnv1a64StructuralFamily | Sha256Family From a389429175238ca1cf3c3ad825e0ffa52d893f46 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 09:52:58 +0000 Subject: [PATCH 07/11] Revert "PLANTED (to be reverted): single-module edit in std.content_hash for the affected-set measurement" This reverts commit 677988a26ddb4d8c73ac3351045d8c17aa5ea660. --- dag/std/content_hash.dag | 2 -- 1 file changed, 2 deletions(-) diff --git a/dag/std/content_hash.dag b/dag/std/content_hash.dag index 67658ef8e77..357974e77ce 100644 --- a/dag/std/content_hash.dag +++ b/dag/std/content_hash.dag @@ -8,8 +8,6 @@ data content_hash_family_grounding_note: String = "ContentHash is grounded on ha data content_hash_cross_family_eq_v1_seed_bridge_note: String = "Seed-retained hand-Rust backstop (reviews 45323/45331/45353/45359; same wall-after-grounding class as numeric-tower CrossRepresentationEquality in src/v1/stage0/src/v1_interpreter.rs cross_representation_numeric_straddle). cross_family_content_hash_straddle refuses bare `==`/`!=` when both operands are ContentHash values (Value::Variant type_name ContentHash) with mismatched family tags (Fnv1a64 vs Sha256Hash vs Sha1Hash vs Sha512Hash), because the substrate still derives PartialEq on coproducts and a cross-variant compare would fabricate `false` (DESIGN §5). Scoped by coproduct type_name, not variant-name homonyms alone (review 45353). DELETE SCAFFOLD — not a second digest concept; R0 safety backstop only until Phase B of feature:content-hash-family-grounded lands. CENSUS SHRINK — eval_binop Eq/Ne path in v1_interpreter.rs only; Value::eq stays infallible for map keys (same split as numeric tower). WITNESS — UNWITNESSED as of 2026-08-11 (gunbc#8146, review 51178). The witness WAS src/v1/tests/src/cross_representation_equality_test.rs cross_family_content_hash_bare_eq_refuses and cross_family_content_hash_homonym_variant_names_do_not_trigger_guard (discriminating RED if guard removed or over-broad); that module was deleted by the v1 Rust test-suite cutover. Two facts keep this from reading as a regression this PR caused: the suite was gated by cargo test --no-run, so those assertions had not executed since that gate landed — the receipt was already nominal — and the guard itself is untouched and still live in v1_interpreter.rs eval_binop. What is gone is the proof, not the backstop. DISSOLVE-ON: re-establish both assertions as a floor witness through compile_dag_diagnostic_census (naming the expected refusal, since a bare false cannot separate the guard firing from an unrelated earlier failure), OR land Phase B, which makes union-level == a typecheck refusal and retires the R0 backstop and this row together. EXPLICIT DEFERRAL — dissolves when Phase B makes ContentHash union-level `==`/`!=` a typecheck refusal (substrate coproduct eq-suppression) or when union carriers re-ground to family-specific types zero reachable sites; LANE model↔realization fork (DESIGN open thread, same row as CrossRepresentationEquality); ROADMAP docs/plans/compiler-guarantee-recovery-gap-analysis.md Cross-representation `==` full class (R0 backstop today, R3 primitive-grounding ceiling) plus DESIGN §3 dissolve-on feature:content-hash-family-grounded Phase B." -data regen_affected_set_probe_note: String = "PLANTED SINGLE-MODULE EDIT for the affected-set scoping measurement (gunbc.regen_round_cost): one unreferenced data row in one std module, so the regen drift set names exactly the mirrors whose bytes depend on this module." - type HashFamily = | Fnv1a64StructuralFamily | Sha256Family From 777f12cce9b4a45d3451f62c61713933cb55965c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 09:54:08 +0000 Subject: [PATCH 08/11] gunbc.regen_round_cost: the affected-set disposition (DerivableNow, with its residual) and the measured cost shape, from the two planted-edit rounds Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- dag/gunbc/regen_round_cost.dag | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/dag/gunbc/regen_round_cost.dag b/dag/gunbc/regen_round_cost.dag index d5ec1926404..f6e723b9355 100644 --- a/dag/gunbc/regen_round_cost.dag +++ b/dag/gunbc/regen_round_cost.dag @@ -225,3 +225,63 @@ fn regen_round_cost_render(receipt: RegenRoundCostReceipt) -> String { concat(concat(regen_round_total_line(receipt: receipt), "\n"), concat(regen_round_changed_paths_line(receipt: receipt), "\n")) ) } + +// --------------------------------------------------------------------------------------------- +// AFFECTED-SET SCOPING: which mirrors does ONE edit change, and is that set derivable today? +// +// Question (operator, 2026-08-30): does the emitted mirror for module M depend only on M's .dag +// transitive import closure, or also on cross-module emitter state? Method: a planted +// single-module edit, then the regen's own drift answer (`changed_paths`, identity grain) +// compared against the reverse import closure computed from `import` lines over dag/, src/v1 +// and src/v2, restricted to modules whose mirror is in the compared population. +// --------------------------------------------------------------------------------------------- + +type AffectedSetDisposition + = DerivableNow { from: String, residual: String } + | BlockedOn { fact: String } + +// Two planted edits, both measured by `claim_executor --regen-round-cost` on the tree named. +// +// (1) v1.compiler.runtime_rust (the runtime template): reverse import closure predicts nine +// mirrors; the drift set was {v1_compiler_runtime_rust.rs, v1_rt.rs}. v1_rt.rs is NOT a +// module's mirror and is NOT in the predicted set -- it is the template's product, emitted +// from the string baked into the EMITTING SEED's own v1_compiler_runtime_rust.rs mirror. +// That is the one cross-module state found: the emitter is compiled from the previous +// generation's mirrors, so the bytes it emits for the runtime depend on the seed binary, +// not on the closure. It is why a template edit needs two rounds (round 1 on srv1 and +// BuildBuddy, tree 0fe2c517: the stale seed emitted the OLD v1_rt.rs over the hand-synced +// one and rebuild-from-installed refused; round 2, tree 75bc5007, reached the fixed point). +// (2) std.content_hash (an unreferenced data row): reverse import closure predicts 72 mirrors; +// the drift set was exactly {std_content_hash.rs} on BuildBuddy (tree 677988a2) and on +// srv1 (same tree). Contained, with a 71-mirror over-approximation. +// +// Both drift sets are contained in (reverse import closure) ∪ {runtime template products}, so +// an affected-set regen is DERIVABLE NOW from the modeled import graph as an upper bound. The +// closure authority already in the seed is `cli_run` `regen_input_sources_over_roots` +// (`extend_sources_to_both_closure_fixpoint`: imports PLUS dotted PLUS bare references), which +// is strictly wider than `import` lines and therefore contains every measured dependent. +// +// WHAT THE MEASUREMENT DOES NOT SHOW, stated so the bound is not over-read: `v1.compiler.emit_rust` +// `reference_derived_use_lines` takes closure-wide arguments (registry, export_sets, +// typed_modules, module_index, shared_types), so an edit that changes what M EXPORTS can change +// the synthesized `use` lines of M's importers. Those importers are in M's reverse closure, so +// the bound holds; what does not hold is the converse -- "unchanged importers" is not +// derivable from the graph alone, only from emitting them. The affected set is therefore a +// correct over-approximation (DESIGN section 5: a structural over-approximation computed AS +// the answer), not an exact set. +data regen_affected_set_disposition: AffectedSetDisposition = DerivableNow { + from: "the .dag dependency closure the regen already builds: cli_run regen_input_sources_over_roots / extend_sources_to_both_closure_fixpoint (imports, dotted and bare references), read in reverse from the edited module, restricted to the compared mirror population, plus the runtime template products (v1_rt.rs) whenever v1.compiler.runtime_rust is in the set", + residual: "the emitting seed's own baked mirrors are a generation-level input to every emitted byte (the two-round bootstrap); the reverse closure over-approximates -- 72 predicted against 1 changed for a std.content_hash data row -- and exactness needs the per-module emit itself" +} + +// WHERE THE ROUND'S TIME GOES, read from the instrument on 2026-08-30 (producer: this module's +// receipt; hosts srv1 and BuildBuddy runner 192.168.241.2; trees 75bc5007 and 677988a2). The +// numbers live in the receipts those runs printed and are not restated here; the SHAPE is: +// every phase runs at CPU ~= wall except the two cargo builds, so the regen process is one core +// end to end; emit is the largest regen phase at roughly half the regen process and a fifth of +// a round with a real edit; the two cargo builds of the seed (before the round, and +// rebuild-from-installed) are the majority of such a round even for a one-mirror change, +// because every mirror compiles into one crate and cargo recompiles that crate whole; the +// corpus is read twice (corpus_load then corpus_reload, the second inside compile_stage0) and +// rustfmt normalizes the whole population three times (mirror_write, adjudicate, digest). +data regen_round_cost_shape_note: String = "The instrument, not the number: re-run `claim_executor --regen-round-cost --source-root dag --source-root src/v2` on a tree and read its receipt (target/stage0-regen-round-cost.txt). A change-denominated regen is bounded above by the reverse closure (regen_affected_set_disposition); a change-denominated REBUILD is bounded below by one whole-crate compile until the stage0 partition (gunbc.stage0_crate_partition_generated) carries the mirrors, which is the larger of the two costs on every measured host." From 45c4ba729ef4dd1553d99c596352ac02e7077dab Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 10:08:05 +0000 Subject: [PATCH 09/11] Regen cost shape: one corpus read threaded to both consumers, and rustfmt normalizes each distinct input once (memo + verified per-version disk cache); rustfmt_spawns on the receipt Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- dag/gunbc/regen_round_cost.dag | 20 +- .../claim/regen_round_cost_witness_test.dag | 3 +- src/v1/stage0/src/required_regen_host.rs | 172 ++++++++++++++++-- 3 files changed, 168 insertions(+), 27 deletions(-) diff --git a/dag/gunbc/regen_round_cost.dag b/dag/gunbc/regen_round_cost.dag index f6e723b9355..40255e777a3 100644 --- a/dag/gunbc/regen_round_cost.dag +++ b/dag/gunbc/regen_round_cost.dag @@ -35,7 +35,6 @@ import std.types { String, Int, List, Bool } type RegenRoundPhase = SeedBuild | CorpusLoad - | CorpusReload | Frontend | Normalize | Reconcile @@ -74,6 +73,7 @@ type RegenRoundCostReceipt { tree_dirty: Bool seed_build_compiled_crates: Int rebuild_compiled_crates: Int + rustfmt_spawns: Int marks: List changed_paths: List } @@ -82,7 +82,6 @@ fn regen_round_phase_tag(phase: RegenRoundPhase) -> String { match phase { SeedBuild => "seed_build" CorpusLoad => "corpus_load" - CorpusReload => "corpus_reload" Frontend => "compile.frontend" Normalize => "compile.normalize" Reconcile => "compile.reconcile" @@ -108,7 +107,6 @@ fn regen_round_phase_label(phase: RegenRoundPhase) -> String { match phase { SeedBuild => "round.seed_build" CorpusLoad => "regen.corpus_load" - CorpusReload => "regen.corpus_reload" Frontend => "compile.frontend" Normalize => "compile.normalize" Reconcile => "compile.reconcile" @@ -126,7 +124,7 @@ fn regen_round_phase_label(phase: RegenRoundPhase) -> String { } fn regen_round_phases() -> List { - [SeedBuild, CorpusLoad, CorpusReload, Frontend, Normalize, Reconcile, Analyses, Emit, MirrorWrite, CandidateVerify, Adjudicate, HandVerify, Digest, Install, RebuildFromInstalled, Diff] + [SeedBuild, CorpusLoad, Frontend, Normalize, Reconcile, Analyses, Emit, MirrorWrite, CandidateVerify, Adjudicate, HandVerify, Digest, Install, RebuildFromInstalled, Diff] } fn regen_round_phase_of_label(label: String) -> RegenRoundPhaseAttribution { @@ -191,7 +189,10 @@ fn regen_round_header_line(receipt: RegenRoundCostReceipt) -> String { concat(concat(" tree=", receipt.tree), concat(" tree_dirty=", regen_round_bool_text(b: receipt.tree_dirty))), concat( concat(" seed_build_compiled_crates=", to_string(receipt.seed_build_compiled_crates)), - concat(" rebuild_compiled_crates=", to_string(receipt.rebuild_compiled_crates)) + concat( + concat(" rebuild_compiled_crates=", to_string(receipt.rebuild_compiled_crates)), + concat(" rustfmt_spawns=", to_string(receipt.rustfmt_spawns)) + ) ) ) ) @@ -282,6 +283,11 @@ data regen_affected_set_disposition: AffectedSetDisposition = DerivableNow { // a round with a real edit; the two cargo builds of the seed (before the round, and // rebuild-from-installed) are the majority of such a round even for a one-mirror change, // because every mirror compiles into one crate and cargo recompiles that crate whole; the -// corpus is read twice (corpus_load then corpus_reload, the second inside compile_stage0) and -// rustfmt normalizes the whole population three times (mirror_write, adjudicate, digest). +// corpus WAS read twice (corpus_load then a corpus_reload inside compile_stage0) and rustfmt +// normalized the whole population three times (mirror_write, adjudicate, digest) -- both +// closed in the change after the measurement: one read threaded to both consumers, and one +// normalize per distinct input through a memo plus a per-rustfmt-version disk cache under +// target/stage0-regen-rustfmt-cache whose entries are honoured only when the stored raw input +// is byte-equal to the request. rustfmt_spawns on the receipt is the count the fixed-point +// control is checked against. data regen_round_cost_shape_note: String = "The instrument, not the number: re-run `claim_executor --regen-round-cost --source-root dag --source-root src/v2` on a tree and read its receipt (target/stage0-regen-round-cost.txt). A change-denominated regen is bounded above by the reverse closure (regen_affected_set_disposition); a change-denominated REBUILD is bounded below by one whole-crate compile until the stage0 partition (gunbc.stage0_crate_partition_generated) carries the mirrors, which is the larger of the two costs on every measured host." diff --git a/dag/test/claim/regen_round_cost_witness_test.dag b/dag/test/claim/regen_round_cost_witness_test.dag index 01a5a3e0b02..1b0567ed784 100644 --- a/dag/test/claim/regen_round_cost_witness_test.dag +++ b/dag/test/claim/regen_round_cost_witness_test.dag @@ -29,6 +29,7 @@ fn a_receipt(marks: List) -> RegenRoundCostReceipt { tree_dirty: false, seed_build_compiled_crates: 0, rebuild_compiled_crates: 2, + rustfmt_spawns: 7, marks: marks, changed_paths: ["v1_rt.rs", "v1_compiler_runtime_rust.rs"] } @@ -109,7 +110,7 @@ test fn a_receipt_renders_provenance_marks_totals_and_changed_paths() -> Bool { a_mark(label: "compile.emit", wall_ms: 300000, cpu_ms: 299000) ])) text == concat( - "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=false seed_build_compiled_crates=0 rebuild_compiled_crates=2\n", + "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=false seed_build_compiled_crates=0 rebuild_compiled_crates=2 rustfmt_spawns=7\n", concat( "regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\nregen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=299000\n", "regen-round-cost: total wall_ms=301500 cpu_ms=308000\nregen-round-cost: changed_paths=2 [v1_rt.rs, v1_compiler_runtime_rust.rs]\n" diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index c7d1342f173..2c80642eed7 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -286,6 +286,7 @@ pub fn run_required_regen( let sources = super::regen_input_sources(&workspace)?; v1_rt::trace_mark("regen.corpus_load.done".to_string()); let authority_digest = authority_digest_from_sources(&sources)?; + let formatter = formatter.with_normalize_cache(&workspace)?; // PRODUCTION, THEN ADJUDICATION -- and the order is the whole repair. // @@ -310,7 +311,7 @@ pub fn run_required_regen( // refusal, because it is emit's output and not a reward for agreeing with the committed tree. // Authority for the ordering: `v2.workflow.required_regen` `required_regen_run`, whose verdict // arms cannot be spelled without the tree they judged. - let (emitted, emitted_basenames) = match emit_generated_surface(&workspace)? { + let (emitted, emitted_basenames) = match emit_generated_surface(&sources)? { // EMIT PRODUCED NOTHING IS NOT A VERDICT ABOUT A TREE. Writing a receipt here would name a // `candidate_artifact` no pass had written, which is the impersonation the receipt split // above exists to end, one field over. `CandidateTreeUnproduced` in @@ -519,10 +520,10 @@ pub fn run_required_regen_fixed_point( } let pass1 = reconcile_pass1_digest(pass1_digest, &prior.candidate_generated_digest)?; - let formatter = ResolvedFormatter::admit()?; + let formatter = ResolvedFormatter::admit()?.with_normalize_cache(&workspace)?; let sources = super::regen_input_sources(&workspace)?; let authority_digest = authority_digest_from_sources(&sources)?; - let emitted = compile_stage0(&workspace)?; + let emitted = compile_stage0(&sources)?; let committed_basenames = committed_generated_basenames(&workspace.join("src/v1/stage0/src"))?; if emitted.is_empty() { return Err("refusal: fixed-point emit produced zero files".to_string()); @@ -635,8 +636,8 @@ enum GeneratedSurfaceAdjudicated { Refused { reason: String }, } -fn emit_generated_surface(workspace: &Path) -> Result { - let emitted = compile_stage0(workspace)?; +fn emit_generated_surface(sources: &[(String, String)]) -> Result { + let emitted = compile_stage0(sources)?; if emitted.is_empty() { return Ok(GeneratedSurfaceEmit::EmitRefused { reason: "refusal: emit produced zero files".to_string(), @@ -674,10 +675,10 @@ fn adjudicate_generated_surface( /// write the candidate BETWEEN them, reaches for the halves. fn measure_generated_surface( formatter: &ResolvedFormatter, - workspace: &Path, + sources: &[(String, String)], stage0_src: &Path, ) -> Result { - let (emitted, emitted_basenames) = match emit_generated_surface(workspace)? { + let (emitted, emitted_basenames) = match emit_generated_surface(sources)? { GeneratedSurfaceEmit::EmitRefused { reason } => { return Ok(GeneratedSurfaceMeasured::Refused { reason }) } @@ -715,8 +716,9 @@ pub fn emitted_generated_sources() -> Result, String> { // rustfmt" would not have named it -- it is three calls above the spawn. Threading the // resolved formatter as an argument made the omission a compile error instead of a fourth // way to discover an absent formatter fifty minutes in. - let formatter = ResolvedFormatter::admit()?; - let emitted = match measure_generated_surface(&formatter, &workspace, &stage0_src)? { + let formatter = ResolvedFormatter::admit()?.with_normalize_cache(&workspace)?; + let sources = super::regen_input_sources(&workspace)?; + let emitted = match measure_generated_surface(&formatter, &sources, &stage0_src)? { GeneratedSurfaceMeasured::Refused { reason } => return Err(reason), GeneratedSurfaceMeasured::Measured { emitted, .. } => emitted, }; @@ -781,16 +783,20 @@ fn is_declared_divergent(file_name: &str) -> bool { EMITTER_PRODUCED_DIVERGENT_STAGE0_FILES.contains(&file_name) } -fn compile_stage0(workspace: &Path) -> Result, String> { - // A SECOND READ OF THE SAME CLOSURE. `run_required_regen` already read these sources for - // the authority digest; this is the same walk again, and it is stamped under its own label - // so the receipt prices the duplication instead of folding it into the frontend. - v1_rt::trace_mark("regen.corpus_reload.begin".to_string()); - let sources = super::regen_input_sources(workspace)?; - v1_rt::trace_mark("regen.corpus_reload.done".to_string()); +/// ONE READ OF THE CLOSURE, SUPPLIED BY THE CALLER. This used to call `regen_input_sources` +/// itself, so every regen walked and read the corpus twice -- once for the authority digest +/// and once here -- and the receipt priced the second walk at a quarter of the first phase +/// (`regen.corpus_reload`, measured 2026-08-30: 24 s on srv1, 17 s on BuildBuddy). The +/// sources are one fact; the caller reads them once and both consumers take that one value. +fn compile_stage0(sources: &[(String, String)]) -> Result, String> { let source_files: Vec> = sources - .into_iter() - .map(|(path, content)| Rc::new(SourceFile { path, content })) + .iter() + .map(|(path, content)| { + Rc::new(SourceFile { + path: path.clone(), + content: content.clone(), + }) + }) .collect(); let result = compile_sources(Rc::new(source_files.into()), RenderTarget::Rust); if let Some(message) = @@ -1372,6 +1378,69 @@ const NORMALIZE_FIXED_POINT_MAX_PASSES: usize = 8; fn normalize_generated_source( formatter: &ResolvedFormatter, content: &str, +) -> Result { + if let Some(hit) = formatter.memo.borrow().get(content) { + return Ok(hit.clone()); + } + if let Some(hit) = normalize_cache_read(formatter, content) { + formatter + .memo + .borrow_mut() + .insert(content.to_string(), hit.clone()); + return Ok(hit); + } + let normalized = normalize_generated_source_uncached(formatter, content)?; + formatter + .memo + .borrow_mut() + .insert(content.to_string(), normalized.clone()); + normalize_cache_write(formatter, content, &normalized)?; + Ok(normalized) +} + +/// The cache file layout: `\n`. The raw bytes +/// are stored whole and compared whole on read, so the file name (a digest) only locates the +/// entry and never stands in for its identity. +fn normalize_cache_path(formatter: &ResolvedFormatter, content: &str) -> Option { + formatter + .disk_cache + .as_ref() + .map(|dir| dir.join(v1_rt::bytes_identity_hash(content.as_bytes()))) +} + +fn normalize_cache_read(formatter: &ResolvedFormatter, content: &str) -> Option { + let path = normalize_cache_path(formatter, content)?; + let bytes = fs::read(&path).ok()?; + let newline = bytes.iter().position(|b| *b == b'\n')?; + let raw_len: usize = std::str::from_utf8(&bytes[..newline]).ok()?.parse().ok()?; + let raw_start = newline + 1; + let raw_end = raw_start.checked_add(raw_len)?; + if raw_end > bytes.len() || &bytes[raw_start..raw_end] != content.as_bytes() { + return None; + } + String::from_utf8(bytes[raw_end..].to_vec()).ok() +} + +fn normalize_cache_write( + formatter: &ResolvedFormatter, + content: &str, + normalized: &str, +) -> Result<(), String> { + let Some(path) = normalize_cache_path(formatter, content) else { + return Ok(()); + }; + let mut bytes = format!("{}\n", content.len()).into_bytes(); + bytes.extend_from_slice(content.as_bytes()); + bytes.extend_from_slice(normalized.as_bytes()); + // Write-then-rename, so a reader never sees a half-written entry as a miss-with-garbage. + let tmp = path.with_extension(format!("tmp{}", std::process::id())); + fs::write(&tmp, &bytes).map_err(|e| format!("write {}: {e}", tmp.display()))?; + fs::rename(&tmp, &path).map_err(|e| format!("rename {}: {e}", path.display())) +} + +fn normalize_generated_source_uncached( + formatter: &ResolvedFormatter, + content: &str, ) -> Result { let mut current = normalize_generated_source_attempt(formatter, content)?; for _ in 1..NORMALIZE_FIXED_POINT_MAX_PASSES { @@ -1429,8 +1498,34 @@ pub struct ResolvedFormatter { /// The PATH the program was found on, kept so a later failure can say what was searched /// rather than making the reader reconstruct it from the environment they are not in. searched: String, + /// NORMALIZE ONCE PER DISTINCT INPUT. Every regen pass normalized the whole emitted + /// population THREE times -- `write_emitted_tree`, `compare_generated_surfaces`, + /// `tree_digest_from_map` each called `normalize_generated_source` on the same bytes -- + /// and each normalize is at least two rustfmt spawns (the fixed-point seek). Measured + /// 2026-08-30 by `--regen-round-cost`: mirror_write + adjudicate + digest = 67 s on the + /// BuildBuddy runner, 97 s on srv1, for one population. The memo is keyed by the RAW + /// input bytes, so a hit is exact by construction; clones share it so the three + /// consumers see one table. + memo: Rc>>, + /// The on-disk half, keyed by rustfmt's own version string, so a fixed-point run on a tree + /// whose emit did not change pays ZERO rustfmt spawns. Each entry stores the raw input + /// beside the normalized output and is honoured only when the stored raw is byte-equal to + /// the request -- a digest-named file is a LOCATION, never the identity, so a hash + /// collision cannot serve another input's formatting (DESIGN section 5). + disk_cache: Option, +} + +/// Every rustfmt spawn this process has made. Read by `--regen-round-cost` before and after +/// the round so the receipt carries the count -- the fixed-point control's claim is that it +/// is ~0, and a claim about a count is checked against the count. +static RUSTFMT_SPAWNS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +pub fn rustfmt_spawn_count() -> u64 { + RUSTFMT_SPAWNS.load(std::sync::atomic::Ordering::Relaxed) } +const NORMALIZE_CACHE_DIR_REL: &str = "target/stage0-regen-rustfmt-cache"; + impl ResolvedFormatter { /// Resolve `rustfmt` against a supplied PATH string. Separate from the environment read so /// the refusal is testable without mutating a process-global that every other test shares. @@ -1444,6 +1539,8 @@ impl ResolvedFormatter { let resolved = ResolvedFormatter { program: candidate, searched: path_var.to_string(), + memo: Rc::new(std::cell::RefCell::new(HashMap::new())), + disk_cache: None, }; resolved.probe()?; return Ok(resolved); @@ -1524,6 +1621,36 @@ impl ResolvedFormatter { Self::from_path_var(&std::env::var("PATH").unwrap_or_default()) } + /// Attach the on-disk normalize cache for this formatter's version under `target/`. + /// The version is read from the program itself, so a toolchain change opens a new + /// directory rather than serving another rustfmt's fixed points. + fn with_normalize_cache(mut self, workspace: &Path) -> Result { + let out = self + .command() + .arg("--version") + .output() + .map_err(|e| self.spawn_refusal(e))?; + if !out.status.success() { + return Err(format!( + "rustfmt --version failed at {}: {}", + self.program.display(), + String::from_utf8_lossy(&out.stderr) + )); + } + let version: String = String::from_utf8_lossy(&out.stdout) + .trim() + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect(); + if version.is_empty() { + return Err("rustfmt --version printed nothing; the cache has no key".to_string()); + } + let dir = workspace.join(NORMALIZE_CACHE_DIR_REL).join(version); + fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?; + self.disk_cache = Some(dir); + Ok(self) + } + fn command(&self) -> Command { Command::new(&self.program) } @@ -1550,6 +1677,7 @@ fn normalize_generated_source_attempt( formatter: &ResolvedFormatter, content: &str, ) -> Result { + RUSTFMT_SPAWNS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let mut child = formatter .command() .arg("--edition") @@ -1587,6 +1715,7 @@ fn normalize_with_workdir( ) -> Result { let path = work_dir.join(format!("{label}.rs")); fs::write(&path, content).map_err(|e| format!("write {}: {e}", path.display()))?; + RUSTFMT_SPAWNS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let output = formatter .command() .arg("--edition") @@ -2296,6 +2425,7 @@ fn render_round_cost_receipt( tree_dirty: bool, seed_build_compiled_crates: u64, rebuild_compiled_crates: u64, + rustfmt_spawns: u64, marks: &[v1_rt::TraceLedgerRow], changed_paths: &[String], ) -> Result { @@ -2346,6 +2476,7 @@ fn render_round_cost_receipt( ctx.sym("rebuild_compiled_crates"), Value::Int(rebuild_compiled_crates as i64), ), + (ctx.sym("rustfmt_spawns"), Value::Int(rustfmt_spawns as i64)), (ctx.sym("marks"), Value::List(Rc::new(mark_values.into()))), ( ctx.sym("changed_paths"), @@ -2381,6 +2512,7 @@ pub fn run_regen_round_cost( let exe_before = current_exe_digest()?; v1_rt::trace_ledger_arm(); + let rustfmt_spawns_before = rustfmt_spawn_count(); let seed_build = seed_cargo_build(&workspace, "round.seed_build")?; let exe_after = current_exe_digest()?; if exe_before != exe_after { @@ -2451,6 +2583,7 @@ pub fn run_regen_round_cost( tree_dirty, seed_build.compiled_crates, rebuild_compiled_crates, + rustfmt_spawn_count() - rustfmt_spawns_before, &marks, &changed_paths, )?; @@ -2497,6 +2630,7 @@ mod regen_round_cost_tests { true, 0, 2, + 7, &marks, &["v1_rt.rs".to_string()], ) @@ -2505,7 +2639,7 @@ mod regen_round_cost_tests { rendered, "regen-round-cost: producer=claim_executor --regen-round-cost host=srv1 \ tree=2a11b317d2caf3c37d1d38a4421e8e0c06188925 tree_dirty=true \ - seed_build_compiled_crates=0 rebuild_compiled_crates=2\n\ + seed_build_compiled_crates=0 rebuild_compiled_crates=2 rustfmt_spawns=7\n\ regen-round-cost: phase=seed_build wall_ms=1500 cpu_ms=9000\n\ regen-round-cost: phase=compile.emit wall_ms=300000 cpu_ms=na\n\ regen-round-cost: total wall_ms=301500 cpu_ms=na\n\ From fce29f50d78f38550e06774050b9b478c8607658 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 10:31:43 +0000 Subject: [PATCH 10/11] gunbc.regen_round_cost: durations are std.observation TimedMeasurement rows over std.measure Nanosecond on a named clock, never a bare Int with the unit in the field name (review 57566) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- .regen-probe-untracked | 0 dag/gunbc/regen_round_cost.dag | 98 ++++++++++++------- .../claim/regen_round_cost_witness_test.dag | 54 +++++++--- src/v1/stage0/src/required_regen_host.rs | 61 ++++++++++-- 4 files changed, 153 insertions(+), 60 deletions(-) create mode 100644 .regen-probe-untracked diff --git a/.regen-probe-untracked b/.regen-probe-untracked new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dag/gunbc/regen_round_cost.dag b/dag/gunbc/regen_round_cost.dag index f6e723b9355..a302366226f 100644 --- a/dag/gunbc/regen_round_cost.dag +++ b/dag/gunbc/regen_round_cost.dag @@ -1,6 +1,9 @@ module gunbc.regen_round_cost -import std.types { String, Int, List, Bool } +import std.types { String, Int, List, Bool, NonEmptyStr } +import std.nat { Nat } +import std.measure { ClockBasis, CpuClock, WallClock, Nanosecond, nanosecond, nanosecond_count } +import std.observation { TimedMeasurement, Measured, MeasuredValue, MeasuredUnavailable, observation_duration_of } // ONE REGEN ROUND, PRICED BY PHASE, ON TWO CLOCKS -- the instrument behind // `claim_executor --regen-round-cost`. A round is the convergence recipe that @@ -27,10 +30,14 @@ import std.types { String, Int, List, Bool } // lands on the mark that reaped it. A phase whose wall is large and whose CPU is near zero is // therefore waiting, not computing -- the discrimination cli_run's floor heartbeat exists for. // -// WHERE THE READING CANNOT BE TAKEN IT IS UNREADABLE, NEVER ZERO. CpuUnreadable is the arm for a -// host without process accounting; a fabricated 0 would read as "free", which is the direction -// that looks survivable (gunbc.observation_seed_render seed_heartbeat_unreadable_cause is the -// same refusal one instrument over). +// WHERE THE READING CANNOT BE TAKEN IT IS UNREADABLE, NEVER ZERO. A mark's clocks are +// std.observation TimedMeasurement rows -- the basis is always known, the elapsed is Measured +// -- so an unreadable CPU clock is MeasuredUnavailable with its cause, and a renderer prints +// the cause's sentinel rather than a 0 that reads as "free" (observation_measured_note; the +// same refusal gunbc.observation_seed_render seed_heartbeat_unreadable_cause keeps one +// instrument over). No unit lives in a field name here: every duration is a Nanosecond +// carried by its clock basis (std.measure measure_clock_basis_note), and the receipt renders +// milliseconds as a projection of that value. type RegenRoundPhase = SeedBuild @@ -50,17 +57,43 @@ type RegenRoundPhase | RebuildFromInstalled | Diff -type CpuReading - = CpuMs { ms: Int } - | CpuUnreadable - // A drained ledger row. `label` is the trace_mark label with its `.done` suffix removed (a plain -// mark keeps its label whole); `wall_ms` is the elapsed since the matching `.begin`, or since the -// previous mark when no begin was opened, which is the contract trace_mark already had. +// mark keeps its label whole); the wall duration is the elapsed since the matching `.begin`, or +// since the previous mark when no begin was opened, which is the contract trace_mark already had; +// the CPU duration is the process-tree CPU spent over the same span, on its own clock row. type TraceMark { label: String - wall_ms: Int - cpu: CpuReading + durations: List +} + +data regen_round_cpu_unreadable_cause: NonEmptyStr = "process accounting unreadable" as NonEmptyStr + +fn regen_round_mark(label: String, wall: Nanosecond, cpu: Measured) -> TraceMark { + TraceMark { + label: label, + durations: [ + TimedMeasurement { basis: WallClock, elapsed: MeasuredValue { value: wall } }, + TimedMeasurement { basis: CpuClock, elapsed: cpu } + ] + } +} + +data regen_round_nanoseconds_per_millisecond: Nat = 1000000 + +// The receipt renders whole milliseconds; the carrier keeps the nanosecond the clock read. +fn regen_round_ms_text(d: Measured) -> String { + match d { + MeasuredValue { value: v } => to_string(nanosecond_count(n: v) / regen_round_nanoseconds_per_millisecond) + MeasuredUnavailable { cause: c } => "na" + } +} + +fn regen_round_mark_wall(mark: TraceMark) -> Measured { + observation_duration_of(durations: mark.durations, basis: WallClock) +} + +fn regen_round_mark_cpu(mark: TraceMark) -> Measured { + observation_duration_of(durations: mark.durations, basis: CpuClock) } type RegenRoundPhaseAttribution @@ -143,13 +176,6 @@ fn regen_round_phase_of_label(label: String) -> RegenRoundPhaseAttribution { }) } -fn regen_round_cpu_text(cpu: CpuReading) -> String { - match cpu { - CpuMs { ms: m } => to_string(m) - CpuUnreadable => "na" - } -} - fn regen_round_mark_line(mark: TraceMark) -> String { let subject = match regen_round_phase_of_label(label: mark.label) { Attributed { phase: p } => concat("phase=", regen_round_phase_tag(phase: p)) @@ -157,24 +183,24 @@ fn regen_round_mark_line(mark: TraceMark) -> String { } concat( concat("regen-round-cost: ", subject), - concat(concat(" wall_ms=", to_string(mark.wall_ms)), concat(" cpu_ms=", regen_round_cpu_text(cpu: mark.cpu))) + concat( + concat(" wall_ms=", regen_round_ms_text(d: regen_round_mark_wall(mark: mark))), + concat(" cpu_ms=", regen_round_ms_text(d: regen_round_mark_cpu(mark: mark))) + ) ) } -fn regen_round_wall_total_ms(marks: List) -> Int { - fold(marks, init: 0, f: fn(acc, mark) { acc + mark.wall_ms }) -} - -// The CPU total is readable only when every row is; one unreadable row makes the sum a -// fabrication and the total says so instead of summing the rows it could read. -fn regen_round_cpu_total(marks: List) -> CpuReading { - fold(marks, init: CpuMs { ms: 0 }, f: fn(acc, mark) { +// A total on one clock is readable only when every row's reading on that clock is; one +// unreadable row makes the sum a fabrication, and the total says so instead of summing the rows +// it could read. +fn regen_round_total(marks: List, basis: ClockBasis) -> Measured { + fold(marks, init: MeasuredValue { value: nanosecond(count: 0) }, f: fn(acc, mark) { match acc { - CpuUnreadable => CpuUnreadable - CpuMs { ms: sum } => - match mark.cpu { - CpuUnreadable => CpuUnreadable - CpuMs { ms: m } => CpuMs { ms: sum + m } + MeasuredUnavailable { cause: c } => MeasuredUnavailable { cause: c } + MeasuredValue { value: sum } => + match observation_duration_of(durations: mark.durations, basis: basis) { + MeasuredUnavailable { cause: c } => MeasuredUnavailable { cause: c } + MeasuredValue { value: v } => MeasuredValue { value: nanosecond(count: nanosecond_count(n: sum) + nanosecond_count(n: v)) } } } }) @@ -199,8 +225,8 @@ fn regen_round_header_line(receipt: RegenRoundCostReceipt) -> String { fn regen_round_total_line(receipt: RegenRoundCostReceipt) -> String { concat( - concat("regen-round-cost: total wall_ms=", to_string(regen_round_wall_total_ms(marks: receipt.marks))), - concat(" cpu_ms=", regen_round_cpu_text(cpu: regen_round_cpu_total(marks: receipt.marks))) + concat("regen-round-cost: total wall_ms=", regen_round_ms_text(d: regen_round_total(marks: receipt.marks, basis: WallClock))), + concat(" cpu_ms=", regen_round_ms_text(d: regen_round_total(marks: receipt.marks, basis: CpuClock))) ) } diff --git a/dag/test/claim/regen_round_cost_witness_test.dag b/dag/test/claim/regen_round_cost_witness_test.dag index 01a5a3e0b02..cf6d19f26f4 100644 --- a/dag/test/claim/regen_round_cost_witness_test.dag +++ b/dag/test/claim/regen_round_cost_witness_test.dag @@ -1,11 +1,13 @@ module test.claim.regen_round_cost_witness -import std.types { Bool, String, List } +import std.types { Bool, String, List, NonEmptyStr } +import std.measure { CpuClock, WallClock, Nanosecond, nanosecond, nanosecond_count } +import std.observation { Measured, MeasuredValue, MeasuredUnavailable } import gunbc.regen_round_cost { - RegenRoundCostReceipt, TraceMark, CpuReading, CpuMs, CpuUnreadable, + RegenRoundCostReceipt, TraceMark, regen_round_mark, regen_round_cpu_unreadable_cause, RegenRoundPhase, Emit, SeedBuild, RegenRoundPhaseAttribution, Attributed, Unattributed, regen_round_phase_of_label, regen_round_phase_tag, regen_round_phase_label, regen_round_phases, - regen_round_cpu_total, regen_round_wall_total_ms, regen_round_mark_line, regen_round_cost_render + regen_round_total, regen_round_mark_line, regen_round_cost_render } // WHAT THIS WITNESS COVERS: the fold from a drained trace ledger to the receipt lines the host @@ -17,8 +19,23 @@ import gunbc.regen_round_cost { // know renders as `unattributed`, so the disagreement is visible in the receipt rather than // silent. +fn ms(count: Int) -> Nanosecond { + nanosecond(count: count * 1000000) +} + fn a_mark(label: String, wall_ms: Int, cpu_ms: Int) -> TraceMark { - TraceMark { label: label, wall_ms: wall_ms, cpu: CpuMs { ms: cpu_ms } } + regen_round_mark(label: label, wall: ms(count: wall_ms), cpu: MeasuredValue { value: ms(count: cpu_ms) }) +} + +fn a_cpu_unreadable_mark(label: String, wall_ms: Int) -> TraceMark { + regen_round_mark(label: label, wall: ms(count: wall_ms), cpu: MeasuredUnavailable { cause: regen_round_cpu_unreadable_cause }) +} + +fn measured_ms_is(d: Measured, expected_ms: Int) -> Bool { + match d { + MeasuredValue { value: v } => nanosecond_count(n: v) == expected_ms * 1000000 + MeasuredUnavailable { cause: c } => false + } } fn a_receipt(marks: List) -> RegenRoundCostReceipt { @@ -68,27 +85,36 @@ test fn w_RED_an_emit_sub_mark_is_unattributed_not_folded_into_emit() -> Bool { } test fn a_wall_total_is_the_sum_of_the_marks() -> Bool { - regen_round_wall_total_ms(marks: [a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), a_mark(label: "compile.emit", wall_ms: 300000, cpu_ms: 299000)]) == 301500 + measured_ms_is( + d: regen_round_total(marks: [a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), a_mark(label: "compile.emit", wall_ms: 300000, cpu_ms: 299000)], basis: WallClock), + expected_ms: 301500 + ) } // THE DISCRIMINATING RED for the CPU total: one unreadable row makes the total unreadable. A // total that summed the readable rows would report a smaller CPU than the round spent. test fn w_RED_one_unreadable_cpu_row_makes_the_cpu_total_unreadable() -> Bool { - let total = regen_round_cpu_total(marks: [ + let total = regen_round_total(marks: [ a_mark(label: "round.seed_build", wall_ms: 1500, cpu_ms: 9000), - TraceMark { label: "compile.emit", wall_ms: 300000, cpu: CpuUnreadable } - ]) + a_cpu_unreadable_mark(label: "compile.emit", wall_ms: 300000) + ], basis: CpuClock) match total { - CpuUnreadable => true - CpuMs { ms: m } => false + MeasuredUnavailable { cause: c } => true + MeasuredValue { value: v } => false } } test fn a_all_readable_cpu_rows_sum() -> Bool { - match regen_round_cpu_total(marks: [a_mark(label: "a", wall_ms: 1, cpu_ms: 2), a_mark(label: "b", wall_ms: 3, cpu_ms: 4)]) { - CpuMs { ms: m } => m == 6 - CpuUnreadable => false - } + measured_ms_is(d: regen_round_total(marks: [a_mark(label: "a", wall_ms: 1, cpu_ms: 2), a_mark(label: "b", wall_ms: 3, cpu_ms: 4)], basis: CpuClock), expected_ms: 6) +} + +// The wall total of the same rows is readable even though the CPU total is not: the two +// clocks are two rows, and an unreadable CPU never poisons the wall. +test fn a_wall_total_survives_an_unreadable_cpu_row() -> Bool { + measured_ms_is( + d: regen_round_total(marks: [a_mark(label: "a", wall_ms: 1500, cpu_ms: 9000), a_cpu_unreadable_mark(label: "b", wall_ms: 300000)], basis: WallClock), + expected_ms: 301500 + ) } test fn a_mark_line_names_the_phase_and_both_clocks() -> Bool { diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index c7d1342f173..0129150e856 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2307,26 +2307,62 @@ fn render_round_cost_receipt( format!("refusal: {entry} did not resolve, so the receipt cannot render: {e}") })?; let ctx = super::make_eval_context(&graph, indices, ExecutionMode::Hermetic); - let cpu_value = |cpu_ms: Option| match cpu_ms { + // The model's carriers, built in the model's vocabulary: a duration is a + // `std.measure` Nanosecond (`Measure { count }`) inside `std.observation` Measured, on a + // `TimedMeasurement` row that names its clock. Milliseconds from the ledger become + // nanoseconds here so no unit lives in a field name past this boundary. + let nanosecond = |ms: u64| Value::Record { + type_name: ctx.sym("Measure"), + fields: Rc::new(vec![( + ctx.sym("count"), + Value::Int((ms * 1_000_000) as i64), + )]), + }; + let measured = |ms: Option| match ms { Some(ms) => Value::Variant { - type_name: ctx.sym("CpuReading"), - variant_name: ctx.sym("CpuMs"), - fields: Rc::new(vec![(ctx.sym("ms"), Value::Int(ms as i64))]), + type_name: ctx.sym("Measured"), + variant_name: ctx.sym("MeasuredValue"), + fields: Rc::new(vec![(ctx.sym("value"), nanosecond(ms))]), }, None => Value::Variant { - type_name: ctx.sym("CpuReading"), - variant_name: ctx.sym("CpuUnreadable"), - fields: Rc::new(vec![]), + type_name: ctx.sym("Measured"), + variant_name: ctx.sym("MeasuredUnavailable"), + fields: Rc::new(vec![( + ctx.sym("cause"), + str_value("process accounting unreadable"), + )]), }, }; + let timed = |basis: &str, ms: Option| Value::Record { + type_name: ctx.sym("TimedMeasurement"), + fields: Rc::new(vec![ + ( + ctx.sym("basis"), + Value::Variant { + type_name: ctx.sym("ClockBasis"), + variant_name: ctx.sym(basis), + fields: Rc::new(vec![]), + }, + ), + (ctx.sym("elapsed"), measured(ms)), + ]), + }; let mark_values: Vec = marks .iter() .map(|row| Value::Record { type_name: ctx.sym("TraceMark"), fields: Rc::new(vec![ (ctx.sym("label"), str_value(row.label.clone())), - (ctx.sym("wall_ms"), Value::Int(row.wall_ms as i64)), - (ctx.sym("cpu"), cpu_value(row.cpu_ms)), + ( + ctx.sym("durations"), + Value::List(Rc::new( + vec![ + timed("WallClock", Some(row.wall_ms)), + timed("CpuClock", row.cpu_ms), + ] + .into(), + )), + ), ]), }) .collect(); @@ -2477,7 +2513,12 @@ mod regen_round_cost_tests { /// forty-minute round. The expected text is the same fixture the .dag witness asserts. #[test] fn host_built_receipt_renders_through_the_model() { - let roots = vec![workspace_root().join("dag").to_string_lossy().into_owned()]; + // Both roots the production driver passes: `std.observation`'s closure reaches + // `std.cache_interface`, which imports `v2.std.optional` from src/v2. + let roots: Vec = ["dag", "src/v2"] + .iter() + .map(|r| workspace_root().join(r).to_string_lossy().into_owned()) + .collect(); let marks = vec![ v1_rt::TraceLedgerRow { label: "round.seed_build".to_string(), From 23fd817ad032d9657a9be00780c93df5e676065b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sun, 30 Aug 2026 11:38:56 +0000 Subject: [PATCH 11/11] The regen round cost witness lives under test.claim.self_host_ so the required floor plans it (it is a self-host regen instrument) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KGCnzm2zkb37mVHgrGCt9L --- ...ess_test.dag => self_host_regen_round_cost_witness_test.dag} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename dag/test/claim/{regen_round_cost_witness_test.dag => self_host_regen_round_cost_witness_test.dag} (99%) diff --git a/dag/test/claim/regen_round_cost_witness_test.dag b/dag/test/claim/self_host_regen_round_cost_witness_test.dag similarity index 99% rename from dag/test/claim/regen_round_cost_witness_test.dag rename to dag/test/claim/self_host_regen_round_cost_witness_test.dag index 7e5b98124b6..2a173e9c3eb 100644 --- a/dag/test/claim/regen_round_cost_witness_test.dag +++ b/dag/test/claim/self_host_regen_round_cost_witness_test.dag @@ -1,4 +1,4 @@ -module test.claim.regen_round_cost_witness +module test.claim.self_host_regen_round_cost_witness import std.types { Bool, String, List, NonEmptyStr } import std.measure { CpuClock, WallClock, Nanosecond, nanosecond, nanosecond_count }