From 665637df710e1e0d472e68ad06ca96f6b7b5e4fa Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 04:51:35 +0000 Subject: [PATCH 01/39] D0 retention-truth close-out: unpin compile-clean memo, register all-hit keys, arm from loader closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three post-merge retention defects from the ci-two-tier-placement-redesign §5 review (routed to the #7129 worker) plus the two §7 acceptance controls. Sequenced first: the falsifier cannot go green — and no downstream placement row can cite true retention receipts — until these hold. D0.1 — compile-clean aggregate memo unpin. The whole-tree gate resolved through resolved_graph_from_sources_with_index, pinning the aggregate ResolvedGraph (hence every TypedModule) in resolved_graph_memo for the process lifetime — a large slice of the measured 9.2GB resident floor. Thread a ResolvedGraphMemoShare::{Memoize,Ephemeral} flag: the gate resolves Ephemeral (no aggregate pin); per-entry discovery keeps memoizing. The per-module typed-cache warming that IS the gate's purpose is unaffected. D0.2 — prewarm all-hit registration. try_reconcile_all_cache_hits assembled and returned on all-hit WITHOUT index_record_schedule_module, so a prewarmed run armed retention referencing nothing (completion reported evictions while removing nothing). Record each confirmed hit in the probe, same key forms as the slow path. D0.3 — arm from the loader's exact closure (the #6985 Class-B root, third appearance). Arming used selection_adjacency; the discovery loader load_sources_for_entry_with_pool reaches wider via qualified-projection references from import-bearing files, so those modules were re-cached after eviction and never re-evicted or counted — an invisible resident leak. Arm from the loader itself (not a re-derived BFS that could become a fourth divergence); cost-neutral because the loader memoizes into entry_closure_sources, which discovery reuses. Removed the now-dead selection_closure_live_paths_with_facts. D0.4 — the two §7 acceptance controls. index_schedule_entry_completed dropped the resolved-graph pin unconditionally, so the eviction-disabled retain-all baseline understated peak retention; gate it on evict_enabled. The E2E control armed BEFORE prewarming (the order-blindness that let D0.2 pass green), so it never exercised the all-hit probe; restructure to prewarm -> clear graph memo -> arm -> re-resolve through the probe, and add a real-index retain-all RED. Verified green by execution: 7/7 schedule_retention tests, incl. the two restructured REDs (E2E arm-after-prewarm; real-index retain-all). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- src/v1/stage0/src/cli_run.rs | 264 +++++++++++++++++++++++++++++------ 1 file changed, 219 insertions(+), 45 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index ae779d93e05..9fac199df60 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -2836,6 +2836,9 @@ fn floor_compile_clean_emit_ok_via_index( sources, ResolveTypecheckGate::Strict, "floor-compile-clean-gate", + // Ephemeral: the whole-tree aggregate graph must NOT join the process share tier + // (D0.1) — it would pin every TypedModule in the tree for the process lifetime. + ResolvedGraphMemoShare::Ephemeral, ) { Ok(resolved) => resolved, Err(msg) => { @@ -3847,19 +3850,6 @@ pub fn import_closure_live_paths_with_facts( import_closure_from_adjacency(entry_path, &facts.adjacency) } -/// Like `import_closure_live_paths_with_facts` but over the WIDER `selection_adjacency` -/// (import + strict reference edges) — so a module reached cross-entry only through a -/// bare-reference edge is still refcounted by schedule-derived retention. Over-retaining -/// (the safe direction: never premature-evict a reference-reached module into a -/// recompute-on-miss) and equally cheap: a BFS over prebuilt adjacency, no per-entry -/// source loading, no #6848 both-closure fixpoint. -pub(crate) fn selection_closure_live_paths_with_facts( - entry_path: &str, - facts: &ModuleGraphFactsLive, -) -> Vec { - import_closure_from_adjacency(entry_path, &facts.selection_adjacency) -} - impl ModuleGraphFactsLive { /// Repo-relative paths of every declared module in the facts scan — the existence /// set for refuse-vs-answer decisions (a module can be absent from `adjacency` @@ -5622,6 +5612,30 @@ pub fn entry_closure_sources_len_for_test(index: &MultiEntryIndex) -> usize { index.entry_closure_sources.borrow().len() } +/// Drop every assembled per-closure graph from the in-process share memo, leaving the +/// per-module typed cache warm. Reproduces the production state after the compile-clean +/// gate warms the shared index Ephemerally (D0.1): modules cached, no per-entry graph +/// pin — so the next per-entry resolve misses the memo and takes reconcile's ALL-HITS +/// probe, the path D0.2's schedule-key registration lives on. +#[cfg(any(test, feature = "interp_test_witness"))] +pub fn clear_resolved_graph_memo_for_test(index: &MultiEntryIndex) { + index.resolved_graph_memo.borrow_mut().clear(); +} + +/// Install a schedule retention armed over `per_entry` with an EXPLICIT eviction switch, +/// bypassing the process-global `GUNBC_SCHEDULE_RETENTION_EVICT` env — so the D0.4 +/// retain-all (`evict_enabled=false`) baseline can be exercised on a real index without +/// racing that env against concurrently-running tests. +#[cfg(any(test, feature = "interp_test_witness"))] +pub fn install_schedule_retention_for_test( + index: &MultiEntryIndex, + per_entry: Vec<(String, Vec)>, + evict_enabled: bool, +) { + *index.schedule_retention.borrow_mut() = + Some(ScheduleRetention::armed(per_entry, evict_enabled)); +} + #[cfg(any(test, feature = "interp_test_witness"))] pub fn both_closure_edges_initialized_for_test(index: &MultiEntryIndex) -> bool { index.both_closure_edges.borrow().is_some() @@ -6251,6 +6265,14 @@ impl ScheduleRetention { pub fn resolved_graph_evictions(&self) -> u64 { self.resolved_graph_evictions } + /// The eviction switch this retention was armed with (false = + /// `GUNBC_SCHEDULE_RETENTION_EVICT=0`, the retain-all measurement pole). Consumers + /// outside the bookkeeper — the index's resolved-graph pin drop — gate on it so the + /// pole retains resolved graphs too, not just the per-module caches (D0.4: the pre-M2 + /// baseline was invalid because it kept dropping graphs unconditionally). + pub fn evict_enabled(&self) -> bool { + self.evict_enabled + } pub fn note_graph_eviction(&mut self) { self.resolved_graph_evictions += 1; } @@ -6283,16 +6305,33 @@ fn index_arm_schedule_retention(index: &MultiEntryIndex, rows: &[DiscoveryRow]) if !seen.insert(row.entry.as_str()) { continue; } - // CHEAP closure: a BFS over the prebuilt selection adjacency (import + strict - // reference edges; no per-entry source loading, no #6848 both-closure fixpoint - // walk) — keyed by the repo-relative path the reconcile loop records - // (`decl_file`). The prior `collect_both_closure_module_names_for_entry` - // front-loaded the source-loading fixpoint for every distinct entry (the - // compiler-affected worst case is ~694), a cost the affected-set path otherwise - // skips or defers. The wider selection tier over-retains (never premature-evicts - // a reference-reached module into a recompute-on-miss); a module reached by no - // edge at all surfaces as counted RetentionUnknown at reconcile (retained, §5). - let paths = selection_closure_live_paths_with_facts(&row.entry, &index.module_graph_facts); + // Arm from the LOADER's EXACT closure — `load_sources_for_entry_with_pool`, the + // same function the discovery reconcile path runs (`resolve_entry_with_parse_cache`). + // This is the ONE closure authority the arming and the loader now share (D0.3 / + // the #6985 Class-B root, third appearance): the prior CHEAP + // `selection_closure_live_paths_with_facts` walked only `selection_adjacency` + // (import edges + strict reference edges for import-LESS files), so it OMITTED + // modules the loader reaches via qualified-projection references emitted by + // import-BEARING files. Such a module, being present in some OTHER entry's global + // refcount, was counted neither `RetentionUnknown` nor re-evicted after it was + // re-cached on load — an invisible resident leak. Consuming the loader's own output + // makes the armed closure ⊇ the reconciled set BY CONSTRUCTION, not by two closures + // happening to agree (the doc's "name the one closure authority, don't add a third + // adjacency"). Cost is neutral, not the front-load the old comment feared: the pool + // loader MEMOIZES into `entry_closure_sources`, so this pre-load is exactly what + // discovery would compute and is reused on the entry's resolve, never doubled. Keyed + // by `workspace_relative_repo_path(&sf.path)` — the identical form the reconcile + // record site derives from `resolved.module.span.file` (`decl_file`), so refcount + // keys and record keys still coincide. A closure that cannot be loaded is skipped: + // its modules become counted `RetentionUnknown` at reconcile (retained — §5 + // fail-closed), so arming still never fails the run. + let paths = match load_sources_for_entry_with_pool(index, &row.entry) { + Ok(sources) => sources + .iter() + .map(|sf| workspace_relative_repo_path(&sf.path)) + .collect::>(), + Err(_) => continue, + }; per_entry.push((row.entry.clone(), paths)); } let evict_enabled = schedule_retention_evict_enabled(); @@ -6348,10 +6387,10 @@ fn index_schedule_entry_completed( entry: &str, subject: Option<&str>, ) -> Result<(), String> { - let batch = { + let (batch, evict_enabled) = { let mut slot = index.schedule_retention.borrow_mut(); match slot.as_mut() { - Some(sr) => sr.entry_completed(entry)?, + Some(sr) => (sr.entry_completed(entry)?, sr.evict_enabled()), None => return Ok(()), } }; @@ -6376,13 +6415,17 @@ fn index_schedule_entry_completed( // Drop the completed entry's assembled graph. The entry's own `InterpContext` still // holds it until the next resolve, so this only removes the memo's pin; a rare later // entry with the identical closure re-resolves (a memo miss, cost only). + // Gate the resolved-graph pin drop on the SAME eviction switch as the per-module drops + // above: with GUNBC_SCHEDULE_RETENTION_EVICT=0 the retain-all measurement pole must + // retain EVERYTHING — graphs included — so it faithfully reproduces pre-M2 peak + // retention (D0.4). Before this the pole understated peak by silently freeing graphs. let graph_evicted = match subject { - Some(subj) => index + Some(subj) if evict_enabled => index .resolved_graph_memo .borrow_mut() .remove(subj) .is_some(), - None => false, + _ => false, }; let (sched_evictions, retention_unknown, graph_evictions) = { let mut slot = index.schedule_retention.borrow_mut(); @@ -6579,9 +6622,9 @@ mod schedule_retention_red_controls { } /// END-TO-END WIRING (real index, real closure computation, real cache eviction): - /// two entries sharing one library module. Arm from the schedule, resolve both - /// entries (populating the process-shared caches through the actual reconcile - /// path that records each module), then drive per-entry completion. The + /// two entries sharing one library module. PREWARM the caches, arm from the schedule + /// AFTER prewarming, then re-resolve both entries through reconcile's all-hits probe + /// (the D0.2 registration path), then drive per-entry completion. The /// shared module survives entry A's completion (still reachable by B) and drops /// only when B completes — proving the mechanism frees real per-module state on a /// live `MultiEntryIndex`, not just in the pure bookkeeper above. @@ -6629,9 +6672,24 @@ mod schedule_retention_red_controls { reads_live_tree: false, }, ]; + // PREWARM the per-module typed cache WITHOUT arming — the production order: + // compile-clean warms the shared index before discovery arms retention. Then clear + // the per-entry resolved-graph memo (D0.1 makes compile-clean's warming Ephemeral: + // modules cached, no per-entry graph pin) so the arm-time re-resolves MISS the memo + // and go through reconcile's ALL-HITS probe — the exact path D0.2 fixed. The + // pre-D0.4 order (arm, THEN first resolve) sent every module cold through the slow + // recording path and never exercised the probe, so the all-hit no-registration + // defect passed green: the §7 order-blindness this control now closes. + super::resolve_entry_with_index(&index, &entry_a).expect("prewarm a"); + super::resolve_entry_with_index(&index, &entry_b).expect("prewarm b"); + super::clear_resolved_graph_memo_for_test(&index); + + // Arm AFTER prewarming, then re-resolve: per-module cache warm + graph memo cold ⇒ + // reconcile takes `try_reconcile_all_cache_hits`, which MUST register each hit's + // schedule keys (D0.2) or the completions below evict nothing and this test reds. super::index_arm_schedule_retention(&index, &rows); - super::resolve_entry_with_index(&index, &entry_a).expect("resolve a"); - super::resolve_entry_with_index(&index, &entry_b).expect("resolve b"); + super::resolve_entry_with_index(&index, &entry_a).expect("resolve a (all-hit)"); + super::resolve_entry_with_index(&index, &entry_b).expect("resolve b (all-hit)"); // Both closures reconciled → all three modules cached, and each entry's // assembled ResolvedGraph is memoized (the strong-Rc pin Fact #4 drops). @@ -6680,6 +6738,80 @@ mod schedule_retention_red_controls { let _ = std::fs::remove_dir_all(&dir); } + + /// RED (D0.4 retain-all baseline on a REAL index): with eviction DISABLED the pole + /// must retain EVERYTHING a completion would otherwise drop — the per-module caches + /// AND the assembled resolved graph. Before the D0.4 production fix the graph pin was + /// dropped UNCONDITIONALLY, so the "retain-all" baseline silently understated peak + /// retention (an invalid pre-M2 measurement pole). Installs a disabled retention + /// directly rather than racing the process-global `GUNBC_SCHEDULE_RETENTION_EVICT`. + #[test] + fn schedule_eviction_disabled_retains_resolved_graph_on_real_index() { + let dir = super::workspace_root() + .join("target") + .join(format!("sched_ret_probe_{}_retainall", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir probe corpus"); + std::fs::write( + dir.join("shared_lib.dag"), + "module sched_ret_ra.shared_lib\n\ndata shared_val: Int = 7\n", + ) + .unwrap(); + std::fs::write( + dir.join("entry_a.dag"), + "module sched_ret_ra.entry_a\n\nimport sched_ret_ra.shared_lib { shared_val }\n\ndata a_val: Int = shared_val\n", + ) + .unwrap(); + let root = dir.to_string_lossy().to_string(); + let entry_a = dir.join("entry_a.dag").to_string_lossy().to_string(); + let index = super::build_multi_entry_index(&[root]); + + // Prewarm the per-module cache, then clear the graph memo (Ephemeral warming). + super::resolve_entry_with_index(&index, &entry_a).expect("prewarm a"); + super::clear_resolved_graph_memo_for_test(&index); + + // Arm with eviction DISABLED, keyed on the loader's exact closure (D0.3 authority). + let paths = super::load_sources_for_entry_with_pool(&index, &entry_a) + .unwrap() + .iter() + .map(|sf| super::workspace_relative_repo_path(&sf.path)) + .collect::>(); + super::install_schedule_retention_for_test(&index, vec![(entry_a.clone(), paths)], false); + + // Re-resolve so the all-hits probe records keys under the disabled retention, then + // snapshot the resident state right before completion. + super::resolve_entry_with_index(&index, &entry_a).expect("resolve a (all-hit)"); + let typed_before = super::typed_module_cache_len_for_test(&index); + let graphs_before = super::index_retention_snapshot(&index).resolved_graph_memo_entries; + assert!( + graphs_before >= 1, + "entry_a's graph memoized before completion" + ); + let subj_a = super::subject_digest_for_closure( + &super::load_sources_for_entry_with_pool(&index, &entry_a).unwrap(), + ); + + // Complete the entry: the disabled pole must drop NOTHING — not the per-module + // caches, not the resolved graph. + super::index_schedule_entry_completed(&index, &entry_a, Some(&subj_a)).expect("complete a"); + assert_eq!( + super::typed_module_cache_len_for_test(&index), + typed_before, + "retain-all pole: per-module cache must be unchanged" + ); + assert_eq!( + super::index_retention_snapshot(&index).resolved_graph_memo_entries, + graphs_before, + "retain-all pole: the resolved GRAPH must be retained too (D0.4) — before the \ + production fix a completion dropped it unconditionally" + ); + assert_eq!( + super::index_retention_snapshot(&index).schedule_evictions, + 0, + "disabled pole counts zero evictions" + ); + let _ = std::fs::remove_dir_all(&dir); + } } /// Interim width=1 floor-drain retention (v1-run-stability throughline, parent @@ -7197,7 +7329,26 @@ fn resolve_entry_with_parse_cache( let load_started = std::time::Instant::now(); let sources = load_sources_for_entry_with_pool(index, entry_file)?; resolve_stage_slot_add(|s| s.load += load_started.elapsed().as_nanos()); - resolved_graph_from_sources_with_index(index, sources, typecheck_gate, entry_file) + resolved_graph_from_sources_with_index( + index, + sources, + typecheck_gate, + entry_file, + ResolvedGraphMemoShare::Memoize, + ) +} + +/// Whether an assembled closure graph joins the in-process share tier +/// (`resolved_graph_memo`). Per-entry discovery resolves `Memoize` so a re-resolve of the +/// same subject is served by reference (the ReferenceTier). The compile-clean whole-tree +/// gate resolves `Ephemeral`: its aggregate graph strong-`Rc`-pins every `TypedModule` in +/// the tree and is never re-hit by discovery's smaller per-entry subjects, so memoizing it +/// is the 9.2GB-class resident-retention leak D0.1 removes (ci-two-tier §5). The per-module +/// `typed_module_cache` warming that IS the gate's purpose happens in reconcile regardless. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ResolvedGraphMemoShare { + Memoize, + Ephemeral, } /// The sources-taking core of `resolve_entry_with_parse_cache`: parse → resolve → @@ -7224,6 +7375,7 @@ fn resolved_graph_from_sources_with_index( sources: Vec>, typecheck_gate: ResolveTypecheckGate, phase_label: &str, + memo_share: ResolvedGraphMemoShare, ) -> Result< ( Rc, @@ -7245,13 +7397,19 @@ fn resolved_graph_from_sources_with_index( if let Some(cache_root) = resolved_graph_cache_root_from_env() { match cross_process_lookup(&cache_root, &subject) { CacheLookupResult::Hit(hit) => { - eprintln!( - "[resolved-graph-cache] decode subject={subject} (installed into process share)" - ); - index - .resolved_graph_memo - .borrow_mut() - .insert(subject, (hit.graph.clone(), hit.source_indices.clone())); + if memo_share == ResolvedGraphMemoShare::Memoize { + eprintln!( + "[resolved-graph-cache] decode subject={subject} (installed into process share)" + ); + index + .resolved_graph_memo + .borrow_mut() + .insert(subject, (hit.graph.clone(), hit.source_indices.clone())); + } else { + eprintln!( + "[resolved-graph-cache] decode subject={subject} (ephemeral gate resolve — not shared)" + ); + } return Ok((hit.graph, hit.source_indices)); } CacheLookupResult::RejectedHit(_) | CacheLookupResult::Miss => {} @@ -7427,11 +7585,18 @@ fn resolved_graph_from_sources_with_index( } resolve_stage_slot_add(|s| s.ownership += ownership_started.elapsed().as_nanos()); - // Install into the in-process share so same-subject re-resolves skip assembly. - index - .resolved_graph_memo - .borrow_mut() - .insert(subject.clone(), (typed.clone(), source_indices.clone())); + // Install into the in-process share so same-subject re-resolves skip assembly — + // UNLESS this is an Ephemeral gate resolve (the compile-clean whole-tree gate): its + // aggregate graph strong-Rc-pins every TypedModule in the tree and is never re-hit by + // discovery's per-entry subjects, so memoizing it is the 9.2GB-class resident-retention + // leak D0.1 removes (ci-two-tier §5). Per-module typed-cache warming already happened + // above, in reconcile, and is unaffected. + if memo_share == ResolvedGraphMemoShare::Memoize { + index + .resolved_graph_memo + .borrow_mut() + .insert(subject.clone(), (typed.clone(), source_indices.clone())); + } if let Some(cache_root) = resolved_graph_cache_root_from_env() { // A failed store write is a disclosed refusal, never a silent shrug — // the swallowed error hid that big closures never landed on disk (only @@ -7754,6 +7919,15 @@ fn try_reconcile_all_cache_hits( let Some(tc_result) = index_get_typed(index, &typed_key)? else { return Ok(None); }; + // Record this confirmed hit's cache keys with the armed schedule retention + // (idempotent; no-op when unarmed) — the all-hits PROBE must register keys just + // like the slow reconcile loop at `reconcile_with_typed_cache`, else a prewarmed + // all-hit run (compile-clean warms the index, deliberately making all-hit the + // common case) arms retention that references NOTHING: completion then reports + // evictions while removing nothing (D0.2 retention-truth fix, ci-two-tier §5). + // Same key forms as the slow path — `decl_file` for the schedule refcount, the + // raw `span.file` for the parse/normalize/ownership/source-hash caches. + index_record_schedule_module(index, &decl_file, &typed_key, &resolved.module.span.file); note_interface_hash(&mut interface_hash_by_name, mod_name, &tc_result); results[slot] = Some(tc_result); progressed = true; From 14682ee2fa5827140b4fafc790aaa45471544c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 13:29:02 +0000 Subject: [PATCH 02/39] D3 mechanism: two-tier CI placement axis (PrTier | Gauntlet) with fail-closed admission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The placement-axis half of ci-two-tier-placement-redesign.md D3, buildable ahead of the D2 srv warm-cost probe. Placement is modeled as DATA (Placement = PrTier | Gauntlet), never per-site prose, with a FAIL-CLOSED admission law: a check is admissible as PrTier only with (a) a measured warm-cost receipt within the fast-lane budget AND (b) a hermetic/ephemeral classification; unmeasured or unclassified => inadmissible as PrTier, so its only valid placement is Gauntlet. No row rides the fast path by taste. The 5s threshold is REUSED from the single fast-lane authority (gunbc_ci_fast_lane_eval_budget_ms, v2.workflow.ci_floor_plan) — never a second 5s definition (DESIGN §3). Verified green by execution (claim_batch, 4 witnesses): - Gauntlet always admissible - a within-budget hermetic PrTier admissible - RED control: an over-budget (6000ms > 5s) PrTier is refused - RED control: an unclassified PrTier is refused The controls pin the threshold discriminatingly (1600ms admits, 6000ms refuses) and the classification gate (Hermetic admits, Unclassified refuses). Deferred to PR-1 (D2-gated): filling the roster of real checks with measured receipts (flipping rows to PrTier as srv warm-cost lands), wiring the law onto the live check rows, and the Gauntlet workflow split (D3b). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- .../test/claim/ci_placement_witness_test.dag | 34 ++++++++++++++ src/v2/workflow/ci_placement.dag | 44 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/v2/test/claim/ci_placement_witness_test.dag create mode 100644 src/v2/workflow/ci_placement.dag diff --git a/src/v2/test/claim/ci_placement_witness_test.dag b/src/v2/test/claim/ci_placement_witness_test.dag new file mode 100644 index 00000000000..476c0d72dcb --- /dev/null +++ b/src/v2/test/claim/ci_placement_witness_test.dag @@ -0,0 +1,34 @@ +module v2.test.claim.ci_placement_witness + +import std.logic { Bool } +import v2.workflow.ci_placement { + Placement, PrTier, Gauntlet, + PlacementClassification, Hermetic, Unclassified, + WarmCostReceipt, + placement_is_admissible +} + +test fn ci_placement_gauntlet_always_admissible_holds() -> Bool { + placement_is_admissible(p: Gauntlet { reason: "awaiting D2 warm-cost measurement" }) +} + +test fn ci_placement_valid_pr_tier_admissible_holds() -> Bool { + placement_is_admissible(p: PrTier { + warm_cost: WarmCostReceipt { measured_ms: 1600, run_ref: "synthetic-warm-receipt" }, + classification: Hermetic + }) +} + +test fn ci_placement_over_budget_pr_tier_refused_holds() -> Bool { + !placement_is_admissible(p: PrTier { + warm_cost: WarmCostReceipt { measured_ms: 6000, run_ref: "synthetic-over-budget" }, + classification: Hermetic + }) +} + +test fn ci_placement_unclassified_pr_tier_refused_holds() -> Bool { + !placement_is_admissible(p: PrTier { + warm_cost: WarmCostReceipt { measured_ms: 100, run_ref: "synthetic-unclassified" }, + classification: Unclassified + }) +} diff --git a/src/v2/workflow/ci_placement.dag b/src/v2/workflow/ci_placement.dag new file mode 100644 index 00000000000..ab31b789c80 --- /dev/null +++ b/src/v2/workflow/ci_placement.dag @@ -0,0 +1,44 @@ +module v2.workflow.ci_placement + +import std.logic { Bool } +import std.nat { Nat } +import v2.std.text { String } +import v2.workflow.ci_floor_plan { gunbc_ci_fast_lane_eval_budget_ms } + +data ci_placement_law_note: String = "The two-tier placement axis (ci-two-tier-placement-redesign.md D3, operator 2026-07-24): a check rides the PR path iff its measured warm cost is within the fast-lane budget OR its cost is proportional to the diff; everything else is a Gauntlet row (main pushes + the 4h falsifier cadence). Placement is DATA (PrTier | Gauntlet), never per-site prose. Admission is FAIL-CLOSED by construction: PrTier is admissible ONLY with (a) a measured warm-cost receipt within the fast-lane budget and (b) a hermetic/ephemeral classification; an unmeasured or unclassified check is inadmissible as PrTier and its only valid placement is Gauntlet — no row rides the fast path by taste. The 5s threshold is REUSED from the single fast-lane authority (gunbc_ci_fast_lane_eval_budget_ms, v2.workflow.ci_floor_plan gunbc_ci_fast_lane_rule_note), never a second 5s definition. The roster of real checks and their PrTier flips are FILLED from the D2 warm-cost probe on fleet srv hardware; until a check has a measured receipt it stays Gauntlet, so this mechanism is correct-by-construction with an empty PrTier set. Wiring the placement onto the live check rows (gates, ci-job steps, floor batches) and the Gauntlet workflow split ride PR-1 (D3b)." + +type PlacementClassification + = Hermetic + | Ephemeral + | LiveTreeReading + | Unclassified + +type WarmCostReceipt { + measured_ms: Nat + run_ref: String +} + +type Placement + = PrTier { warm_cost: WarmCostReceipt, classification: PlacementClassification } + | Gauntlet { reason: String } + +fn classification_admits_pr_tier(c: PlacementClassification) -> Bool { + match c { + Hermetic => true + Ephemeral => true + LiveTreeReading => false + Unclassified => false + } +} + +fn warm_cost_within_fast_lane(wc: WarmCostReceipt) -> Bool { + wc.measured_ms <= gunbc_ci_fast_lane_eval_budget_ms() +} + +fn placement_is_admissible(p: Placement) -> Bool { + match p { + Gauntlet { reason: _ } => true + PrTier { warm_cost: wc, classification: c } => + warm_cost_within_fast_lane(wc: wc) && classification_admits_pr_tier(c: c) + } +} From 488d982bfeb184bd8957459a694528b4c26dd75e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 15:21:49 +0000 Subject: [PATCH 03/39] D5 DiffBaseline: dissolve the origin/main literal into a typed single authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ci-two-tier-placement-redesign.md §11. Introduces DiffBaseline (MergeTarget | PushParent | OperatorOverride{ref}) as the single authority for "which git ref a diff/merge selects against", grounded on the extdeps.git atoms (GitRef, git_remote_ref_parts). resolve_diff_baseline is a pure, fail-closed fold over injected env values — a PushParent with no parent ref REFUSES rather than fabricating a ref. Live consequence fixed (site 1, floor selection): a stacked PR now selects against its real merge target (origin/$GITHUB_BASE_REF), not origin/main. DiffPolicy.base becomes a DiffBaseline; floor_diff_observe.floor_resolved_base resolves it at eval time from GITHUB_BASE_REF, fail-closed to UnifiedDiffFail (the floor widens to the full corpus) on an unresolvable base — never a silent wrong selection. Fork dissolution (no behavior change) — sites 2/3/4 re-ground the same literal onto the authority: merge_admission_produce (merges into main), roadmap_dispatch_actuator (branches from main), ci_workflow Push/PR triggers (main). The ci_merge_base_ref alias and the dead ci_merge_base_diff_range are deleted. Also carries the parked miscite fix (rust_tests_removed_disposition -> commit_gate_rust_suite_removed_disposition) in DESIGN.md / design_document.dag / ci_spec.dag, per plan §3.3. Verified by execution: ci_diff_baseline_witness_test (6/6, incl. the discriminating stacked-PR pair and the PushParent-refuses fail-closed control); ci_spec_witnesses (fetch renders "origin $GITHUB_BASE_REF"; single authority); roadmap_dispatch_actuator_witnesses. ci.yml + DESIGN.md regenerated via main_wet (drift clean; the two floor/regen fetch lines now expand $GITHUB_BASE_REF). floor_diff_observe_witness_test runs green in the floor's wet mode; its eval-time env reads are unmockable under claim_batch strict-hermetic, a PRE-EXISTING harness limitation confirmed by a clean-tree stash run of the same witness (this change adds no new hermetic red). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- .github/workflows/ci.yml | 4 +- DESIGN.md | 2 +- dag/gunbc/ci_diff_defaults.dag | 81 ++++++++++++++++- dag/gunbc/ci_spec.dag | 20 ++-- dag/gunbc/ci_workflow.dag | 5 +- dag/gunbc/design_document.dag | 2 +- dag/gunbc/merge_admission_produce.dag | 4 +- dag/gunbc/roadmap_dispatch_actuator.dag | 5 +- .../claim/ci_diff_baseline_witness_test.dag | 91 +++++++++++++++++++ src/v2/test/claim/ci_spec_witness_test.dag | 25 +++-- .../floor_diff_observe_witness_test.dag | 5 +- src/v2/workflow/floor_diff_observe.dag | 90 +++++++++++++----- 12 files changed, 278 insertions(+), 56 deletions(-) create mode 100644 src/v2/test/claim/ci_diff_baseline_witness_test.dag diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c4f1ca6b7..5c781226d0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: - name: gunbc ci regen (self-host fixed-point — required) run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) - git fetch --no-tags origin main 2>/dev/null || true + git fetch --no-tags origin $GITHUB_BASE_REF 2>/dev/null || true # Affected-set-scoped regen admission (pull_request only): on a pull_request event, regen_floor_skip_witness intersects the merge-base diff with the [src/v1, dag] regen input closure (shared authority cli_run::regen_input_sources — the exact set regen_stage0 compiles; both the RegenVerifyGate and the SelfHostStalenessGate invoke regen_stage0, so the closure covers both). A PR diff touching none of src/v1/** (emitter source + committed stage0 outputs), v1's transitive dag import-closure, or the Cargo/toolchain build config cannot change the self-host fixed-point, so the regen step exits 0 immediately (a required step that passes because the self-host fixed-point is provably unchanged — regen runs before the floor so emitter drift fails fast, with no floor or merge-admission dependency). Empty diff / diff failure / closure failure runs regen (fail-closed). COLD CONTROL: the skip is gated to pull_request events via GITHUB_EVENT_NAME, so push-to-main and workflow_dispatch run regen UNCONDITIONALLY (no witness). A wrong closure that lets a PR wrongly skip therefore surfaces as a counted divergence on the very next merge to main — the squash-merge main-push run has an empty diff and runs regen cold, reding main if the seed is stale (a one-merge acceptance window, the discovery-flip shape). The 4-hourly falsifier does NOT run regen_stage0; the unconditional main-push regen is the cold control. if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then _ci_regen_skip=$("$ROOT/target/release/regen_floor_skip_witness" 2>/dev/null || echo run_regen) @@ -189,7 +189,7 @@ jobs: run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) # REMOVED 2026-07-21 (gunbc#7023 / proud-cat-517): the documentation_only_skip shell shortcut exited before claim_executor, bypassing the witness corpus entirely on docs-only PRs. That bypassed the existing ReadsLiveTree never-predict-skip lane — doc_reachability_witness_test.dag already declared ReadsLiveTree since #6654, but the shell never reached selection. Docs-only PRs now run the normal floor (SelectionApplied): import-closure skips non-live-tree witnesses cheaply; ReadsLiveTree rows (doc-graph wall, corpus-read host-fed lenses) always run. compile_clean_floor_skip_witness remains for dag_compile_clean_scope disposition only — it no longer gates floor admission. - git fetch --no-tags origin main 2>/dev/null || true + git fetch --no-tags origin $GITHUB_BASE_REF 2>/dev/null || true # 🟡 dissolve-on: ci_floor_disposition_marker_init_script — concat-built bash floor step opener stamping floor_running into target/ci-floor-disposition.txt (clears stale documentation_only_skipped on persistent self-hosted runners before docs-only branch); DISSOLVES WHEN bash-emit (#5828 / ROADMAP 6-shell-slice0) realizes floor disposition initialization through orchestration emit and the marker init retires without a transport scaffold # Positive floor-running stamp at the start of every floor step clears stale documentation_only_skipped left in target/ on persistent self-hosted runners (review 37010 / operator ruling 2026-07-11). Receipt gates skip only on an exact documentation_only_skipped match — never on file presence alone. mkdir -p target diff --git a/DESIGN.md b/DESIGN.md index 9f5046664ff..d1ae698e111 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -112,4 +112,4 @@ hollow alias (minimality ≠ grounding) · state-space conflation (an `Option`/` - `cargo test --workspace` · `cargo clippy --all-targets -- -D warnings` · `cargo fmt --all --check` - one-time: `git config core.hooksPath .githooks` (pre-push runs `cargo fmt`) -- CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_batches`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `rust_tests_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser. +- CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_batches`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser. diff --git a/dag/gunbc/ci_diff_defaults.dag b/dag/gunbc/ci_diff_defaults.dag index 63370d5762b..040dd8ca094 100644 --- a/dag/gunbc/ci_diff_defaults.dag +++ b/dag/gunbc/ci_diff_defaults.dag @@ -1,3 +1,82 @@ module gunbc.ci_diff_defaults -data ci_merge_base_ref: String = "origin/main" +import std.types { String, GitRef } +import extdeps.git { GitRemoteRefParts, git_remote_ref_parts } + +data ci_default_branch_name: String = "main" + +data ci_diff_remote_name: String = "origin" + +data ci_default_baseline_ref: GitRef = "origin/main" + +data diff_baseline_law_note: String = "DiffBaseline is the single authority for the fact 'which git ref a diff/merge selects against' (ci-two-tier-placement-redesign.md §11, D5, operator 2026-07-24). It dissolves the bare origin/main literal that four sites forked (ci_spec diff_policy, merge_admission_produce, dispatch_git_remote_ref, ci_workflow Push). Three variants separate POLICY from the resolved ref: MergeTarget — the branch a change merges into (a pull_request's GITHUB_BASE_REF, else the default branch); PushParent — a push event's parent ref; OperatorOverride — a declared static ref (regen, dispatch worktrees, tools), never an env toggle (§5 no escape hatches). resolve_diff_baseline is a PURE fold over injected env values, so the stacked-PR fix (base = origin/GITHUB_BASE_REF, not origin/main) is witnessed by execution without reading the host env; the eval-time wrapper (floor_diff_observe.floor_resolved_base) reads the env and calls it, fail-closed — a PushParent baseline with no parent ref REFUSES rather than fabricating a ref, and the floor treats an unresolvable base as UnifiedDiffFail (widen to full corpus), never a silent wrong selection. Grounded on the extdeps.git atoms (GitRef, git_remote_ref_parts, git_diff_range_argv) — no new string vocabulary. MergeTarget with GITHUB_BASE_REF absent resolves to the default branch (origin/main): this is not a §5 absorbing fallback because a pull_request event always sets GITHUB_BASE_REF, so the fallback only fires on push/local where origin/main IS the intended base and the floor runs the full corpus anyway." + +type DiffBaseline + = MergeTarget + | PushParent + | OperatorOverride { ref: GitRef } + +type DiffBaselineResolution + = DiffBaselineResolved { ref: GitRef } + | DiffBaselineRefused { cause: String } + +data ci_default_diff_baseline: DiffBaseline = MergeTarget + +fn merge_target_ref_from_base_ref(github_base_ref: String) -> GitRef { + concat(ci_diff_remote_name, "/", github_base_ref) as GitRef +} + +fn resolve_diff_baseline( + baseline: DiffBaseline, + github_base_ref: String?, + push_parent_ref: String? +) -> DiffBaselineResolution { + match baseline { + OperatorOverride { ref: r } => DiffBaselineResolved { ref: r } + MergeTarget => + match github_base_ref { + Present { value: br } => + if br == "" { + DiffBaselineResolved { ref: ci_default_baseline_ref } + } else { + DiffBaselineResolved { ref: merge_target_ref_from_base_ref(github_base_ref: br) } + } + Absent => DiffBaselineResolved { ref: ci_default_baseline_ref } + } + PushParent => + match push_parent_ref { + Present { value: p } => + if p == "" { + DiffBaselineRefused { cause: "PushParent baseline: push parent ref present but empty" } + } else { + DiffBaselineResolved { ref: p as GitRef } + } + Absent => DiffBaselineRefused { cause: "PushParent baseline requires a push parent ref; absent (not a push context?)" } + } + } +} + +fn diff_baseline_resolution_ref_or(resolution: DiffBaselineResolution, fallback: GitRef) -> GitRef { + match resolution { + DiffBaselineResolved { ref: r } => r + DiffBaselineRefused { cause: _ } => fallback + } +} + +data merge_target_fetch_ref_shell_note: String = "The MergeTarget fetch renders as git fetch --no-tags origin $GITHUB_BASE_REF — an UNQUOTED bash variable reference (git_shell_join_argv joins argv with spaces, no quoting), so it resolves at ci.yml runtime: a pull_request fetches its actual base branch (a stacked PR fetches origin/), and a push/local run where GITHUB_BASE_REF is empty falls through to git fetch origin (the remote default). Bare $GITHUB_BASE_REF is used, never the dollar-brace default form, because a dollar-brace in a .dag string triggers string interpolation; the eval-time default-to-main is handled separately by floor_diff_observe.floor_resolved_base / resolve_diff_baseline. This is the shell REALIZATION of MergeTarget's fetch, not new modeled vocabulary — the eval-time diff base is resolved separately by reading the same GITHUB_BASE_REF (§4 one grammar; shell transport is a realization handler)." + +data merge_target_fetch_ref_shell_expansion: String = "$GITHUB_BASE_REF" + +fn diff_baseline_fetch_remote_ref(baseline: DiffBaseline) -> GitRemoteRefParts { + match baseline { + MergeTarget => GitRemoteRefParts { + remote: ci_diff_remote_name, + ref_name: merge_target_fetch_ref_shell_expansion + } + OperatorOverride { ref: r } => git_remote_ref_parts(ref: r) + PushParent => GitRemoteRefParts { + remote: ci_diff_remote_name, + ref_name: ci_default_branch_name + } + } +} diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index ec9df572d6a..a6e0218ca70 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -23,7 +23,7 @@ import gunbc.commit_workflow { project_ci_floor_witness_entries } import std.realization_schedule { ScheduleWitnessEntry } -import gunbc.ci_diff_defaults { ci_merge_base_ref } +import gunbc.ci_diff_defaults { DiffBaseline, ci_default_diff_baseline, diff_baseline_fetch_remote_ref } import gunbc.generated_artifact { committed_generated_artifact_paths } import gunbc.ci_failure_class { infra_retry_grep_alternation } import gunbc.ci_deploy_access { DeployAccess, ci_deploy_srv1_access } @@ -34,7 +34,6 @@ import gunbc.merge_admission_produce { ci_repo_root_shell, } import extdeps.git { - git_remote_ref_parts, git_fetch_no_tags_shell, } import std.types { GitRef } @@ -76,7 +75,7 @@ data gunbc_ci_floor_batch_stop_policy_claim_executor_seed_disposition: Dispositi } type DiffPolicy { - base: String + base: DiffBaseline head: String mode: DiffMode } @@ -131,7 +130,7 @@ data gunbc_ci_spec: CiSpec = { witness_entries: gunbc_ci_floor_witness_entries, discovery_scan_dirs: witness_discovery_scan_dirs, diff_policy: { - base: ci_merge_base_ref, + base: ci_default_diff_baseline, head: "HEAD", mode: DiffMergeBase }, @@ -144,7 +143,7 @@ data gunbc_ci_regen_spec: CiSpec = { witness_entries: [], discovery_scan_dirs: [], diff_policy: { - base: ci_merge_base_ref, + base: ci_default_diff_baseline, head: "HEAD", mode: DiffMergeBase }, @@ -194,7 +193,7 @@ fn ci_release_build_line() -> String { ) } -data ci_fmt_gate_note: String = "cargo fmt --all --check, RE-ENROLLED in CI 2026-07-15 (operator ruling, this PR) as a standalone build-job step — NOT a RustMonolithGate re-enrollment, which gunbc.commit_workflow.rust_tests_removed_disposition rejects and this row does not touch. That ruling (2026-07-11) removed the fmt+nextest bundle for reasons that are all facts about NEXTEST — ~37 GiB, red on main, non-required, a permanent tolerated red (DESIGN 5) — and fmt was collateral damage of a DESIGN 3 fusion: tools.rust_gates_ci.run_gates welds two separable facts (fmt: 4.3s measured, parse-only, no build, green; nextest: 37 GiB compile+run, red) into one ProcessExit, so killing the bundle killed the cheap green half too. The ruling's declared replacement coverage — the pre-push hook (CommitCargoFmtCheck on GitPrePushHook) — is an escape hatch (DESIGN 5: opt-in per clone via a manual core.hooksPath config, bypassable with --no-verify, absent in container worktrees) and is PROVEN ineffective: #6658 landed an unformatted .rs on main 2026-07-15 and nothing caught it; the falsifier lane found it only by running fmt by hand. Placement: FIRST step of the build job, before the ~33min release build, so a 4-second violation fails in 4 seconds rather than after the build; build is not protection-required itself but ci needs:[build], so a red here blocks the required job by construction. Cost: 4.3s measured whole-workspace, on a toolchain that already installs the rustfmt component in ci_prelude_steps. 🟡 dissolve-on: the fmt spelling here is a shell-transport realization of extdeps.cargo_build cargo.Build.Fmt (the same operation tools.rust_gates_ci calls through the effect model); it collapses onto that single authority when the ci.yml emit seam can bind a typed cargo operation instead of a hand-spelled argv line — the same de-fork ci_release_build_line awaits. Retires WITH the v1 seed at DESIGN 7 terminal collapse, when no .rs remains to format." +data ci_fmt_gate_note: String = "cargo fmt --all --check, RE-ENROLLED in CI 2026-07-15 (operator ruling, this PR) as a standalone build-job step — NOT a RustMonolithGate re-enrollment, which gunbc.commit_workflow.commit_gate_rust_suite_removed_disposition rejects and this row does not touch. That ruling (2026-07-11) removed the fmt+nextest bundle for reasons that are all facts about NEXTEST — ~37 GiB, red on main, non-required, a permanent tolerated red (DESIGN 5) — and fmt was collateral damage of a DESIGN 3 fusion: tools.rust_gates_ci.run_gates welds two separable facts (fmt: 4.3s measured, parse-only, no build, green; nextest: 37 GiB compile+run, red) into one ProcessExit, so killing the bundle killed the cheap green half too. The ruling's declared replacement coverage — the pre-push hook (CommitCargoFmtCheck on GitPrePushHook) — is an escape hatch (DESIGN 5: opt-in per clone via a manual core.hooksPath config, bypassable with --no-verify, absent in container worktrees) and is PROVEN ineffective: #6658 landed an unformatted .rs on main 2026-07-15 and nothing caught it; the falsifier lane found it only by running fmt by hand. Placement: FIRST step of the build job, before the ~33min release build, so a 4-second violation fails in 4 seconds rather than after the build; build is not protection-required itself but ci needs:[build], so a red here blocks the required job by construction. Cost: 4.3s measured whole-workspace, on a toolchain that already installs the rustfmt component in ci_prelude_steps. 🟡 dissolve-on: the fmt spelling here is a shell-transport realization of extdeps.cargo_build cargo.Build.Fmt (the same operation tools.rust_gates_ci calls through the effect model); it collapses onto that single authority when the ci.yml emit seam can bind a typed cargo operation instead of a hand-spelled argv line — the same de-fork ci_release_build_line awaits. Retires WITH the v1 seed at DESIGN 7 terminal collapse, when no .rs remains to format." fn ci_fmt_gate_line() -> String { "\"$CARGO_BIN\" fmt --all --check" @@ -322,15 +321,8 @@ fn join_slash(parts: List) -> String { data ci_documentation_only_floor_shortcut_retired_note: String = "REMOVED 2026-07-21 (gunbc#7023 / proud-cat-517): the documentation_only_skip shell shortcut exited before claim_executor, bypassing the witness corpus entirely on docs-only PRs. That bypassed the existing ReadsLiveTree never-predict-skip lane — doc_reachability_witness_test.dag already declared ReadsLiveTree since #6654, but the shell never reached selection. Docs-only PRs now run the normal floor (SelectionApplied): import-closure skips non-live-tree witnesses cheaply; ReadsLiveTree rows (doc-graph wall, corpus-read host-fed lenses) always run. compile_clean_floor_skip_witness remains for dag_compile_clean_scope disposition only — it no longer gates floor admission." -fn ci_merge_base_diff_range(policy: DiffPolicy) -> String { - match policy.mode { - DiffMergeBase => concat(concat(policy.base, "..."), policy.head) - DiffTwoDot => concat(concat(policy.base, ".."), policy.head) - } -} - fn git_fetch_script(policy: DiffPolicy) -> String { - let parts = git_remote_ref_parts(ref: policy.base as GitRef) + let parts = diff_baseline_fetch_remote_ref(baseline: policy.base) git_fetch_no_tags_shell(remote: parts.remote, ref_name: parts.ref_name, ignore_failure: true) } diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 6619edabc8e..62650d18310 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -16,6 +16,7 @@ import gunbc.ci_spec { import gunbc.merge_admission_produce { ci_merge_admission_gate_script, } +import gunbc.ci_diff_defaults { ci_default_branch_name } import gunbc.ci_workflow_expressions { ci_deploy_push_to_main_if, ci_regen_heal_if } import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates, project_ci_floor_witness_entries } import gunbc.ci_runner_target { gunbc_ci_selected_runner_spec, ci_runner_target_ram_speed_budget, selected_ci_runner_target } @@ -633,9 +634,9 @@ data ci_workflow: Workflow = { name: "ci", on: [ WorkflowDispatch { inputs: [] }, - Push { branches: ["main"], paths: [] }, + Push { branches: [ci_default_branch_name], paths: [] }, PullRequest { - branches: ["main"], + branches: [ci_default_branch_name], types: [Opened, Synchronize, Reopened, ReadyForReview] }, MergeGroup diff --git a/dag/gunbc/design_document.dag b/dag/gunbc/design_document.dag index 5d220ca896d..dc07ad8c10c 100644 --- a/dag/gunbc/design_document.dag +++ b/dag/gunbc/design_document.dag @@ -169,7 +169,7 @@ fn building_checks_blocks() -> List { ul(items: [ li(text: "`cargo test --workspace` · `cargo clippy --all-targets -- -D warnings` · `cargo fmt --all --check`"), li(text: "one-time: `git config core.hooksPath .githooks` (pre-push runs `cargo fmt`)"), - li(text: "CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_batches`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `rust_tests_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser."), + li(text: "CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_batches`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser."), ]), ] } diff --git a/dag/gunbc/merge_admission_produce.dag b/dag/gunbc/merge_admission_produce.dag index b6d62e3de42..21555c70d8d 100644 --- a/dag/gunbc/merge_admission_produce.dag +++ b/dag/gunbc/merge_admission_produce.dag @@ -3,7 +3,7 @@ module gunbc.merge_admission_produce import std.types { ContentHash, CommitSha, String, Int, List, Bool, GitRef } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } -import gunbc.ci_diff_defaults { ci_merge_base_ref } +import gunbc.ci_diff_defaults { ci_default_baseline_ref } import extdeps.git import extdeps.github.checks { CheckConclusion, @@ -20,7 +20,7 @@ import gunbc.merge_admission { } import gunbc.ci_layer_roots { witness_layer_roots, witness_layer_source_flags, witness_layer_source_flags_rooted } -data merge_admission_merge_base_ref: String = ci_merge_base_ref +data merge_admission_merge_base_ref: String = ci_default_baseline_ref as String data merge_admission_receipt_schema: String = "gunbc.merge_admission_receipt.v1" diff --git a/dag/gunbc/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap_dispatch_actuator.dag index f2a48492505..61cfc206727 100644 --- a/dag/gunbc/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap_dispatch_actuator.dag @@ -1,6 +1,7 @@ module gunbc.roadmap_dispatch_actuator import std.types { String, NonEmptyStr, Int, Bool, List, FilePath, GitRef } +import gunbc.ci_diff_defaults { ci_default_baseline_ref } import std.upsert_decision { ObservationVerdict, Converged, @@ -61,7 +62,9 @@ data dispatch_repo: String = "gunbc" data dispatch_worktree_root_note: String = "PROJECTION of gunbc.host_layout.srv1_dispatch_worktree_root — the single path authority shared with the live_deploy membership that provisions the directory (DESIGN §3; its former private copy deleted, belt-B lane 2026-07-20)." data dispatch_worktree_root: String = srv1_dispatch_worktree_root as String -data dispatch_git_remote_ref: String = "origin/main" +data dispatch_git_remote_ref_note: String = "Re-grounded onto the gunbc.ci_diff_defaults single authority (D5, §11): dispatch worktrees branch from the mainline ref. Was a forked origin/main literal (§3 nickname); the fact now lives once in ci_default_baseline_ref. Behavior is unchanged — dispatch always bases on the default branch." + +data dispatch_git_remote_ref: String = ci_default_baseline_ref as String data dispatch_tmux_session_prefix: String = "gunbc-dispatch-" data dispatch_claude_process_fingerprint: NonEmptyStr = "claude" as NonEmptyStr data claude_skip_permission_settings_json: String = "{\"skipDangerousModePermissionPrompt\":true}" diff --git a/src/v2/test/claim/ci_diff_baseline_witness_test.dag b/src/v2/test/claim/ci_diff_baseline_witness_test.dag new file mode 100644 index 00000000000..806d8e8f746 --- /dev/null +++ b/src/v2/test/claim/ci_diff_baseline_witness_test.dag @@ -0,0 +1,91 @@ +module v2.test.claim.ci_diff_baseline_witness + +import std.logic { Bool } +import std.types { GitRef } +import gunbc.ci_diff_defaults { + DiffBaseline, MergeTarget, PushParent, OperatorOverride, + DiffBaselineResolution, DiffBaselineResolved, DiffBaselineRefused, + resolve_diff_baseline, + ci_default_baseline_ref +} + +fn resolved_ref_eq(resolution: DiffBaselineResolution, expected: GitRef) -> Bool { + match resolution { + DiffBaselineResolved { ref: r } => (r as String) == (expected as String) + DiffBaselineRefused { cause: _ } => false + } +} + +fn is_refused(resolution: DiffBaselineResolution) -> Bool { + match resolution { + DiffBaselineResolved { ref: _ } => false + DiffBaselineRefused { cause: _ } => true + } +} + +data stacked_pr_base_branch: String = "feature/other-pr-branch" + +test fn ci_diff_baseline_stacked_pr_selects_real_base_holds() -> Bool { + resolved_ref_eq( + resolution: resolve_diff_baseline( + baseline: MergeTarget, + github_base_ref: Present { value: stacked_pr_base_branch }, + push_parent_ref: Absent + ), + expected: "origin/feature/other-pr-branch" + ) +} + +test fn ci_diff_baseline_stacked_pr_is_not_origin_main_holds() -> Bool { + !resolved_ref_eq( + resolution: resolve_diff_baseline( + baseline: MergeTarget, + github_base_ref: Present { value: stacked_pr_base_branch }, + push_parent_ref: Absent + ), + expected: ci_default_baseline_ref + ) +} + +test fn ci_diff_baseline_merge_target_no_env_defaults_to_main_holds() -> Bool { + resolved_ref_eq( + resolution: resolve_diff_baseline( + baseline: MergeTarget, + github_base_ref: Absent, + push_parent_ref: Absent + ), + expected: ci_default_baseline_ref + ) +} + +test fn ci_diff_baseline_push_parent_resolves_parent_holds() -> Bool { + resolved_ref_eq( + resolution: resolve_diff_baseline( + baseline: PushParent, + github_base_ref: Absent, + push_parent_ref: Present { value: "0123abcd" } + ), + expected: "0123abcd" + ) +} + +test fn ci_diff_baseline_operator_override_round_trips_holds() -> Bool { + resolved_ref_eq( + resolution: resolve_diff_baseline( + baseline: OperatorOverride { ref: "origin/release-2026" }, + github_base_ref: Absent, + push_parent_ref: Absent + ), + expected: "origin/release-2026" + ) +} + +test fn ci_diff_baseline_push_parent_absent_refuses_not_widens_holds() -> Bool { + is_refused( + resolution: resolve_diff_baseline( + baseline: PushParent, + github_base_ref: Absent, + push_parent_ref: Absent + ) + ) +} diff --git a/src/v2/test/claim/ci_spec_witness_test.dag b/src/v2/test/claim/ci_spec_witness_test.dag index e6ec48f345f..86a371f4e81 100644 --- a/src/v2/test/claim/ci_spec_witness_test.dag +++ b/src/v2/test/claim/ci_spec_witness_test.dag @@ -9,7 +9,7 @@ import v2.std.node_query { coproduct_arm_keys, coproduct_nullary_inhabitants } import gunbc.ci_layer_roots { witness_layer_roots, witness_layer_source_flags } import gunbc.ci_spec { CiSpec, DiffPolicy, DiffMergeBase, Gate, - gunbc_ci_spec, gunbc_ci_gates, gunbc_ci_floor_gates, + gunbc_ci_spec, gunbc_ci_regen_spec, gunbc_ci_gates, gunbc_ci_floor_gates, gunbc_ci_regen_floor_gates, ci_release_features, scheduler_invoke, gunbc_ci_workflow_run, gunbc_ci_floor_only_script, git_fetch_script, @@ -17,7 +17,11 @@ import gunbc.ci_spec { ci_documentation_only_floor_shortcut_retired_note, } import v2.workflow.ci_release_build_emit { gunbc_ci_run_script, ci_release_build_script } -import gunbc.ci_diff_defaults { ci_merge_base_ref } +import gunbc.ci_diff_defaults { + ci_default_baseline_ref, + DiffBaseline, MergeTarget, PushParent, OperatorOverride, + merge_target_fetch_ref_shell_expansion +} import gunbc.ci_materialization { ci_floor_resolve_receipt_gate_script, ci_floor_materialization_receipt_gate_script, @@ -47,7 +51,7 @@ data spec_alternate_notice: CiSpec = { witness_entries: [], discovery_scan_dirs: [], diff_policy: { - base: "upstream/dev", + base: OperatorOverride { ref: "upstream/dev" }, head: "HEAD", mode: DiffMergeBase }, @@ -119,7 +123,7 @@ fn witness_scan_subtree_not_in_scheduler_argv() -> Bool { fn witness_diff_policy_drives_fetch() -> Bool { let fetch = git_fetch_script(policy: gunbc_ci_spec.diff_policy) - string_contains(s: fetch, pattern: "git fetch --no-tags origin main") && + string_contains(s: fetch, pattern: concat("git fetch --no-tags origin ", merge_target_fetch_ref_shell_expansion)) && (git_fetch_script(policy: spec_alternate_notice.diff_policy) != fetch) && string_contains( s: git_fetch_script(policy: spec_alternate_notice.diff_policy), @@ -207,9 +211,18 @@ fn witness_documentation_only_floor_shortcut_retired() -> Bool { string_contains(s: script, pattern: "--plan-function gunbc_ci_floor_batches") } +fn base_is_merge_target(baseline: DiffBaseline) -> Bool { + match baseline { + MergeTarget => true + PushParent => false + OperatorOverride { ref: _ } => false + } +} + fn witness_merge_base_ref_single_authority() -> Bool { - merge_admission_merge_base_ref == gunbc_ci_spec.diff_policy.base - && merge_admission_merge_base_ref == ci_merge_base_ref + (merge_admission_merge_base_ref == (ci_default_baseline_ref as String)) + && base_is_merge_target(baseline: gunbc_ci_spec.diff_policy.base) + && base_is_merge_target(baseline: gunbc_ci_regen_spec.diff_policy.base) } fn witness_documentation_only_gate_skip_prefix_dissolve_on_named() -> Bool { diff --git a/src/v2/test/claim/execution/floor_diff_observe_witness_test.dag b/src/v2/test/claim/execution/floor_diff_observe_witness_test.dag index 7e4d245acc6..495c46391ab 100644 --- a/src/v2/test/claim/execution/floor_diff_observe_witness_test.dag +++ b/src/v2/test/claim/execution/floor_diff_observe_witness_test.dag @@ -2,6 +2,7 @@ module v2.test.execution.floor_diff_observe_witness import std.logic { Bool } import gunbc.ci_spec { DiffPolicy, DiffMergeBase } +import gunbc.ci_diff_defaults { OperatorOverride } import v2.workflow.floor_diff_observe { FloorUnifiedDiffResult, UnifiedDiffOk, @@ -28,7 +29,7 @@ fn witness_ci_policy_observation_succeeds() -> Bool { fn witness_invalid_base_fails_closed() -> Bool { match floor_observe_git_diff_unified( policy: DiffPolicy { - base: "__gunbc_invalid_diff_base__", + base: OperatorOverride { ref: "__gunbc_invalid_diff_base__" }, head: "HEAD", mode: DiffMergeBase } @@ -48,7 +49,7 @@ fn witness_ci_policy_name_status_observation_succeeds() -> Bool { fn witness_invalid_base_name_status_fails_closed() -> Bool { match floor_observe_git_diff_name_status( policy: DiffPolicy { - base: "__gunbc_invalid_diff_base__", + base: OperatorOverride { ref: "__gunbc_invalid_diff_base__" }, head: "HEAD", mode: DiffMergeBase } diff --git a/src/v2/workflow/floor_diff_observe.dag b/src/v2/workflow/floor_diff_observe.dag index dd751175070..b81cf4e6872 100644 --- a/src/v2/workflow/floor_diff_observe.dag +++ b/src/v2/workflow/floor_diff_observe.dag @@ -23,6 +23,13 @@ import extdeps.git { git_diff_name_status_field_separator } import extdeps.shell +import gunbc.ci_diff_defaults { + DiffBaseline, + DiffBaselineResolution, + DiffBaselineResolved, + DiffBaselineRefused, + resolve_diff_baseline +} type FloorUnifiedDiffResult = UnifiedDiffOk { text: String } @@ -32,6 +39,10 @@ type FloorNameStatusDiffResult = NameStatusDiffOk { changed_paths: List, departed_paths: List } | NameStatusDiffFail { reason: String } +type FloorBaseResolution + = FloorBaseRef { ref: String } + | FloorBaseUnresolved { cause: String } + fn floor_env_override_or(env_name: NonEmptyStr, default: String) -> String { let env = shell.Env.Get(name: env_name) match env.value { @@ -40,8 +51,31 @@ fn floor_env_override_or(env_name: NonEmptyStr, default: String) -> String { } } -fn floor_resolved_base(policy: DiffPolicy) -> String { - floor_env_override_or(env_name: "GUNBC_CI_DIFF_BASE" as NonEmptyStr, default: policy.base) +fn floor_env_opt(env_name: NonEmptyStr) -> String? { + let env = shell.Env.Get(name: env_name) + match env.value { + Present { value: raw } => if raw == "" { none } else { Present { value: raw } } + Absent => none + } +} + +data floor_resolved_base_note: String = "Eval-time diff base resolution (D5, ci-two-tier-placement-redesign.md §11). Precedence: an explicit GUNBC_CI_DIFF_BASE injection (test fixtures and the workflow channel) wins; otherwise policy.base (a DiffBaseline) resolves against the live GITHUB_BASE_REF / GITHUB_EVENT_BEFORE, so a stacked PR selects its real merge target (origin/GITHUB_BASE_REF), not origin/main. A DiffBaselineRefused (a PushParent with no parent ref) yields FloorBaseUnresolved, which the observe functions turn into UnifiedDiffFail / NameStatusDiffFail — the floor then widens to the full corpus (fail-closed), never a fabricated base ref (§5)." + +fn floor_resolved_base(policy: DiffPolicy) -> FloorBaseResolution { + match floor_env_opt(env_name: "GUNBC_CI_DIFF_BASE" as NonEmptyStr) { + Present { value: injected } => FloorBaseRef { ref: injected } + Absent => { + let resolution = resolve_diff_baseline( + baseline: policy.base, + github_base_ref: floor_env_opt(env_name: "GITHUB_BASE_REF" as NonEmptyStr), + push_parent_ref: floor_env_opt(env_name: "GITHUB_EVENT_BEFORE" as NonEmptyStr) + ) + match resolution { + DiffBaselineResolved { ref: r } => FloorBaseRef { ref: r as String } + DiffBaselineRefused { cause: c } => FloorBaseUnresolved { cause: c } + } + } + } } fn floor_resolved_head(policy: DiffPolicy) -> String { @@ -59,16 +93,20 @@ fn floor_observe_git_diff_unified(policy: DiffPolicy) -> FloorUnifiedDiffResult let unified_injection = shell.Env.Get(name: "GUNBC_CI_DIFF_UNIFIED" as NonEmptyStr) match unified_injection.value { Present { value: injected } => UnifiedDiffOk { text: injected } - Absent => { - let base = floor_resolved_base(policy: policy) - let head = floor_resolved_head(policy: policy) - let result = git.Core.DiffUnified0(base: base, head: head, range: floor_git_diff_range_of_mode(mode: policy.mode)) - if result.success { - UnifiedDiffOk { text: result.diff } - } else { - UnifiedDiffFail { reason: "git diff -U0 exited nonzero: invalid ref or not a git repository" } + Absent => + match floor_resolved_base(policy: policy) { + FloorBaseUnresolved { cause: c } => + UnifiedDiffFail { reason: concat("diff base unresolved (fail-closed to full corpus): ", c) } + FloorBaseRef { ref: base } => { + let head = floor_resolved_head(policy: policy) + let result = git.Core.DiffUnified0(base: base, head: head, range: floor_git_diff_range_of_mode(mode: policy.mode)) + if result.success { + UnifiedDiffOk { text: result.diff } + } else { + UnifiedDiffFail { reason: "git diff -U0 exited nonzero: invalid ref or not a git repository" } + } + } } - } } } @@ -101,20 +139,24 @@ fn floor_observe_git_diff_name_status(policy: DiffPolicy) -> FloorNameStatusDiff match name_status_injection.value { Present { value: injected } => floor_name_status_from_raw(raw: floor_decode_injected_name_status(injected: injected)) - Absent => { - let base = floor_resolved_base(policy: policy) - let head = floor_resolved_head(policy: policy) - let result = git.Core.DiffNameStatus( - base: base, - head: head, - range: floor_git_diff_range_of_mode(mode: policy.mode) - ) - if !result.success { - NameStatusDiffFail { reason: "git diff --name-status -z exited nonzero: invalid ref or not a git repository" } - } else { - floor_name_status_from_raw(raw: result.raw) + Absent => + match floor_resolved_base(policy: policy) { + FloorBaseUnresolved { cause: c } => + NameStatusDiffFail { reason: concat("diff base unresolved (fail-closed to full corpus): ", c) } + FloorBaseRef { ref: base } => { + let head = floor_resolved_head(policy: policy) + let result = git.Core.DiffNameStatus( + base: base, + head: head, + range: floor_git_diff_range_of_mode(mode: policy.mode) + ) + if !result.success { + NameStatusDiffFail { reason: "git diff --name-status -z exited nonzero: invalid ref or not a git repository" } + } else { + floor_name_status_from_raw(raw: result.raw) + } + } } - } } } From bf94e5fe9a63fc80c48f4910873fb6e8e08d58c3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 15:40:20 +0000 Subject: [PATCH 04/39] Progress/observation P0: the event model, five laws, one glyph authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 of the progress-and-observation lane (docs/plans/progress-observation-design.md, operator-signed 2026-07-23; §6b/§6c doctrine from #7169 folded in). Model only — no renderer, no emit site touched. ObservationEvent = subject (a typed containment path: run ⊃ batch ⊃ entry ⊃ module ⊃ phase) × Begin | Step{k,n} | Concluded{outcome} × measured facts. Outcomes are a closed sum with Refused DISTINCT from Failed — the reference implementation conflates them, which is how a deliberate refusal reads as a crash. Grounded on existing authorities rather than minted: - ancestry reuses std.effect_grant.path_is_prefix (one prefix relation, not a second walk) - over-budget arithmetic calls std.temporal_effect.stall_budget_verdict - change kind projects from std.change.KeyedDiffHunk — no second added/modified/removed enum - glyphs extend std.symbols + extdeps.render.glyphs rows (the one table), never a fork - module_path/source_path carry std's existing representation (std.decl_ref's), with convergence to QualifiedName/SourceRef declared, not assumed Derived, never hand-set: AttentionLevel from a supplied basis (the signed clamp constants — no threshold is invented in this module); BlockedOn from SchedulerHold, which is held in lockstep with the seed governor's HoldReason by execution; T from the heartbeat period the seed actually sleeps, declared a Scaffold with the measured quiet-time distribution as its dissolve-on. Construction over validation: the no-op sum is closed (docs-policy | uncovered | no-decls-touched | generated-artifact | departed-path), so a bare unlabelled "nothing" is unwritable rather than censused after the fact; uncovered derives a visible nudge, departed-path is typed as a widen and not a no-op. Green by execution, with discriminating REDs proven by perturbation: - 28 model conjuncts + 6 lockstep conjuncts PASS - orphaned law row (enforced_by names a missing declaration) → RED - Refused/Failed collapsed onto one glyph → RED (both distinctness and collapse laws) - glyph-table row perturbed → presentation moves → RED (single authority, by execution) Compile-clean attributed: the closure's 47 errors are pre-existing in std/measure.dag (46) and std/effect_grant.dag (1) — identical counts compiling those entries alone; zero attributable to this change. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/extdeps/render/glyphs.dag | 51 +- dag/std/observation.dag | 542 ++++++++++++++++++ dag/std/symbols.dag | 11 + .../observation_lockstep_witness_test.dag | 82 +++ .../claim/observation_model_witness_test.dag | 518 +++++++++++++++++ 5 files changed, 1203 insertions(+), 1 deletion(-) create mode 100644 dag/std/observation.dag create mode 100644 dag/test/claim/observation_lockstep_witness_test.dag create mode 100644 dag/test/claim/observation_model_witness_test.dag diff --git a/dag/extdeps/render/glyphs.dag b/dag/extdeps/render/glyphs.dag index 37662b4dd8d..c2b6c20ab68 100644 --- a/dag/extdeps/render/glyphs.dag +++ b/dag/extdeps/render/glyphs.dag @@ -25,7 +25,7 @@ data standard_symbols: List = [ { id: NodeRunning, glyphs: glyphs_for_tiers(emoji: glyph(text: "🔄"), unicode: glyph(text: "◐"), ascii: glyph(text: "[~]")), color: Active }, { id: NodeCompleted, glyphs: glyphs_for_tiers(emoji: glyph(text: "✅"), unicode: glyph(text: "✓"), ascii: glyph(text: "[x]")), color: Success }, { id: NodeFailed, glyphs: glyphs_for_tiers(emoji: glyph(text: "❌"), unicode: glyph(text: "✗"), ascii: glyph(text: "[!]")), color: Error }, - { id: NodeSkipped, glyphs: glyphs_for_tiers(emoji: glyph(text: "⏩"), unicode: glyph(text: "⊘"), ascii: glyph(text: "[-]")), color: Dim }, + { id: NodeSkipped, glyphs: glyphs_for_tiers(emoji: glyph(text: "⏭️"), unicode: glyph(text: "⊘"), ascii: glyph(text: "[-]")), color: Dim }, { id: NodeIntercepted, glyphs: glyphs_for_tiers(emoji: glyph(text: "🔍"), unicode: glyph(text: "◆"), ascii: glyph(text: "[m]")), color: Info }, { id: EdgeIdle, glyphs: glyphs_for_tiers(emoji: glyph(text: "┄"), unicode: glyph(text: "┄"), ascii: glyph(text: "-")), color: Dim }, @@ -56,6 +56,17 @@ data standard_symbols: List = [ { id: StatusWarning, glyphs: glyphs_for_tiers(emoji: glyph(text: "⚠"), unicode: glyph(text: "⚠"), ascii: glyph(text: "WARN")), color: Warning }, { id: StatusInfo, glyphs: glyphs_for_tiers(emoji: glyph(text: "💡"), unicode: glyph(text: "ℹ"), ascii: glyph(text: "INFO")), color: Info }, + { id: StatusRefused, glyphs: glyphs_for_tiers(emoji: glyph(text: "🚫"), unicode: glyph(text: "⛔"), ascii: glyph(text: "REFUSED")), color: Error }, + { id: StatusBlocked, glyphs: glyphs_for_tiers(emoji: glyph(text: "⏳"), unicode: glyph(text: "◷"), ascii: glyph(text: "BLOCKED")), color: Warning }, + { id: StatusTimedOut, glyphs: glyphs_for_tiers(emoji: glyph(text: "⌛"), unicode: glyph(text: "⏱"), ascii: glyph(text: "TIMEOUT")), color: Error }, + { id: StatusDwell, glyphs: glyphs_for_tiers(emoji: glyph(text: "⏱"), unicode: glyph(text: "◴"), ascii: glyph(text: "DWELL")), color: Warning }, + { id: StatusFinal, glyphs: glyphs_for_tiers(emoji: glyph(text: "✦"), unicode: glyph(text: "✦"), ascii: glyph(text: "DONE")), color: Success }, + { id: StatusPulse, glyphs: glyphs_for_tiers(emoji: glyph(text: "🕐"), unicode: glyph(text: "◷"), ascii: glyph(text: "STATUS")), color: Dim }, + + { id: ChangeAdded, glyphs: glyphs_for_tiers(emoji: glyph(text: "+"), unicode: glyph(text: "+"), ascii: glyph(text: "+")), color: Success }, + { id: ChangeModified, glyphs: glyphs_for_tiers(emoji: glyph(text: "~"), unicode: glyph(text: "~"), ascii: glyph(text: "~")), color: Warning }, + { id: ChangeRemoved, glyphs: glyphs_for_tiers(emoji: glyph(text: "-"), unicode: glyph(text: "-"), ascii: glyph(text: "-")), color: Error }, + { id: DataList, glyphs: glyphs_for_tiers(emoji: glyph(text: "☰"), unicode: glyph(text: "☰"), ascii: glyph(text: "[L]")), color: Default }, { id: DataMap, glyphs: glyphs_for_tiers(emoji: glyph(text: "⊞"), unicode: glyph(text: "⊞"), ascii: glyph(text: "[M]")), color: Default }, { id: DataSecret, glyphs: glyphs_for_tiers(emoji: glyph(text: "🔒"), unicode: glyph(text: "✱"), ascii: glyph(text: "[*]")), color: Warning }, @@ -105,3 +116,41 @@ fn symbol_color(id: SymbolId) -> SemanticColor? { Absent => none } } + +data reward_animal_note: String = "The reward animal shown on a terminal success lands as ROWS in the one glyph authority, never as a hardcoded function in a renderer — the reference implementation studied for this lane keeps its emoji table mirrored by hand between Go and shell, which is the maintained dual representation we are here to not have. Selection is DETERMINISTIC: the index is derived from the run's own identity, so replaying a captured run reproduces its animal exactly and the event stream stays replayable by construction. A random draw would make the stream unreplayable and would be a non-determinism root of precisely the kind the determinism gate walls." + +data reward_animals: List = [ + glyphs_for_tiers(emoji: glyph(text: "🦦"), unicode: glyph(text: "✦"), ascii: glyph(text: "otter")), + glyphs_for_tiers(emoji: glyph(text: "🦊"), unicode: glyph(text: "✦"), ascii: glyph(text: "fox")), + glyphs_for_tiers(emoji: glyph(text: "🦉"), unicode: glyph(text: "✦"), ascii: glyph(text: "owl")), + glyphs_for_tiers(emoji: glyph(text: "🐢"), unicode: glyph(text: "✦"), ascii: glyph(text: "turtle")), + glyphs_for_tiers(emoji: glyph(text: "🦫"), unicode: glyph(text: "✦"), ascii: glyph(text: "beaver")), + glyphs_for_tiers(emoji: glyph(text: "🐋"), unicode: glyph(text: "✦"), ascii: glyph(text: "whale")) +] + +type RewardPick { + idx: Int + found: TierGlyphs? +} + +fn reward_animal_at(index: Int, tier: Tier) -> String? { + let n = count(reward_animals) + if n == 0 { + none + } else { + let target = index % n + let picked = fold(reward_animals, + init: RewardPick { idx: 0, found: none }, + f: (acc, g) => + if acc.idx == target { + RewardPick { idx: acc.idx + 1, found: Present { value: g } } + } else { + RewardPick { idx: acc.idx + 1, found: acc.found } + } + ) + match picked.found { + Present { value: g } => Present { value: glyph_at(glyphs: g, tier: tier) } + Absent => none + } + } +} diff --git a/dag/std/observation.dag b/dag/std/observation.dag new file mode 100644 index 00000000000..9091b96c8a3 --- /dev/null +++ b/dag/std/observation.dag @@ -0,0 +1,542 @@ +module std.observation + +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import std.measure { Millisecond, ByteSize, measure_le, millisecond, millisecond_count } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.symbols { + SymbolId, + NodeRunning, + NodeCompleted, + NodeFailed, + NodeSkipped, + StatusInfo, + StatusWarning, + StatusRefused, + StatusBlocked, + StatusTimedOut, + StatusDwell, + StatusFinal, + ChangeAdded, + ChangeModified, + ChangeRemoved, +} +import std.change { + KeyedDiffHunk, + KeyedDiffAddedHunk, + KeyedDiffRemovedHunk, + KeyedDiffModifiedHunk, +} +import std.effect_grant { path_is_prefix } +import std.disposition { Disposition, Terminal, Scaffold, SingleAuthority } +import std.temporal_effect { + EffectStallBudget, + StallBudgetVerdict, + StallWithinBudget, + StallBudgetExceeded, + stall_budget_verdict, +} + +data observation_module_note: String = "P0 of the progress-and-observation lane (docs/plans/progress-observation-design.md, operator-signed 2026-07-23; CI-rework delta in that doc section 6b). One event vocabulary emitted by the process and consumed by every renderer — a progress line is a PROJECTION of a fact the process already has, never a hand-authored string with a hand-chosen level. This module carries the model, the five laws as rows, and the derived attention/presentation assignment; it renders nothing (renderers are P1 and later). Displaced cost priced in the design doc: the 10-minute silent crawl window of run 30044816605, a 55-minute triage cycle burned on a receipt that only wrote at walk end, and workers hand-rolling the discipline ad hoc." + +data observation_homonym_note: String = "Naming diligence (DESIGN section 3): std.upsert_decision.ObservationVerdict is a DIFFERENT concept that shares the word — it classifies a desired-versus-observed STATE for reconcile/upsert (Converged, Drifted, Conflict). This module observes a PROCESS advancing through time. Neither is a nickname for the other; they are not to be consolidated. The distinguishing question: upsert asks what is true of the world, observation asks how far the work has got." + +type ObservationSegment + = RunSegment { id: NonEmptyStr } + | BatchSegment { index: Nat, label: NonEmptyStr } + | EntrySegment { source_path: NonEmptyStr } + | ModuleSegment { module_path: NonEmptyStr } + | PhaseSegment { name: NonEmptyStr } + +data observation_segment_grounding_note: String = "The subject is a CONTAINMENT PATH — run contains batch contains entry contains module contains phase, the tree the floor already walks — decomposed into typed segments rather than carried as one opaque string (DESIGN section 2 deep decomposition). Two segments name identities other authorities own, and are carried here in the same representation std already uses for them, not in a fresh one: module_path matches std.decl_ref.DeclarationRef.module_path, and source_path is a repo-relative source path. Convergence, not fork: module_path becomes v2.std.qualified_name.QualifiedName and source_path becomes the typed SourceRef when the namespace and module-identity lanes land those carriers (DESIGN open threads). Until then the representation is std's existing one, so there is nothing new to migrate." + +type ObservationSubject { + segments: List +} + +fn observation_segment_key(seg: ObservationSegment) -> String { + match seg { + RunSegment { id: i } => concat("run=", i) + BatchSegment { index: n, label: l } => concat("batch=", concat(to_string(n), concat(":", l))) + EntrySegment { source_path: p } => concat("entry=", p) + ModuleSegment { module_path: m } => concat("module=", m) + PhaseSegment { name: n } => concat("phase=", n) + } +} + +fn observation_subject_path(subject: ObservationSubject) -> List { + map(subject.segments, seg => observation_segment_key(seg: seg)) +} + +data observation_prefix_reuse_note: String = "Ancestry reuses std.effect_grant.path_is_prefix — the ONE prefix relation already carrying the containment question for effect targets, rather than a second walk written here (DESIGN section 3). Observation is one more consumer of the containment structure the resolver walks, content-addressing hashes, and effect admissibility tests. The subject tree is deliberately NOT added as a std.effect_grant.NamespaceTree variant in this phase: that enum is another lane's authority and the seed does not check match exhaustiveness, so a new variant would fall through silently in its existing consumers. Dissolve-on: the SymbolIndex-backed position supersedes the path-string interim, per the convergence commitment already declared in std.effect_grant.namespace_tree_note." + +fn observation_subject_contains(ancestor: ObservationSubject, descendant: ObservationSubject) -> Bool { + path_is_prefix( + prefix: observation_subject_path(subject: ancestor), + path: observation_subject_path(subject: descendant) + ) +} + +type ObservationGrain + = RunGrain + | BatchGrain + | EntryGrain + | ModuleGrain + | PhaseGrain + +fn observation_grain_of_segment(seg: ObservationSegment) -> ObservationGrain { + match seg { + RunSegment { id: _ } => RunGrain + BatchSegment { index: _, label: _ } => BatchGrain + EntrySegment { source_path: _ } => EntryGrain + ModuleSegment { module_path: _ } => ModuleGrain + PhaseSegment { name: _ } => PhaseGrain + } +} + +fn observation_grain_depth(g: ObservationGrain) -> Nat { + match g { + RunGrain => 0 + BatchGrain => 1 + EntryGrain => 2 + ModuleGrain => 3 + PhaseGrain => 4 + } +} + +fn observation_subject_grain(subject: ObservationSubject) -> ObservationGrain { + fold(subject.segments, init: RunGrain, f: (acc, seg) => observation_grain_of_segment(seg: seg)) +} + +fn observation_grain_at_or_above_entry(g: ObservationGrain) -> Bool { + observation_grain_depth(g: g) <= observation_grain_depth(g: EntryGrain) +} + +type Measured + = MeasuredValue { value: T } + | MeasuredUnavailable { cause: NonEmptyStr } + +data observation_measured_note: String = "Absence of a measurement is a NAMED state carrying its cause, never a fabricated zero (DESIGN section 5). This mirrors the discipline the floor heartbeat already keeps in the seed: an unreadable cgroup field prints unreadable and refuses to invent a number. A renderer projecting MeasuredUnavailable prints the cause; it never prints 0 and never omits the field, because a silently omitted number is the same fabrication one layer up." + +type ObservationOutcome + = Done + | Refused { diagnostic: NonEmptyStr } + | Failed { error: NonEmptyStr, output: String } + | TimedOut { budget: Millisecond, elapsed: Millisecond } + | Skipped { reason: NonEmptyStr } + | Final { shown_failures: Nat } + +data observation_refused_distinct_note: String = "Refused is DISTINCT from Failed, and this is the point of the sum, not a nicety. A typed refusal is the house's fail-closed spine (DESIGN section 5): the line stopped deliberately, located, with a diagnostic — the stopped line the operator is meant to analyse. A failure is the process breaking. The reference implementation studied for this lane conflates them behind one red glyph, which is exactly how a deliberate refusal reads as a crash and a crash reads as policy. They get different glyphs and different rendering; a renderer that maps both to one symbol is a regression the presentation census reds." + +type ObservationTransition + = Begin + | Step { k: Nat, n: Nat } + | Concluded { outcome: ObservationOutcome } + +type ObservationEvent { + subject: ObservationSubject + transition: ObservationTransition + wall: Measured + rss: Measured +} + +type SchedulerHold + = WindowFull { active: Nat, target: Nat } + | CurrentHighWater { current: ByteSize, high_water: ByteSize } + | PsiPressure { avg10_centi: Nat } + | SwapGrowth { delta: ByteSize } + | AwaitFirstCost { undigested: Nat } + | InsufficientHeadroom { current: ByteSize, share: ByteSize, high_water: ByteSize } + | AdmissionCeiling { current: ByteSize, ceiling: ByteSize } + +data observation_scheduler_hold_note: String = "SchedulerHold is the substrate authority for why admission was withheld this poll; the seed's v1_compiler memory_governor HoldReason is the realization that must stay in lockstep with it (the .dag graph is the authority and Rust is the seed — DESIGN section 7). The lockstep witness proves arity and variant names match by reading the live seed, so a variant added on one side and not the other reds instead of drifting. PsiPressure carries centi-units of the avg10 percentage rather than a float: the pressure reading is a magnitude with a scale, and a bare float in a rendered line is the scalar/unit conflation the house hard-blocks." + +type ContendedResource + = MemoryBudget + | AdmissionWindow + | MaturationReserve + +type ContentionRemaining + = RemainingUnknown { cause: NonEmptyStr } + | RemainingBounded { by: Millisecond } + +type BlockedOn { + resource: ContendedResource + holders: List + remaining: ContentionRemaining +} + +fn observation_hold_resource(hold: SchedulerHold) -> ContendedResource { + match hold { + WindowFull { active: _, target: _ } => AdmissionWindow + CurrentHighWater { current: _, high_water: _ } => MemoryBudget + PsiPressure { avg10_centi: _ } => MemoryBudget + SwapGrowth { delta: _ } => MemoryBudget + AwaitFirstCost { undigested: _ } => MaturationReserve + InsufficientHeadroom { current: _, share: _, high_water: _ } => MemoryBudget + AdmissionCeiling { current: _, ceiling: _ } => MaturationReserve + } +} + +data observation_blocked_on_derived_note: String = "BlockedOn is DERIVED from scheduler and governor state and has no hand-set constructor path in any renderer (design doc section 2). The governor already knows why it held; the crawl window that priced this lane was invisible only because that knowledge never reached the log. holders are the subjects currently occupying the contended resource — existing scheduler state, not a new telemetry source (design doc section 7 forbids new sources in P0 and P1). remaining is a named coproduct because the governor does not always have a bound to give: RemainingUnknown carries why, so the renderer prints the cause rather than an invented estimate." + +fn observation_blocked_on(hold: SchedulerHold, holders: List, remaining: ContentionRemaining) -> BlockedOn { + BlockedOn { + resource: observation_hold_resource(hold: hold), + holders: holders, + remaining: remaining + } +} + +type AttentionLevel + = Ambient + | Notable + | Anomaly + +type AttentionBasis { + average: Millisecond + maximum: Millisecond +} + +data observation_attention_basis_note: String = "AttentionLevel is DERIVED, never chosen at an emit site — there are no per-site log levels in this model (law 5). The basis is the pair of signed cost constants the CI clamp model carries (design doc section 6b): over the signed average is Notable, over the signed maximum is Anomaly. No threshold is invented here — the basis is a parameter supplied by the consumer that owns those constants, so this module cannot smuggle a number of its own. A refusal, failure, timeout, or orphaned begin is an Anomaly on its own terms, independent of any basis." + +fn observation_outcome_is_anomaly(o: ObservationOutcome) -> Bool { + match o { + Done => false + Refused { diagnostic: _ } => true + Failed { error: _, output: _ } => true + TimedOut { budget: _, elapsed: _ } => true + Skipped { reason: _ } => false + Final { shown_failures: _ } => false + } +} + +fn observation_attention_of_elapsed(elapsed: Millisecond, basis: AttentionBasis) -> AttentionLevel { + if measure_le(a: elapsed, b: basis.average) { + Ambient + } else if measure_le(a: elapsed, b: basis.maximum) { + Notable + } else { + Anomaly + } +} + +fn observation_attention(event: ObservationEvent, basis: AttentionBasis) -> AttentionLevel { + match event.transition { + Begin => Ambient + Step { k: _, n: _ } => + match event.wall { + MeasuredValue { value: w } => observation_attention_of_elapsed(elapsed: w, basis: basis) + MeasuredUnavailable { cause: _ } => Ambient + } + Concluded { outcome: o } => + if observation_outcome_is_anomaly(o: o) { + Anomaly + } else { + match event.wall { + MeasuredValue { value: w } => observation_attention_of_elapsed(elapsed: w, basis: basis) + MeasuredUnavailable { cause: _ } => Ambient + } + } + } +} + +type ObservationEventClass + = ClassBegin + | ClassStep + | ClassDone + | ClassRefused + | ClassFailed + | ClassTimedOut + | ClassSkipped + | ClassFinal + | ClassBlocked + | ClassDwellEscalation + +data observation_event_classes: List = [ + ClassBegin, + ClassStep, + ClassDone, + ClassRefused, + ClassFailed, + ClassTimedOut, + ClassSkipped, + ClassFinal, + ClassBlocked, + ClassDwellEscalation +] + +fn observation_outcome_class(o: ObservationOutcome) -> ObservationEventClass { + match o { + Done => ClassDone + Refused { diagnostic: _ } => ClassRefused + Failed { error: _, output: _ } => ClassFailed + TimedOut { budget: _, elapsed: _ } => ClassTimedOut + Skipped { reason: _ } => ClassSkipped + Final { shown_failures: _ } => ClassFinal + } +} + +fn observation_event_class(event: ObservationEvent) -> ObservationEventClass { + match event.transition { + Begin => ClassBegin + Step { k: _, n: _ } => ClassStep + Concluded { outcome: o } => observation_outcome_class(o: o) + } +} + +type ObservationPresentation { + glyph: SymbolId + collapsible: Bool + expands_fully: Bool +} + +data observation_collapse_axes_note: String = "collapsible and expands_fully are two axes, not one flag with a middle value, because three states are real and conflating them loses one. Routine progress collapses and does not expand. An intrinsic anomaly neither collapses nor stays terse: it expands fully and names itself. The run's Final summary is the third state — it never collapses, because a summary that hid itself would defeat its purpose, yet it is NOT an anomaly and must not be rendered as one. A single collapsible flag would force Final to borrow the anomaly's presentation, which is the state-space conflation the house splits into named variants." + +data observation_presentation_note: String = "Event class to presentation is a TOTAL assignment over the class roster, censused and walled the way the register censuses unthemed colors. The presentation carries only the SymbolId: the glyph text per tier and the register colour role are read from the ONE glyph authority (std.symbols identity, extdeps.render.glyphs rows), never restated here, so perturbing a glyph row moves every renderer and a renderer that hardcodes a glyph is provably not reading the table. collapsible and expands_fully carry law 4: sub-threshold work collapses, an anomaly expands fully and names itself, and the two flags are never both permissive for an intrinsically anomalous class." + +fn observation_class_is_intrinsic_anomaly(c: ObservationEventClass) -> Bool { + match c { + ClassBegin => false + ClassStep => false + ClassDone => false + ClassRefused => true + ClassFailed => true + ClassTimedOut => true + ClassSkipped => false + ClassFinal => false + ClassBlocked => true + ClassDwellEscalation => true + } +} + +fn observation_presentation(c: ObservationEventClass) -> ObservationPresentation { + match c { + ClassBegin => ObservationPresentation { glyph: NodeRunning, collapsible: true, expands_fully: false } + ClassStep => ObservationPresentation { glyph: NodeRunning, collapsible: true, expands_fully: false } + ClassDone => ObservationPresentation { glyph: NodeCompleted, collapsible: true, expands_fully: false } + ClassRefused => ObservationPresentation { glyph: StatusRefused, collapsible: false, expands_fully: true } + ClassFailed => ObservationPresentation { glyph: NodeFailed, collapsible: false, expands_fully: true } + ClassTimedOut => ObservationPresentation { glyph: StatusTimedOut, collapsible: false, expands_fully: true } + ClassSkipped => ObservationPresentation { glyph: NodeSkipped, collapsible: true, expands_fully: false } + ClassFinal => ObservationPresentation { glyph: StatusFinal, collapsible: false, expands_fully: false } + ClassBlocked => ObservationPresentation { glyph: StatusBlocked, collapsible: false, expands_fully: true } + ClassDwellEscalation => ObservationPresentation { glyph: StatusDwell, collapsible: false, expands_fully: true } + } +} + +data observation_change_presentation_note: String = "Change kind is NOT re-declared here. std.change.KeyedDiffHunk already carries the added, removed and modified discriminant — it is the house's one diff carrier — so a fresh three-variant enum on this module would be a nickname for it, duplicated again in everything derived from it. What observation owns is the PRESENTATION of that discriminant: a symbol whose register colour role follows the git-diff convention the operator signed, and a text tag. The tag is not optional decoration: colour is never the only channel, so a pipe, a colourless terminal, or a screen reader loses nothing. A presentation with an empty tag is the failure the witness reds." + +type ChangePresentation { + symbol: SymbolId + tag: NonEmptyStr +} + +fn observation_change_presentation(hunk: KeyedDiffHunk) -> ChangePresentation { + match hunk { + KeyedDiffAddedHunk { hunk_key: _, to: _ } => + ChangePresentation { symbol: ChangeAdded, tag: "new" as NonEmptyStr } + KeyedDiffRemovedHunk { hunk_key: _, from: _ } => + ChangePresentation { symbol: ChangeRemoved, tag: "removed" as NonEmptyStr } + KeyedDiffModifiedHunk { hunk_key: _, from: _, to: _ } => + ChangePresentation { symbol: ChangeModified, tag: "edited" as NonEmptyStr } + } +} + +type SelectionNoOp + = DocsPolicy + | Uncovered { declaration: NonEmptyStr } + | NoDeclsTouched + | GeneratedArtifact { drift_gate_authority: NonEmptyStr } + | DepartedPath { cause: NonEmptyStr } + +data observation_selection_no_op_note: String = "A touched file that selected no work is a CLOSED SUM, never a bare nothing. Nothing meaning five different things is the state-space conflation the house splits into named variants, and here the remedies genuinely differ: docs select no executable work and that is correct and quiet; a touched declaration no witness references is a COVERAGE GAP surfaced at review time, so it renders as a visible nudge rather than a comfort; only comments or whitespace intersected is fine and says so; a generated artifact is checked by the drift gate against its authority, not by witnesses, so nothing must never appear beside it; and a departed path is NOT a no-op at all — a deleted module cannot be scoped, so selection widens to baseline and the line says why. Because the sum is closed and the presentation assignment below is total, an unlabelled nothing is UNWRITABLE rather than censused after the fact — construction where a census would have conceded the bad line was writable." + +fn observation_no_op_is_actually_a_widen(n: SelectionNoOp) -> Bool { + match n { + DocsPolicy => false + Uncovered { declaration: _ } => false + NoDeclsTouched => false + GeneratedArtifact { drift_gate_authority: _ } => false + DepartedPath { cause: _ } => true + } +} + +fn observation_no_op_attention(n: SelectionNoOp) -> AttentionLevel { + match n { + DocsPolicy => Ambient + Uncovered { declaration: _ } => Notable + NoDeclsTouched => Ambient + GeneratedArtifact { drift_gate_authority: _ } => Ambient + DepartedPath { cause: _ } => Notable + } +} + +fn observation_no_op_symbol(n: SelectionNoOp) -> SymbolId { + match n { + DocsPolicy => NodeSkipped + Uncovered { declaration: _ } => StatusWarning + NoDeclsTouched => NodeSkipped + GeneratedArtifact { drift_gate_authority: _ } => StatusInfo + DepartedPath { cause: _ } => StatusWarning + } +} + +fn observation_no_op_explanation(n: SelectionNoOp) -> NonEmptyStr { + match n { + DocsPolicy => "documentation never selects executable work" as NonEmptyStr + Uncovered { declaration: d } => concat("no witness covers this change: ", d) as NonEmptyStr + NoDeclsTouched => "only comments or whitespace changed" as NonEmptyStr + GeneratedArtifact { drift_gate_authority: a } => concat("generated artifact — checked by drift against ", a) as NonEmptyStr + DepartedPath { cause: c } => concat("a deleted module cannot be scoped, so selection widened: ", c) as NonEmptyStr + } +} + +data observation_human_units_contract: String = "Rendered magnitudes are in human units, and a raw telemetry dump is a census violation rather than a projection. The named negative example is the floor memory heartbeat's current shape: sixty-plus identical context-free byte dumps an hour, current=16111669248 with no subject attached, which is precisely the line that was present throughout the ten-minute crawl window and told the operator nothing. A projection states the magnitude at a scale a human reads and attaches the subject it belongs to. This contract is stated on the model so the P3 census has an authority to check against; the renderers that satisfy it are P1 and later." + +data observation_heartbeat_period: Millisecond = millisecond(count: 60000) + +data observation_heartbeat_period_note: String = "The heartbeat cadence is a measured fact of the running system, not a preference: the floor's memory heartbeat already samples and prints once per minute, and the lockstep witness reads that period out of the live seed rather than trusting this row. It is stated here because the dwell threshold is derived from it." + +type DwellThreshold { + period: Millisecond + basis: NonEmptyStr +} + +data observation_dwell_threshold: DwellThreshold = DwellThreshold { + period: observation_heartbeat_period, + basis: "one heartbeat period — the log's existing clock. Law 2 requires that no subject stay quiet past T without a line naming it, and the heartbeat is already the cadence at which the log speaks, so T is DERIVED from the emitter's own period rather than picked. Escalation at 2T and 4T rides the same clock, which is why depth is a division and not a table of tuned constants." +} + +data observation_dwell_threshold_disposition: Disposition = Scaffold { + dissolves_to: SingleAuthority, + bind: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_dwell_threshold", + field: WholeDeclaration + } +} + +data observation_dwell_recalibration_note: String = "Declared scaffold, with its trigger: the design doc says T's basis is the measured quiet-time distribution. That distribution is not yet collected — the gauntlet and falsifier whole-corpus runs are the context that will produce it (design doc section 6b names that lane as the escalation law's primary home). Deriving T from the heartbeat period is a grounded interim, not taste, because the period is itself measured and law 2 is stated in terms of it. Dissolve-on: the first measured quiet-time distribution from a gauntlet cadence replaces the basis, and this row's period becomes a projection of it." + +fn observation_escalation_depth(quiet: Millisecond, t: DwellThreshold) -> Nat { + millisecond_count(m: quiet) / millisecond_count(m: t.period) +} + +data observation_escalation_note: String = "Law 3: attention escalates by dwell time, RECURSIVELY. A subject quiet past T surfaces its current child, past 2T the grandchild, and so on to the leaf — the collapse rule of law 4 run in reverse under time pressure, over the same tree and the same events. Depth is floor of quiet over T, so one law covers every level instead of a special case per level; the reference implementation expands one level only, and only on a TTY, which is why the CI crawl window stayed silent." + +fn observation_escalation_reveal_depth(quiet_subject: ObservationSubject, quiet: Millisecond, t: DwellThreshold) -> Nat { + count(quiet_subject.segments) + observation_escalation_depth(quiet: quiet, t: t) +} + +fn observation_escalation_subject(quiet_subject: ObservationSubject, active_leaf: ObservationSubject, quiet: Millisecond, t: DwellThreshold) -> ObservationSubject { + if observation_subject_contains(ancestor: quiet_subject, descendant: active_leaf) { + ObservationSubject { + segments: active_leaf.segments.take(n: observation_escalation_reveal_depth(quiet_subject: quiet_subject, quiet: quiet, t: t)) + } + } else { + quiet_subject + } +} + +type WatchdogVerdict + = WatchdogQuiet + | WatchdogOrphanedBegin { subject: ObservationSubject, elapsed: Millisecond, budget: Millisecond } + +data observation_watchdog_note: String = "Law 1: every subject at entry grain and above emits a matched Begin and exactly one Outcome, so an orphaned Begin is a DETECTABLE DEFECT rather than a silence to be interpreted. The watchdog converts quiet-past-budget into a typed line, which also closes the batch-wall hang gap — a batch that never completes currently rides silently to the step cap. The exceeded-budget arithmetic is not restated here: it calls std.temporal_effect.stall_budget_verdict, the one carrier already answering elapsed-against-limit, so there is a single authority for what over-budget means and this module only adds the subject the verdict is about." + +fn observation_watchdog(subject: ObservationSubject, elapsed: Millisecond, budget: Millisecond) -> WatchdogVerdict { + match stall_budget_verdict(budget: EffectStallBudget { + step_id: observation_subject_render(subject: subject), + limit_ms: budget, + elapsed_ms: elapsed + }) { + StallWithinBudget => WatchdogQuiet + StallBudgetExceeded { step_id: _, elapsed_ms: e, limit_ms: l } => + WatchdogOrphanedBegin { subject: subject, elapsed: e, budget: l } + } +} + +fn observation_subject_render(subject: ObservationSubject) -> NonEmptyStr { + match join(observation_subject_path(subject: subject), " ") { + "" => "run=unattributed" as NonEmptyStr + rendered => rendered as NonEmptyStr + } +} + +fn observation_watchdog_attention(v: WatchdogVerdict) -> AttentionLevel { + match v { + WatchdogQuiet => Ambient + WatchdogOrphanedBegin { subject: _, elapsed: _, budget: _ } => Anomaly + } +} + +type ObservationLaw { + id: NonEmptyStr + statement: NonEmptyStr + enforced_by: DeclarationRef +} + +data observation_laws_note: String = "The five laws land as DATA ROWS, operator-signed 2026-07-23. Each row names the declaration that enforces it, so a law with no enforcement is a detectable orphan rather than a sentence in a document nobody executes — the lockstep witness reads this module out of the live tree and reds when a named declaration is absent. Candidate convergence, flagged not assumed: the enforcement-intent lane models the operator's recurring standing directives as StandingIntent rows with a lens contract and a coverage receipt. These five are directives of exactly that shape, but StandingIntent's fields today are lens-enforcement machinery (scope roster, consumer requirement, narrowing reasons) that these rows would fill hollowly, so they stay plain rows and the consolidation is the operator's call when that lane generalizes." + +data observation_law_process_to_outcome: ObservationLaw = ObservationLaw { + id: "observation.process-to-outcome" as NonEmptyStr, + statement: "Every subject at entry grain and above emits a matched Begin and exactly one Outcome; quiet past budget becomes a typed line, never an interpreted silence." as NonEmptyStr, + enforced_by: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_watchdog", + field: WholeDeclaration + } +} + +data observation_law_heartbeat_identity: ObservationLaw = ObservationLaw { + id: "observation.heartbeat-carries-identity" as NonEmptyStr, + statement: "The heartbeat carries identity, not just vitals: phase, entry, module and k of n. No phase exceeds T without a line naming its current subject." as NonEmptyStr, + enforced_by: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_subject_render", + field: WholeDeclaration + } +} + +data observation_law_dwell_escalation: ObservationLaw = ObservationLaw { + id: "observation.attention-escalates-by-dwell" as NonEmptyStr, + statement: "Attention escalates by dwell time, recursively: quiet past T surfaces the child, past 2T the grandchild, down to the leaf. T is a data row with a measured basis." as NonEmptyStr, + enforced_by: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_escalation_subject", + field: WholeDeclaration + } +} + +data observation_law_quiet_at_arms_length: ObservationLaw = ObservationLaw { + id: "observation.quiet-at-arms-length" as NonEmptyStr, + statement: "Sub-threshold work is silent and collapses to summaries; a red or a refusal expands fully and names itself. The asymmetry is the design, and expansion stays bounded." as NonEmptyStr, + enforced_by: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_presentation", + field: WholeDeclaration + } +} + +data observation_law_every_response_true: ObservationLaw = ObservationLaw { + id: "observation.every-response-true" as NonEmptyStr, + statement: "Every rendered number is a projection of a receipt fact. No vibes strings, no per-site log levels, no fabricated value standing in for an unavailable measurement." as NonEmptyStr, + enforced_by: DeclarationRef { + module_path: "std.observation", + decl_name: "observation_attention", + field: WholeDeclaration + } +} + +data observation_laws: List = [ + observation_law_process_to_outcome, + observation_law_heartbeat_identity, + observation_law_dwell_escalation, + observation_law_quiet_at_arms_length, + observation_law_every_response_true +] + +data observation_renders_nothing_disposition: Disposition = Terminal { + reason: "P0 carries the model only. Renderers are P1 and later, and the CI renderer's wiring lands after the atomic CI PR because both touch the floor's emit sites and churning them twice is the migration class the operator ruled out. This module therefore holds no line formatting, no glyph text, and no ANSI: it holds the vocabulary those renderers project from." +} diff --git a/dag/std/symbols.dag b/dag/std/symbols.dag index fa0f189dcc3..e1bb1cc3a92 100644 --- a/dag/std/symbols.dag +++ b/dag/std/symbols.dag @@ -41,6 +41,17 @@ type SymbolId | StatusWarning | StatusInfo + | StatusRefused + | StatusBlocked + | StatusTimedOut + | StatusDwell + | StatusFinal + | StatusPulse + + | ChangeAdded + | ChangeModified + | ChangeRemoved + | DataList | DataMap | DataSecret diff --git a/dag/test/claim/observation_lockstep_witness_test.dag b/dag/test/claim/observation_lockstep_witness_test.dag new file mode 100644 index 00000000000..c51c9e5ddc4 --- /dev/null +++ b/dag/test/claim/observation_lockstep_witness_test.dag @@ -0,0 +1,82 @@ +module test.claim.observation_lockstep_witness_test + +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import std.types { String, Bool, List } +import std.observation { ObservationLaw, observation_laws, observation_heartbeat_period } +import std.measure { millisecond_count } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data witness_note: String = "Lockstep half of the P0 acceptance (docs/plans/progress-observation-design.md section 5): the laws are data rows and the substrate model is the authority, so both are held against the live tree rather than against a restatement. Three couplings are proven by execution here. One, no orphan law — every law row names a declaration that must actually exist in std.observation, so a law cannot be a sentence nobody executes. Two, the SchedulerHold coproduct and the seed governor's HoldReason must carry the same variants, so a hold reason added on one side reds instead of silently rendering as a missing cause. Three, the dwell threshold's derivation from the heartbeat period is checked against the period the seed actually sleeps, so the row cannot drift into taste. Each check carries a negative control in the same run: a name known to be absent must read as absent, which is what separates this from a grep that passes vacuously." + +data observation_module_path: String = "dag/std/observation.dag" +data governor_source_path: String = "src/v1/stage0/src/memory_governor.rs" +data executor_source_path: String = "src/v1/stage0/src/bin/claim_executor.rs" + +fn lockstep_source(path: String) -> String { + let r = filesystem_read(path: path) + r.content +} + +fn lockstep_has(path: String, needle: String) -> Bool { + string_contains(s: lockstep_source(path: path), pattern: needle) +} + +fn w_every_law_names_a_declaration_that_exists() -> Bool { + let src = lockstep_source(path: observation_module_path) + fold(observation_laws, init: true, f: (acc, l) => + acc && string_contains(s: src, pattern: concat("fn ", concat(l.enforced_by.decl_name, "("))) + ) +} + +fn w_orphan_law_check_discriminates() -> Bool { + let src = lockstep_source(path: observation_module_path) + !string_contains(s: src, pattern: "fn observation_law_with_no_enforcer(") +} + +data governor_hold_variants: List = [ + "WindowFull", + "CurrentHighWater", + "PsiPressure", + "SwapGrowth", + "AwaitFirstCost", + "InsufficientHeadroom", + "AdmissionCeiling" +] + +fn w_scheduler_hold_is_in_lockstep_with_the_seed_governor() -> Bool { + let governor = lockstep_source(path: governor_source_path) + let model = lockstep_source(path: observation_module_path) + fold(governor_hold_variants, init: true, f: (acc, v) => + acc && + string_contains(s: governor, pattern: concat("HoldReason::", v)) && + string_contains(s: model, pattern: v) + ) +} + +fn w_lockstep_check_discriminates() -> Bool { + let governor = lockstep_source(path: governor_source_path) + let model = lockstep_source(path: observation_module_path) + !string_contains(s: governor, pattern: "HoldReason::DiskPressure") && + !string_contains(s: model, pattern: "DiskPressure") +} + +fn w_heartbeat_period_matches_the_seed_cadence() -> Bool { + millisecond_count(m: observation_heartbeat_period) == 60000 && + lockstep_has(path: executor_source_path, needle: "from_secs(60)") && + lockstep_has(path: executor_source_path, needle: "floor-memory-heartbeat") +} + +fn w_heartbeat_source_still_refuses_to_fabricate() -> Bool { + lockstep_has(path: executor_source_path, needle: "refusing to fabricate") +} + +test fn observation_lockstep_witnesses() -> Bool { + w_every_law_names_a_declaration_that_exists() && + w_orphan_law_check_discriminates() && + w_scheduler_hold_is_in_lockstep_with_the_seed_governor() && + w_lockstep_check_discriminates() && + w_heartbeat_period_matches_the_seed_cadence() && + w_heartbeat_source_still_refuses_to_fabricate() +} diff --git a/dag/test/claim/observation_model_witness_test.dag b/dag/test/claim/observation_model_witness_test.dag new file mode 100644 index 00000000000..441501036ec --- /dev/null +++ b/dag/test/claim/observation_model_witness_test.dag @@ -0,0 +1,518 @@ +module test.claim.observation_model_witness_test + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { NonEmptyStr, String, Bool, List } +import std.symbols { + Tier, + Emoji, + Unicode, + Ascii, + SymbolId, + SemanticColor, + Success, + Warning, + Error, + StatusWarning, + StatusPulse, + ChangeAdded, + ChangeModified, + ChangeRemoved, +} +import std.change { KeyedDiffAddedHunk, KeyedDiffRemovedHunk, KeyedDiffModifiedHunk } +import std.measure { Millisecond, millisecond, ByteSize, byte_size } +import std.perturbation { BackwardDiffInDiffOut, ForwardSameInSameOut, perturbation_consistent } +import extdeps.render.glyphs { resolve_symbol, symbol_color, reward_animal_at, reward_animals } +import std.observation { + ObservationSegment, + RunSegment, + BatchSegment, + EntrySegment, + ModuleSegment, + PhaseSegment, + ObservationSubject, + ObservationGrain, + RunGrain, + BatchGrain, + EntryGrain, + ModuleGrain, + PhaseGrain, + observation_grain_of_segment, + observation_grain_depth, + observation_subject_grain, + observation_grain_at_or_above_entry, + observation_subject_contains, + observation_subject_path, + observation_subject_render, + Measured, + MeasuredValue, + MeasuredUnavailable, + ObservationOutcome, + Done, + Refused, + Failed, + TimedOut, + Skipped, + Final, + ObservationTransition, + Begin, + Step, + Concluded, + ObservationEvent, + SchedulerHold, + WindowFull, + CurrentHighWater, + PsiPressure, + SwapGrowth, + AwaitFirstCost, + InsufficientHeadroom, + AdmissionCeiling, + ContendedResource, + MemoryBudget, + AdmissionWindow, + MaturationReserve, + ContentionRemaining, + RemainingUnknown, + RemainingBounded, + BlockedOn, + observation_hold_resource, + observation_blocked_on, + AttentionLevel, + Ambient, + Notable, + Anomaly, + AttentionBasis, + observation_attention, + observation_outcome_is_anomaly, + ObservationEventClass, + ClassBegin, + ClassStep, + ClassDone, + ClassRefused, + ClassFailed, + ClassTimedOut, + ClassSkipped, + ClassFinal, + ClassBlocked, + ClassDwellEscalation, + observation_event_classes, + observation_event_class, + observation_class_is_intrinsic_anomaly, + ObservationPresentation, + observation_presentation, + DwellThreshold, + observation_dwell_threshold, + observation_escalation_depth, + observation_escalation_subject, + WatchdogVerdict, + WatchdogQuiet, + WatchdogOrphanedBegin, + observation_watchdog, + observation_watchdog_attention, + ObservationLaw, + observation_laws, + ChangePresentation, + observation_change_presentation, + SelectionNoOp, + DocsPolicy, + Uncovered, + NoDeclsTouched, + GeneratedArtifact, + DepartedPath, + observation_no_op_is_actually_a_widen, + observation_no_op_attention, + observation_no_op_symbol, + observation_no_op_explanation, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data witness_note: String = "P0 acceptance for the observation model (docs/plans/progress-observation-design.md section 5): presentation totality over the class roster, the derived attention assignment, the derived BlockedOn, the recursive dwell escalation, the orphaned-begin watchdog, and the single-authority glyph perturbation. Every check here runs against the model itself; no renderer exists in this phase. Discrimination is built in rather than asserted: the Refused-versus-Failed check reds if the two ever collapse onto one glyph, the perturbation check reds if a presentation glyph is hardcoded instead of read from the glyph table, and the attention checks red if a threshold is smuggled into this module instead of taken from the supplied basis." + +fn obs_run_subject() -> ObservationSubject { + ObservationSubject { segments: [RunSegment { id: "30044816605" as NonEmptyStr }] } +} + +fn obs_entry_subject() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: "30044816605" as NonEmptyStr }, + BatchSegment { index: 3, label: "discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/effect_reach_test.dag" as NonEmptyStr } + ] + } +} + +fn obs_leaf_subject() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: "30044816605" as NonEmptyStr }, + BatchSegment { index: 3, label: "discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/effect_reach_test.dag" as NonEmptyStr }, + ModuleSegment { module_path: "v2.compiler.normalized_tree" as NonEmptyStr }, + PhaseSegment { name: "typecheck" as NonEmptyStr } + ] + } +} + +fn obs_event(transition: ObservationTransition, wall_ms: Int) -> ObservationEvent { + ObservationEvent { + subject: obs_entry_subject(), + transition: transition, + wall: MeasuredValue { value: millisecond(count: wall_ms) }, + rss: MeasuredUnavailable { cause: "cgroup memory.current unreadable in this fixture" as NonEmptyStr } + } +} + +fn obs_basis() -> AttentionBasis { + AttentionBasis { average: millisecond(count: 1000), maximum: millisecond(count: 5000) } +} + +fn obs_glyph_resolves_all_tiers(c: ObservationEventClass) -> Bool { + let p = observation_presentation(c: c) + match resolve_symbol(id: p.glyph, tier: Emoji) { + Present { value: e } => + match resolve_symbol(id: p.glyph, tier: Unicode) { + Present { value: u } => + match resolve_symbol(id: p.glyph, tier: Ascii) { + Present { value: a } => e != "" && u != "" && a != "" + Absent => false + } + Absent => false + } + Absent => false + } +} + +fn w_presentation_is_total_over_class_roster() -> Bool { + fold(observation_event_classes, init: true, f: (acc, c) => + acc && obs_glyph_resolves_all_tiers(c: c) + ) +} + +fn w_class_roster_covers_every_constructed_class() -> Bool { + count(observation_event_classes) == 10 && + observation_event_class(event: obs_event(transition: Begin, wall_ms: 1)) == ClassBegin && + observation_event_class(event: obs_event(transition: Step { k: 34, n: 61 }, wall_ms: 1)) == ClassStep && + observation_event_class(event: obs_event(transition: Concluded { outcome: Done }, wall_ms: 1)) == ClassDone && + observation_event_class(event: obs_event(transition: Concluded { outcome: Refused { diagnostic: "FLOOR-BATCH-OVER-BUDGET" as NonEmptyStr } }, wall_ms: 1)) == ClassRefused && + observation_event_class(event: obs_event(transition: Concluded { outcome: Failed { error: "panic" as NonEmptyStr, output: "" } }, wall_ms: 1)) == ClassFailed && + observation_event_class(event: obs_event(transition: Concluded { outcome: Skipped { reason: "unaffected" as NonEmptyStr } }, wall_ms: 1)) == ClassSkipped && + observation_event_class(event: obs_event(transition: Concluded { outcome: Final { shown_failures: 0 } }, wall_ms: 1)) == ClassFinal +} + +fn w_refused_is_distinct_from_failed() -> Bool { + let refused = observation_presentation(c: ClassRefused) + let failed = observation_presentation(c: ClassFailed) + let timed_out = observation_presentation(c: ClassTimedOut) + match resolve_symbol(id: refused.glyph, tier: Unicode) { + Present { value: r } => + match resolve_symbol(id: failed.glyph, tier: Unicode) { + Present { value: f } => + match resolve_symbol(id: timed_out.glyph, tier: Unicode) { + Present { value: t } => r != f && r != t && f != t + Absent => false + } + Absent => false + } + Absent => false + } +} + +fn w_anomaly_classes_never_collapse() -> Bool { + fold(observation_event_classes, init: true, f: (acc, c) => { + let p = observation_presentation(c: c) + if observation_class_is_intrinsic_anomaly(c: c) { + acc && !p.collapsible && p.expands_fully + } else { + acc && !p.expands_fully + } + }) +} + +fn w_routine_progress_collapses_but_the_summary_always_shows() -> Bool { + observation_presentation(c: ClassBegin).collapsible && + observation_presentation(c: ClassStep).collapsible && + observation_presentation(c: ClassDone).collapsible && + observation_presentation(c: ClassSkipped).collapsible && + !observation_presentation(c: ClassFinal).collapsible && + !observation_class_is_intrinsic_anomaly(c: ClassFinal) +} + +fn w_refusal_is_anomaly_on_any_basis() -> Bool { + let refused = obs_event(transition: Concluded { outcome: Refused { diagnostic: "budget" as NonEmptyStr } }, wall_ms: 1) + let generous = AttentionBasis { average: millisecond(count: 999999), maximum: millisecond(count: 999999) } + observation_attention(event: refused, basis: generous) == Anomaly && + observation_attention(event: refused, basis: obs_basis()) == Anomaly +} + +fn w_attention_grounds_on_supplied_basis() -> Bool { + let quick = obs_event(transition: Concluded { outcome: Done }, wall_ms: 500) + let over_average = obs_event(transition: Concluded { outcome: Done }, wall_ms: 2000) + let over_maximum = obs_event(transition: Concluded { outcome: Done }, wall_ms: 9000) + observation_attention(event: quick, basis: obs_basis()) == Ambient && + observation_attention(event: over_average, basis: obs_basis()) == Notable && + observation_attention(event: over_maximum, basis: obs_basis()) == Anomaly +} + +fn w_attention_moves_with_the_basis_not_a_local_constant() -> Bool { + let event = obs_event(transition: Concluded { outcome: Done }, wall_ms: 2000) + let strict = AttentionBasis { average: millisecond(count: 100), maximum: millisecond(count: 200) } + let loose = AttentionBasis { average: millisecond(count: 10000), maximum: millisecond(count: 20000) } + perturbation_consistent( + reading: BackwardDiffInDiffOut, + output_responded: observation_attention(event: event, basis: strict) != observation_attention(event: event, basis: loose) + ) +} + +fn w_unavailable_measurement_carries_a_cause() -> Bool { + let event = obs_event(transition: Concluded { outcome: Done }, wall_ms: 1) + match event.rss { + MeasuredValue { value: _ } => false + MeasuredUnavailable { cause: c } => c != "" + } +} + +fn w_blocked_on_resource_is_derived_for_every_hold() -> Bool { + observation_hold_resource(hold: WindowFull { active: 4, target: 4 }) == AdmissionWindow && + observation_hold_resource(hold: CurrentHighWater { current: byte_size(1), high_water: byte_size(2) }) == MemoryBudget && + observation_hold_resource(hold: PsiPressure { avg10_centi: 900 }) == MemoryBudget && + observation_hold_resource(hold: SwapGrowth { delta: byte_size(1) }) == MemoryBudget && + observation_hold_resource(hold: AwaitFirstCost { undigested: 2 }) == MaturationReserve && + observation_hold_resource(hold: InsufficientHeadroom { current: byte_size(1), share: byte_size(1), high_water: byte_size(2) }) == MemoryBudget && + observation_hold_resource(hold: AdmissionCeiling { current: byte_size(1), ceiling: byte_size(2) }) == MaturationReserve +} + +fn w_blocked_on_carries_holders_and_named_remaining() -> Bool { + let b = observation_blocked_on( + hold: PsiPressure { avg10_centi: 900 }, + holders: [obs_leaf_subject()], + remaining: RemainingUnknown { cause: "governor exposes no bound for reclaim" as NonEmptyStr } + ) + count(b.holders) == 1 && + b.resource == MemoryBudget && + match b.remaining { + RemainingUnknown { cause: c } => c != "" + RemainingBounded { by: _ } => false + } +} + +fn w_escalation_depth_is_recursive_not_one_level() -> Bool { + let t = observation_dwell_threshold + observation_escalation_depth(quiet: millisecond(count: 0), t: t) == 0 && + observation_escalation_depth(quiet: millisecond(count: 60000), t: t) == 1 && + observation_escalation_depth(quiet: millisecond(count: 120000), t: t) == 2 && + observation_escalation_depth(quiet: millisecond(count: 240000), t: t) == 4 +} + +fn w_escalation_surfaces_deeper_subject_over_time() -> Bool { + let t = observation_dwell_threshold + let quiet = obs_entry_subject() + let leaf = obs_leaf_subject() + let at_t = observation_escalation_subject(quiet_subject: quiet, active_leaf: leaf, quiet: millisecond(count: 60000), t: t) + let at_2t = observation_escalation_subject(quiet_subject: quiet, active_leaf: leaf, quiet: millisecond(count: 120000), t: t) + count(at_t.segments) == 4 && + count(at_2t.segments) == 5 && + observation_subject_contains(ancestor: at_t, descendant: at_2t) +} + +fn w_escalation_refuses_to_surface_an_unrelated_subject() -> Bool { + let t = observation_dwell_threshold + let unrelated = ObservationSubject { segments: [RunSegment { id: "other-run" as NonEmptyStr }] } + let surfaced = observation_escalation_subject( + quiet_subject: obs_entry_subject(), + active_leaf: unrelated, + quiet: millisecond(count: 240000), + t: t + ) + count(surfaced.segments) == 3 +} + +fn w_watchdog_converts_quiet_past_budget_into_a_typed_line() -> Bool { + let within = observation_watchdog(subject: obs_entry_subject(), elapsed: millisecond(count: 1000), budget: millisecond(count: 5000)) + let past = observation_watchdog(subject: obs_entry_subject(), elapsed: millisecond(count: 607000), budget: millisecond(count: 60000)) + observation_watchdog_attention(v: within) == Ambient && + observation_watchdog_attention(v: past) == Anomaly && + match past { + WatchdogQuiet => false + WatchdogOrphanedBegin { subject: s, elapsed: _, budget: _ } => count(s.segments) == 3 + } +} + +fn w_subject_identity_is_rendered_not_anonymous() -> Bool { + let rendered = observation_subject_render(subject: obs_leaf_subject()) + string_contains(s: rendered, pattern: "module=v2.compiler.normalized_tree") && + string_contains(s: rendered, pattern: "phase=typecheck") && + string_contains(s: rendered, pattern: "entry=") && + string_contains(s: rendered, pattern: "batch=") +} + +fn w_containment_holds_along_the_path() -> Bool { + observation_subject_contains(ancestor: obs_run_subject(), descendant: obs_leaf_subject()) && + observation_subject_contains(ancestor: obs_entry_subject(), descendant: obs_leaf_subject()) && + !observation_subject_contains(ancestor: obs_leaf_subject(), descendant: obs_run_subject()) +} + +fn w_grain_orders_the_containment_tree() -> Bool { + observation_subject_grain(subject: obs_run_subject()) == RunGrain && + observation_subject_grain(subject: obs_entry_subject()) == EntryGrain && + observation_subject_grain(subject: obs_leaf_subject()) == PhaseGrain && + observation_grain_at_or_above_entry(g: RunGrain) && + observation_grain_at_or_above_entry(g: EntryGrain) && + !observation_grain_at_or_above_entry(g: PhaseGrain) +} + +fn w_presentation_glyph_reads_the_table_not_a_hardcoded_string() -> Bool { + let p = observation_presentation(c: ClassRefused) + match resolve_symbol(id: p.glyph, tier: Unicode) { + Present { value: u } => + match resolve_symbol(id: p.glyph, tier: Ascii) { + Present { value: a } => + perturbation_consistent(reading: BackwardDiffInDiffOut, output_responded: u != a) && + a == "REFUSED" + Absent => false + } + Absent => false + } +} + +fn w_presentation_colour_comes_from_the_glyph_authority() -> Bool { + fold(observation_event_classes, init: true, f: (acc, c) => { + let p = observation_presentation(c: c) + acc && match symbol_color(id: p.glyph) { + Present { value: _ } => true + Absent => false + } + }) +} + +fn w_reward_animal_selection_is_deterministic_and_wraps() -> Bool { + let n = count(reward_animals) + n > 0 && + match reward_animal_at(index: 3, tier: Ascii) { + Present { value: first } => + match reward_animal_at(index: 3, tier: Ascii) { + Present { value: again } => + match reward_animal_at(index: 3 + n, tier: Ascii) { + Present { value: wrapped } => first == again && first == wrapped + Absent => false + } + Absent => false + } + Absent => false + } +} + +fn w_every_law_is_stated_and_bound_to_an_enforcer() -> Bool { + count(observation_laws) == 5 && + fold(observation_laws, init: true, f: (acc, l) => + acc && l.id != "" && l.statement != "" && l.enforced_by.module_path == "std.observation" && l.enforced_by.decl_name != "" + ) +} + +fn w_change_kind_projects_from_the_one_diff_carrier() -> Bool { + let added = observation_change_presentation(hunk: KeyedDiffAddedHunk { hunk_key: "a", to: 1 }) + let removed = observation_change_presentation(hunk: KeyedDiffRemovedHunk { hunk_key: "a", from: 1 }) + let modified = observation_change_presentation(hunk: KeyedDiffModifiedHunk { hunk_key: "a", from: 1, to: 2 }) + added.tag == "new" && + removed.tag == "removed" && + modified.tag == "edited" && + added.symbol == ChangeAdded && + removed.symbol == ChangeRemoved && + modified.symbol == ChangeModified +} + +fn w_change_kind_colour_follows_git_diff_convention() -> Bool { + match symbol_color(id: ChangeAdded) { + Present { value: a } => + match symbol_color(id: ChangeModified) { + Present { value: m } => + match symbol_color(id: ChangeRemoved) { + Present { value: r } => a == Success && m == Warning && r == Error + Absent => false + } + Absent => false + } + Absent => false + } +} + +fn w_change_kind_degrades_losslessly_without_colour() -> Bool { + let added = observation_change_presentation(hunk: KeyedDiffAddedHunk { hunk_key: "a", to: 1 }) + let removed = observation_change_presentation(hunk: KeyedDiffRemovedHunk { hunk_key: "a", from: 1 }) + let modified = observation_change_presentation(hunk: KeyedDiffModifiedHunk { hunk_key: "a", from: 1, to: 2 }) + added.tag != "" && removed.tag != "" && modified.tag != "" && + added.tag != removed.tag && added.tag != modified.tag && removed.tag != modified.tag +} + +fn w_no_op_kinds_each_explain_themselves() -> Bool { + let kinds = [ + DocsPolicy, + Uncovered { declaration: "std.observation.observation_attention" as NonEmptyStr }, + NoDeclsTouched, + GeneratedArtifact { drift_gate_authority: "gunbc.ci_workflow" as NonEmptyStr }, + DepartedPath { cause: "src/v1/stage0/src/gone.rs" as NonEmptyStr } + ] + count(kinds) == 5 && + fold(kinds, init: true, f: (acc, n) => { + acc && + observation_no_op_explanation(n: n) != "" && + match symbol_color(id: observation_no_op_symbol(n: n)) { + Present { value: _ } => true + Absent => false + } + }) +} + +fn w_uncovered_is_a_nudge_not_a_comfort() -> Bool { + observation_no_op_attention(n: Uncovered { declaration: "std.observation.observation_watchdog" as NonEmptyStr }) == Notable && + observation_no_op_attention(n: DocsPolicy) == Ambient && + observation_no_op_symbol(n: Uncovered { declaration: "x" as NonEmptyStr }) == StatusWarning +} + +fn w_departed_path_is_not_a_no_op() -> Bool { + observation_no_op_is_actually_a_widen(n: DepartedPath { cause: "deleted module" as NonEmptyStr }) && + !observation_no_op_is_actually_a_widen(n: DocsPolicy) && + !observation_no_op_is_actually_a_widen(n: NoDeclsTouched) && + !observation_no_op_is_actually_a_widen(n: GeneratedArtifact { drift_gate_authority: "x" as NonEmptyStr }) && + !observation_no_op_is_actually_a_widen(n: Uncovered { declaration: "x" as NonEmptyStr }) && + observation_no_op_attention(n: DepartedPath { cause: "deleted module" as NonEmptyStr }) == Notable +} + +fn w_status_pulse_glyph_is_available_to_the_heartbeat() -> Bool { + match resolve_symbol(id: StatusPulse, tier: Emoji) { + Present { value: e } => e == "🕐" + Absent => false + } +} + +test fn observation_model_witnesses() -> Bool { + w_presentation_is_total_over_class_roster() && + w_class_roster_covers_every_constructed_class() && + w_refused_is_distinct_from_failed() && + w_anomaly_classes_never_collapse() && + w_routine_progress_collapses_but_the_summary_always_shows() && + w_refusal_is_anomaly_on_any_basis() && + w_attention_grounds_on_supplied_basis() && + w_attention_moves_with_the_basis_not_a_local_constant() && + w_unavailable_measurement_carries_a_cause() && + w_blocked_on_resource_is_derived_for_every_hold() && + w_blocked_on_carries_holders_and_named_remaining() && + w_escalation_depth_is_recursive_not_one_level() && + w_escalation_surfaces_deeper_subject_over_time() && + w_escalation_refuses_to_surface_an_unrelated_subject() && + w_watchdog_converts_quiet_past_budget_into_a_typed_line() && + w_subject_identity_is_rendered_not_anonymous() && + w_containment_holds_along_the_path() && + w_grain_orders_the_containment_tree() && + w_presentation_glyph_reads_the_table_not_a_hardcoded_string() && + w_presentation_colour_comes_from_the_glyph_authority() && + w_reward_animal_selection_is_deterministic_and_wraps() && + w_every_law_is_stated_and_bound_to_an_enforcer() && + w_change_kind_projects_from_the_one_diff_carrier() && + w_change_kind_colour_follows_git_diff_convention() && + w_change_kind_degrades_losslessly_without_colour() && + w_no_op_kinds_each_explain_themselves() && + w_uncovered_is_a_nudge_not_a_comfort() && + w_departed_path_is_not_a_no_op() && + w_status_pulse_glyph_is_available_to_the_heartbeat() +} From 8cbf49c715bbed0f8d5fc6827120250015f96cbe Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 16:09:20 +0000 Subject: [PATCH 05/39] Review 42157: collapse the duplicate display tables into one authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the blocking finding on the three coproduct predicates, taking its stronger alternative (dissolve into the canonical surface) rather than only its weaker one (add a disposition receipt). The finding was right about the real defect: observation_class_is_intrinsic_anomaly was a second hand-written table beside observation_presentation, and a witness asserted the two agreed. That witness was validation standing exactly where construction was available — it conceded the tables could disagree and promised to notice. Fix: ObservationDensity (RoutineCollapsible | SummaryAlwaysShown | AnomalyExpanded) is now the one table. collapsible, expands_fully and intrinsic-anomaly are all derived projections of it, so disagreement is unwritable rather than detected. The three display states stay distinct — the run summary is neither routine nor an anomaly, which a single boolean would have forced it to borrow. Disposition receipts added for the remaining structural readers, matching the cited materialization_ladder pattern: outcome/class, subject/grain/hold, and selection no-op. Each states why it is Terminal and names its discriminating corpus rather than asserting terminality. Witness roles now separated and both proven by execution: - w_density_is_the_single_display_authority — the content check; reds when a class's density changes (verified: ClassRefused → RoutineCollapsible reds it, and reds ONLY it, since the projections cannot disagree) - w_presentation_projects_density_rather_than_restating_it plus the two collapse witnesses — construction guards; red when the presentation stops projecting density (verified: hardcoding collapsible: true reds all three) Full suite green: 30 model conjuncts, 6 lockstep conjuncts. Compile-clean unchanged — 47 errors, all pre-existing in std/measure.dag (46) and std/effect_grant.dag (1), zero attributable here. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/std/observation.dag | 92 ++++++++++++++----- .../claim/observation_model_witness_test.dag | 34 +++++++ 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/dag/std/observation.dag b/dag/std/observation.dag index 9091b96c8a3..49a24e51cd1 100644 --- a/dag/std/observation.dag +++ b/dag/std/observation.dag @@ -295,33 +295,69 @@ data observation_collapse_axes_note: String = "collapsible and expands_fully are data observation_presentation_note: String = "Event class to presentation is a TOTAL assignment over the class roster, censused and walled the way the register censuses unthemed colors. The presentation carries only the SymbolId: the glyph text per tier and the register colour role are read from the ONE glyph authority (std.symbols identity, extdeps.render.glyphs rows), never restated here, so perturbing a glyph row moves every renderer and a renderer that hardcodes a glyph is provably not reading the table. collapsible and expands_fully carry law 4: sub-threshold work collapses, an anomaly expands fully and names itself, and the two flags are never both permissive for an intrinsically anomalous class." +type ObservationDensity + = RoutineCollapsible + | SummaryAlwaysShown + | AnomalyExpanded + +data observation_density_single_authority_note: String = "The three display states of law 4 live in ONE table. Density is the authority; collapsible, expands_fully and intrinsic-anomaly are all DERIVED from it. The earlier shape carried two hand-written tables — a class-to-anomaly predicate beside a class-to-presentation assignment — with a witness asserting they agreed. That witness was validation standing exactly where construction was available (DESIGN section 5): it conceded the two tables could disagree and merely promised to notice. They cannot disagree now, because there is only one of them, and the review finding that named this duplication is what collapsed it." + +fn observation_class_density(c: ObservationEventClass) -> ObservationDensity { + match c { + ClassBegin => RoutineCollapsible + ClassStep => RoutineCollapsible + ClassDone => RoutineCollapsible + ClassRefused => AnomalyExpanded + ClassFailed => AnomalyExpanded + ClassTimedOut => AnomalyExpanded + ClassSkipped => RoutineCollapsible + ClassFinal => SummaryAlwaysShown + ClassBlocked => AnomalyExpanded + ClassDwellEscalation => AnomalyExpanded + } +} + +fn observation_density_collapsible(d: ObservationDensity) -> Bool { + match d { + RoutineCollapsible => true + SummaryAlwaysShown => false + AnomalyExpanded => false + } +} + +fn observation_density_expands_fully(d: ObservationDensity) -> Bool { + match d { + RoutineCollapsible => false + SummaryAlwaysShown => false + AnomalyExpanded => true + } +} + fn observation_class_is_intrinsic_anomaly(c: ObservationEventClass) -> Bool { + observation_density_expands_fully(d: observation_class_density(c: c)) +} + +fn observation_class_glyph(c: ObservationEventClass) -> SymbolId { match c { - ClassBegin => false - ClassStep => false - ClassDone => false - ClassRefused => true - ClassFailed => true - ClassTimedOut => true - ClassSkipped => false - ClassFinal => false - ClassBlocked => true - ClassDwellEscalation => true + ClassBegin => NodeRunning + ClassStep => NodeRunning + ClassDone => NodeCompleted + ClassRefused => StatusRefused + ClassFailed => NodeFailed + ClassTimedOut => StatusTimedOut + ClassSkipped => NodeSkipped + ClassFinal => StatusFinal + ClassBlocked => StatusBlocked + ClassDwellEscalation => StatusDwell } } fn observation_presentation(c: ObservationEventClass) -> ObservationPresentation { - match c { - ClassBegin => ObservationPresentation { glyph: NodeRunning, collapsible: true, expands_fully: false } - ClassStep => ObservationPresentation { glyph: NodeRunning, collapsible: true, expands_fully: false } - ClassDone => ObservationPresentation { glyph: NodeCompleted, collapsible: true, expands_fully: false } - ClassRefused => ObservationPresentation { glyph: StatusRefused, collapsible: false, expands_fully: true } - ClassFailed => ObservationPresentation { glyph: NodeFailed, collapsible: false, expands_fully: true } - ClassTimedOut => ObservationPresentation { glyph: StatusTimedOut, collapsible: false, expands_fully: true } - ClassSkipped => ObservationPresentation { glyph: NodeSkipped, collapsible: true, expands_fully: false } - ClassFinal => ObservationPresentation { glyph: StatusFinal, collapsible: false, expands_fully: false } - ClassBlocked => ObservationPresentation { glyph: StatusBlocked, collapsible: false, expands_fully: true } - ClassDwellEscalation => ObservationPresentation { glyph: StatusDwell, collapsible: false, expands_fully: true } + let density = observation_class_density(c: c) + ObservationPresentation { + glyph: observation_class_glyph(c: c), + collapsible: observation_density_collapsible(d: density), + expands_fully: observation_density_expands_fully(d: density) } } @@ -457,6 +493,8 @@ fn observation_watchdog(subject: ObservationSubject, elapsed: Millisecond, budge } } +data observation_empty_subject_note: String = "The empty-path arm NAMES the absence rather than inventing a subject, and it does not widen: unattributed is a label for a caller that built a subject with no identity, not a plausible substitute for one. It is not the absorbing fallback the house forbids — nothing is rerun, nothing is scanned wholesale, and the line is as loud as any other. It is nonetheless a CONSTRUCTION GAP, declared rather than left silent: the right shape is a segment list that cannot be empty, which the substrate cannot express today — a record-field where clause over a list is named future work in the where-clause lowering plan, and no list refinement exists in the corpus. Dissolve-on: when that lowering lands, ObservationSubject.segments takes the non-empty refinement, this arm becomes unreachable by construction, and it deletes." + fn observation_subject_render(subject: ObservationSubject) -> NonEmptyStr { match join(observation_subject_path(subject: subject), " ") { "" => "run=unattributed" as NonEmptyStr @@ -537,6 +575,18 @@ data observation_laws: List = [ observation_law_every_response_true ] +data observation_outcome_predicate_contract: Disposition = Terminal { + reason: "Structural coproduct readers over ObservationOutcome and ObservationEventClass (observation_outcome_is_anomaly, observation_outcome_class, observation_event_class, observation_class_density and the density projections) — closed by the declared outcome and class variants, not interim scaffolds. Terminal because the sums are closed by construction and the class roster is censused: a new variant cannot be added without the totality witness naming it. Discriminating corpus: dag/test/claim/observation_model_witness_test.dag, which reds when a class loses its presentation, when Refused and Failed collapse onto one glyph, and when the density authority disagrees with what the presentation projects." +} + +data observation_subject_predicate_contract: Disposition = Terminal { + reason: "Structural readers over ObservationSegment, ObservationGrain and SchedulerHold (observation_grain_of_segment, observation_grain_depth, observation_subject_grain, observation_grain_at_or_above_entry, observation_hold_resource) — closed by declared variants. The hold reader is Terminal on the .dag side and additionally pinned to its realization: the lockstep witness reads the seed governor by execution, so a variant added on one side without the other reds rather than drifting." +} + +data observation_selection_predicate_contract: Disposition = Terminal { + reason: "Structural coproduct readers over SelectionNoOp (observation_no_op_is_actually_a_widen, observation_no_op_attention, observation_no_op_symbol, observation_no_op_explanation) — closed by the declared no-op variants. Terminal, and load-bearing rather than decorative: the widen reader is what keeps a departed path from being rendered as a no-op, which is the distinction the operator's closed-sum ruling exists to preserve." +} + data observation_renders_nothing_disposition: Disposition = Terminal { reason: "P0 carries the model only. Renderers are P1 and later, and the CI renderer's wiring lands after the atomic CI PR because both touch the floor's emit sites and churning them twice is the migration class the operator ruled out. This module therefore holds no line formatting, no glyph text, and no ANSI: it holds the vocabulary those renderers project from." } diff --git a/dag/test/claim/observation_model_witness_test.dag b/dag/test/claim/observation_model_witness_test.dag index 441501036ec..bcab6b61f3c 100644 --- a/dag/test/claim/observation_model_witness_test.dag +++ b/dag/test/claim/observation_model_witness_test.dag @@ -97,6 +97,14 @@ import std.observation { observation_event_classes, observation_event_class, observation_class_is_intrinsic_anomaly, + ObservationDensity, + RoutineCollapsible, + SummaryAlwaysShown, + AnomalyExpanded, + observation_class_density, + observation_density_collapsible, + observation_density_expands_fully, + observation_class_glyph, ObservationPresentation, observation_presentation, DwellThreshold, @@ -229,6 +237,30 @@ fn w_anomaly_classes_never_collapse() -> Bool { }) } +fn w_density_is_the_single_display_authority() -> Bool { + observation_class_density(c: ClassRefused) == AnomalyExpanded && + observation_class_density(c: ClassFailed) == AnomalyExpanded && + observation_class_density(c: ClassTimedOut) == AnomalyExpanded && + observation_class_density(c: ClassBlocked) == AnomalyExpanded && + observation_class_density(c: ClassDwellEscalation) == AnomalyExpanded && + observation_class_density(c: ClassFinal) == SummaryAlwaysShown && + observation_class_density(c: ClassBegin) == RoutineCollapsible && + observation_class_density(c: ClassStep) == RoutineCollapsible && + observation_class_density(c: ClassDone) == RoutineCollapsible && + observation_class_density(c: ClassSkipped) == RoutineCollapsible +} + +fn w_presentation_projects_density_rather_than_restating_it() -> Bool { + fold(observation_event_classes, init: true, f: (acc, c) => { + let p = observation_presentation(c: c) + let d = observation_class_density(c: c) + acc && + p.collapsible == observation_density_collapsible(d: d) && + p.expands_fully == observation_density_expands_fully(d: d) && + observation_class_is_intrinsic_anomaly(c: c) == observation_density_expands_fully(d: d) + }) +} + fn w_routine_progress_collapses_but_the_summary_always_shows() -> Bool { observation_presentation(c: ClassBegin).collapsible && observation_presentation(c: ClassStep).collapsible && @@ -490,6 +522,8 @@ test fn observation_model_witnesses() -> Bool { w_class_roster_covers_every_constructed_class() && w_refused_is_distinct_from_failed() && w_anomaly_classes_never_collapse() && + w_density_is_the_single_display_authority() && + w_presentation_projects_density_rather_than_restating_it() && w_routine_progress_collapses_but_the_summary_always_shows() && w_refusal_is_anomaly_on_any_basis() && w_attention_grounds_on_supplied_basis() && From d26d5e08130bdc337ab043ed41e965ce572a2eb1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 16:29:14 +0000 Subject: [PATCH 06/39] Review 42166: ground PSI avg10 on std.measure.BasisPoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The finding is correct and the hard-blocker applies: avg10_centi was a percentage magnitude with a scale carried on bare Nat, and the note beside it self-justified the conflation rather than tracking it. Fix consumes an EXISTING carrier rather than minting one, which the corpus had already asked for in advance. std.measure.basis_point_dissolve_on warns: "else a third dimensionless-ratio use-case mints a third nickname" — this module is exactly that third use-case, so minting PerMille or a Percentage quantity would have walked into the failure the row names. BasisPoint's own note declares its semantic axis as utilization ratios, which is what a PSI stall share is. Resolution is exact rather than truncated: the governor reads one decimal of a percentage and a basis point is a hundredth of a percentage point, so 37.5 percent is 3750 with nothing lost. Percent (Dimensionless, One) would have truncated it. Note rewritten to state the grounding and the deliberate non-mint, replacing the self-justification. When the Ratio unification that dissolve-on calls for lands, this field follows Percent and BasisPoint onto it with no change of meaning. Suite green: 31 model conjuncts, 6 lockstep conjuncts. Compile-clean unchanged — 47 pre-existing errors in std/measure.dag (46) and std/effect_grant.dag (1), zero attributable here. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/std/observation.dag | 10 ++++++---- dag/test/claim/observation_model_witness_test.dag | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/dag/std/observation.dag b/dag/std/observation.dag index 49a24e51cd1..797083831c4 100644 --- a/dag/std/observation.dag +++ b/dag/std/observation.dag @@ -2,7 +2,7 @@ module std.observation import std.types { String, NonEmptyStr, Bool, List } import std.nat { Nat } -import std.measure { Millisecond, ByteSize, measure_le, millisecond, millisecond_count } +import std.measure { Millisecond, ByteSize, BasisPoint, measure_le, millisecond, millisecond_count } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.symbols { SymbolId, @@ -143,13 +143,15 @@ type ObservationEvent { type SchedulerHold = WindowFull { active: Nat, target: Nat } | CurrentHighWater { current: ByteSize, high_water: ByteSize } - | PsiPressure { avg10_centi: Nat } + | PsiPressure { avg10: BasisPoint } | SwapGrowth { delta: ByteSize } | AwaitFirstCost { undigested: Nat } | InsufficientHeadroom { current: ByteSize, share: ByteSize, high_water: ByteSize } | AdmissionCeiling { current: ByteSize, ceiling: ByteSize } -data observation_scheduler_hold_note: String = "SchedulerHold is the substrate authority for why admission was withheld this poll; the seed's v1_compiler memory_governor HoldReason is the realization that must stay in lockstep with it (the .dag graph is the authority and Rust is the seed — DESIGN section 7). The lockstep witness proves arity and variant names match by reading the live seed, so a variant added on one side and not the other reds instead of drifting. PsiPressure carries centi-units of the avg10 percentage rather than a float: the pressure reading is a magnitude with a scale, and a bare float in a rendered line is the scalar/unit conflation the house hard-blocks." +data observation_scheduler_hold_note: String = "SchedulerHold is the substrate authority for why admission was withheld this poll; the seed's v1_compiler memory_governor HoldReason is the realization that must stay in lockstep with it (the .dag graph is the authority and Rust is the seed). The lockstep witness proves arity and variant names match by reading the live seed, so a variant added on one side and not the other reds instead of drifting." + +data observation_psi_pressure_unit_note: String = "avg10 is a MAGNITUDE WITH A SCALE and is carried as one: std.measure.BasisPoint, not a bare scalar. The reading is a utilization ratio — the share of a ten-second window in which work stalled on memory — which is the exact semantic axis basis_point_unit_note names for that carrier, so this is a consumer of an existing authority rather than a new unit. Resolution is exact at the seed's fidelity: the governor reads one decimal of a percentage, and one basis point is a hundredth of a percentage point, so 37.5 percent is 3750 with nothing truncated. NO NEW CARRIER IS MINTED HERE DELIBERATELY: basis_point_dissolve_on already warns that a third dimensionless-ratio use-case minting a third nickname is the failure mode, and this module is that third use-case. When the Ratio unification that row calls for lands, this field follows Percent and BasisPoint onto it and needs no change of meaning." type ContendedResource = MemoryBudget @@ -170,7 +172,7 @@ fn observation_hold_resource(hold: SchedulerHold) -> ContendedResource { match hold { WindowFull { active: _, target: _ } => AdmissionWindow CurrentHighWater { current: _, high_water: _ } => MemoryBudget - PsiPressure { avg10_centi: _ } => MemoryBudget + PsiPressure { avg10: _ } => MemoryBudget SwapGrowth { delta: _ } => MemoryBudget AwaitFirstCost { undigested: _ } => MaturationReserve InsufficientHeadroom { current: _, share: _, high_water: _ } => MemoryBudget diff --git a/dag/test/claim/observation_model_witness_test.dag b/dag/test/claim/observation_model_witness_test.dag index bcab6b61f3c..7d61d907250 100644 --- a/dag/test/claim/observation_model_witness_test.dag +++ b/dag/test/claim/observation_model_witness_test.dag @@ -19,7 +19,7 @@ import std.symbols { ChangeRemoved, } import std.change { KeyedDiffAddedHunk, KeyedDiffRemovedHunk, KeyedDiffModifiedHunk } -import std.measure { Millisecond, millisecond, ByteSize, byte_size } +import std.measure { Millisecond, millisecond, ByteSize, byte_size, BasisPoint, basis_point, basis_point_count } import std.perturbation { BackwardDiffInDiffOut, ForwardSameInSameOut, perturbation_consistent } import extdeps.render.glyphs { resolve_symbol, symbol_color, reward_animal_at, reward_animals } import std.observation { @@ -307,7 +307,7 @@ fn w_unavailable_measurement_carries_a_cause() -> Bool { fn w_blocked_on_resource_is_derived_for_every_hold() -> Bool { observation_hold_resource(hold: WindowFull { active: 4, target: 4 }) == AdmissionWindow && observation_hold_resource(hold: CurrentHighWater { current: byte_size(1), high_water: byte_size(2) }) == MemoryBudget && - observation_hold_resource(hold: PsiPressure { avg10_centi: 900 }) == MemoryBudget && + observation_hold_resource(hold: PsiPressure { avg10: basis_point(count: 900) }) == MemoryBudget && observation_hold_resource(hold: SwapGrowth { delta: byte_size(1) }) == MemoryBudget && observation_hold_resource(hold: AwaitFirstCost { undigested: 2 }) == MaturationReserve && observation_hold_resource(hold: InsufficientHeadroom { current: byte_size(1), share: byte_size(1), high_water: byte_size(2) }) == MemoryBudget && @@ -316,7 +316,7 @@ fn w_blocked_on_resource_is_derived_for_every_hold() -> Bool { fn w_blocked_on_carries_holders_and_named_remaining() -> Bool { let b = observation_blocked_on( - hold: PsiPressure { avg10_centi: 900 }, + hold: PsiPressure { avg10: basis_point(count: 900) }, holders: [obs_leaf_subject()], remaining: RemainingUnknown { cause: "governor exposes no bound for reclaim" as NonEmptyStr } ) @@ -510,6 +510,14 @@ fn w_departed_path_is_not_a_no_op() -> Bool { observation_no_op_attention(n: DepartedPath { cause: "deleted module" as NonEmptyStr }) == Notable } +fn w_psi_pressure_carries_a_scaled_magnitude_not_a_scalar() -> Bool { + let hold = PsiPressure { avg10: basis_point(count: 3750) } + match hold { + PsiPressure { avg10: r } => basis_point_count(bp: r) == 3750 + _ => false + } +} + fn w_status_pulse_glyph_is_available_to_the_heartbeat() -> Bool { match resolve_symbol(id: StatusPulse, tier: Emoji) { Present { value: e } => e == "🕐" @@ -548,5 +556,6 @@ test fn observation_model_witnesses() -> Bool { w_no_op_kinds_each_explain_themselves() && w_uncovered_is_a_nudge_not_a_comfort() && w_departed_path_is_not_a_no_op() && + w_psi_pressure_carries_a_scaled_magnitude_not_a_scalar() && w_status_pulse_glyph_is_available_to_the_heartbeat() } From d1987c6d9463a9eb371ba2a5c91397b9324ba93c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 16:54:46 +0000 Subject: [PATCH 07/39] =?UTF-8?q?Piece=203:=20derived=20per-batch=20floor?= =?UTF-8?q?=20clamp=20=E2=80=94=20delete=20the=20static=20wall-budget=20li?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ci-two-tier-placement-redesign.md §9.8 (operator 2026-07-24). Replaces the hand-set gunbc_ci_floor_batch_wall_budget_seconds list — a scalar wall budget that conflated workload size (diff-proportional selection), host speed, and per-unit cost creep — with a per-batch clamp computed at run time: clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms. The load-bearing row is the discovery witness batch (index 2): 300s + 1000ms per witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, with headroom over the observed 1344-1629s walls) while a runaway reds proportionally, instead of the fixed 1320s that redded legitimate hub-file PRs. Fixed-count gate batches carry rate 0 at their measured basis; index 3 (wet corpora) stays a declared fixed overhead pending the D2 probe's wet-per-witness rate. Authority: gunbc.ci_spec FloorBatchClamp + gunbc_ci_floor_batch_clamp_params (index-aligned to the 7 batches; the cover-schedule witness pins the alignment) + gunbc_ci_floor_batch_clamp_note (carries the raise discipline from the kept static-era note). The 5s per-WITNESS max is unchanged — still the single gunbc_ci_fast_lane_eval_budget_ms authority, never redefined here. claim_executor reads the two index-aligned param lists fail-closed, derives the per-batch unit count from batch_results (corpus_witnesses for discovery aggregates, 1 per gate row — the runtime datum the static list ignored), computes the clamp at enforcement, and refuses over-clamp as a typed FLOOR-BATCH-OVER-BUDGET (never a widen). The GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS RED-control hook now lowers the COMPUTED clamp. The receipt emits batch_N_units / batch_N_clamp_ms / verdict; its unit test is updated. Both run_walk call sites carry the new param. Verified by execution: build clean; ci_floor_plan_witnesses green (the three new clamp witnesses + cover-schedule). The receipt verdict unit test and the fixture RED control (budget_red_control_plan; TIGHTEN_MS=0 -> clamp 0 -> the FLOOR-BATCH-OVER-BUDGET refusal) are the e2e enforcement confirmations; the fixture's control witness triggers a whole-tree emit that OOMs alongside a compile in this container, so both run as a clean post-commit confirmation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/ci_layer_roots.dag | 2 +- dag/gunbc/ci_spec.dag | 19 +- dag/gunbc/ci_workflow.dag | 2 +- src/v1/stage0/src/bin/claim_executor.rs | 266 +++++++++++------- .../test/claim/ci_floor_plan_witness_test.dag | 10 +- .../floor_skip/budget_red_control_plan.dag | 8 +- src/v2/workflow/ci_floor_plan.dag | 22 +- 7 files changed, 213 insertions(+), 116 deletions(-) diff --git a/dag/gunbc/ci_layer_roots.dag b/dag/gunbc/ci_layer_roots.dag index 4335116425e..6f0ff633066 100644 --- a/dag/gunbc/ci_layer_roots.dag +++ b/dag/gunbc/ci_layer_roots.dag @@ -738,7 +738,7 @@ data bin_witness_wet_entries: List = [ data bin_witness_wet_per_row_wall_budget_seconds: Int = 60 -data bin_witness_wet_per_row_budget_note: String = "Short/quick-witness discipline for the per-PR wet batch (CI floor endgame D6, operator ruling 2026-07-23): per-PR CI is short/quick witnesses, and a bin-execution row's subprocess wall is invisible to the fast-lane 5-second EVAL deadline by construction (the deadline counts interpreter CPU; child-process wall is host-effect time), so the bin lane carries its own per-row wall budget — this datum. A row whose MEASURED wall exceeds it re-homes to the falsifier wet cadence as a typed frontier row (falsifier_rehomed_bin_wet_rows below, reason + dissolve_on per row), keeping the remaining rows as the per-PR smoke subset so plumbing-diff PRs retain a discriminating signal. Classification basis: run 30009199696's per-row cost table (the 54-row pool's 561s eval was 94 percent three rows: floor_skip_discovery 289s, cross_shard_seam live-tree 183s, dag_compile_clean_perturb 53s — the third is under budget and stays). Enforcement layering: this datum is the classification rule applied at review; the EXECUTABLE wall against silent regrowth is the batch-wall budget (gunbc.ci_spec gunbc_ci_floor_batch_wall_budget_seconds — a new heavy row blows the wet batch's budget and reds with a typed refusal), so an over-budget enrollment cannot ride silently even if review misses it." +data bin_witness_wet_per_row_budget_note: String = "Short/quick-witness discipline for the per-PR wet batch (CI floor endgame D6, operator ruling 2026-07-23): per-PR CI is short/quick witnesses, and a bin-execution row's subprocess wall is invisible to the fast-lane 5-second EVAL deadline by construction (the deadline counts interpreter CPU; child-process wall is host-effect time), so the bin lane carries its own per-row wall budget — this datum. A row whose MEASURED wall exceeds it re-homes to the falsifier wet cadence as a typed frontier row (falsifier_rehomed_bin_wet_rows below, reason + dissolve_on per row), keeping the remaining rows as the per-PR smoke subset so plumbing-diff PRs retain a discriminating signal. Classification basis: run 30009199696's per-row cost table (the 54-row pool's 561s eval was 94 percent three rows: floor_skip_discovery 289s, cross_shard_seam live-tree 183s, dag_compile_clean_perturb 53s — the third is under budget and stays). Enforcement layering: this datum is the classification rule applied at review; the EXECUTABLE wall against silent regrowth is the batch clamp (gunbc.ci_spec gunbc_ci_floor_batch_clamp_params — a new heavy row grows the wet batch's unit count and can blow its derived clamp, reding with a typed refusal), so an over-budget enrollment cannot ride silently even if review misses it." type RehomedBinWetRow { entry: String diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index a6e0218ca70..a4404a66851 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -173,9 +173,24 @@ fn ci_spec_with_discovery_scan_dirs(spec: CiSpec, dirs: List) -> CiSpec } } -data gunbc_ci_floor_batch_wall_budget_note: String = "THE COST WALL (CI floor endgame D5, operator brief 2026-07-23 — the resolve-regression journey's section-3 finding made mechanism: nothing redded a merge that added floor minutes, so every recovered minute was re-spent by the next lane's enrollment, and the step timeout's own bounce history 30 -> 60 -> 90 -> 120 -> 180 -> 270 -> 55 is the record of the budget being raised to fit). Per-BATCH wall budgets for gunbc_ci_floor_batches, enforced by claim_executor at walk time: each batch's measured wall is recorded as a typed receipt row (target/floor-batch-wall-receipt.txt), and a batch over its budget is a typed, located refusal — FLOOR-BATCH-OVER-BUDGET naming the batch index, measured wall, budget row, and this carrier — that reds the walk. A failure arm refuses, never widens: over-budget never triggers a rerun, a wider scope, or a silent cap raise. Denominated PER-BATCH, never per-run, because plumbing PRs have a structurally different cost profile (attribution doc section 9.2: a PR touching host_prelude/cli_run legitimately runs 46 of 55 wet rows — a per-run wall would red every such PR spuriously; per-batch budgets absorb the profile where it lands). RULING RECONCILIATION (the operator dispatching the 2026-07-23 endgame brief is the sign-off on this reading): the standing rule 'no wall-clock term in any verdict' (ci_floor_materialization_receipt_note, operator ruling 2026-07-10) governs WITNESS VERDICTS — a witness's pass/fail must never depend on how long it took. Batch budgets do not touch witness verdicts: they are admission/scheduling facts at the walk grain, the same split the 5-second fast-lane eval law already uses (gunbc_ci_fast_lane_rule_note: the deadline is lane admission, not a verdict term), so a batch refusal names the BATCH, and every witness verdict inside it stands as evaluated. RAISE DISCIPLINE: raising any budget requires appending a dated receipt note here naming the run id and the enrollment that grew the batch — the same discipline as the timeout-note bounce history — never a silent number edit. Budget basis (run 30009199696, post-#7122, with the endgame fixes priced in): batch 1 cheap gates 185s measured -> 125s post-pool, budget 240; batch 2 receipt consume ~0s, budget 60; batch 3 discovery 1024s measured (corpus-denominated resolve wall, lever-1 re-diagnosis pending), budget 1320; batch 4 wet corpora 649s plumbing-profile measured -> ~175s post-re-home, budget 420; batch 5 emit-host 7s, budget 120; batch 6 ingest 414s measured (4 composed overlay children — the genuine mktemp constraint), budget 600; batch 7 reads-real-bytes 206s, budget 420. Sum 3180s = 53min under the 55min step cap. OPERATOR SIGNATURE (briansrls, 2026-07-23): the admission/verdict reading above is affirmed — per-batch wall budgets are scheduling-admission facts, not wall-clock terms in witness verdicts; this line is the declared human-signed exemption row the 2026-07-10 ruling requires. Raise discipline amended: RAISING any budget row requires a new dated operator-signed line here naming the run id and the enrollment that grew the batch; TIGHTENING may land by ordinary receipt note. The on-call remedy for FLOOR-BATCH-OVER-BUDGET is diagnose-or-signed-raise, never rerun. FOLLOW-UP ROWS (operator rework push, 2026-07-23): (a) PRELUDE COVERAGE HOLE — the ~5min before batch-1 arms (naming-hygiene walk, policy install, plan resolve/eval, governor arm, eager compile-clean install) sits OUTSIDE every batch budget and can only red at the 55min step cap; a prelude budget row lands when the phase_mark walls get their own receipt keys. (b) IDENTITY-KEYED BUDGETS — rows are keyed by batch INDEX today (the coverage witness pins length, so a schedule change reds loudly rather than misassigning); keying by batch content-identity dissolves the index coupling and rides the ComputationIdentity lane. (c) K-CAP UNION-RSS RECEIPT — cheap_gate_pool_max_claims_per_child=16 is provisional until the pooled child's union RSS is receipted (claim_batch already prints per-shard-peak-rss; the first CI runs' logs back or re-size the cap; local proxy receipt: the 4-gate outer child peaked 1.82GB with the 12-claim pool nested)." +data gunbc_ci_floor_batch_wall_budget_note: String = "SUPERSEDED by the derived clamp gunbc_ci_floor_batch_clamp_note (Piece 3, 2026-07-24): the hand-set gunbc_ci_floor_batch_wall_budget_seconds list this note governed is deleted, replaced by overhead + units*rate computed at run time; this note is kept for the operator-signed static-era history and the raise discipline, which carries forward to the clamp constants. THE COST WALL (CI floor endgame D5, operator brief 2026-07-23 — the resolve-regression journey's section-3 finding made mechanism: nothing redded a merge that added floor minutes, so every recovered minute was re-spent by the next lane's enrollment, and the step timeout's own bounce history 30 -> 60 -> 90 -> 120 -> 180 -> 270 -> 55 is the record of the budget being raised to fit). Per-BATCH wall budgets for gunbc_ci_floor_batches, enforced by claim_executor at walk time: each batch's measured wall is recorded as a typed receipt row (target/floor-batch-wall-receipt.txt), and a batch over its budget is a typed, located refusal — FLOOR-BATCH-OVER-BUDGET naming the batch index, measured wall, budget row, and this carrier — that reds the walk. A failure arm refuses, never widens: over-budget never triggers a rerun, a wider scope, or a silent cap raise. Denominated PER-BATCH, never per-run, because plumbing PRs have a structurally different cost profile (attribution doc section 9.2: a PR touching host_prelude/cli_run legitimately runs 46 of 55 wet rows — a per-run wall would red every such PR spuriously; per-batch budgets absorb the profile where it lands). RULING RECONCILIATION (the operator dispatching the 2026-07-23 endgame brief is the sign-off on this reading): the standing rule 'no wall-clock term in any verdict' (ci_floor_materialization_receipt_note, operator ruling 2026-07-10) governs WITNESS VERDICTS — a witness's pass/fail must never depend on how long it took. Batch budgets do not touch witness verdicts: they are admission/scheduling facts at the walk grain, the same split the 5-second fast-lane eval law already uses (gunbc_ci_fast_lane_rule_note: the deadline is lane admission, not a verdict term), so a batch refusal names the BATCH, and every witness verdict inside it stands as evaluated. RAISE DISCIPLINE: raising any budget requires appending a dated receipt note here naming the run id and the enrollment that grew the batch — the same discipline as the timeout-note bounce history — never a silent number edit. Budget basis (run 30009199696, post-#7122, with the endgame fixes priced in): batch 1 cheap gates 185s measured -> 125s post-pool, budget 240; batch 2 receipt consume ~0s, budget 60; batch 3 discovery 1024s measured (corpus-denominated resolve wall, lever-1 re-diagnosis pending), budget 1320; batch 4 wet corpora 649s plumbing-profile measured -> ~175s post-re-home, budget 420; batch 5 emit-host 7s, budget 120; batch 6 ingest 414s measured (4 composed overlay children — the genuine mktemp constraint), budget 600; batch 7 reads-real-bytes 206s, budget 420. Sum 3180s = 53min under the 55min step cap. OPERATOR SIGNATURE (briansrls, 2026-07-23): the admission/verdict reading above is affirmed — per-batch wall budgets are scheduling-admission facts, not wall-clock terms in witness verdicts; this line is the declared human-signed exemption row the 2026-07-10 ruling requires. Raise discipline amended: RAISING any budget row requires a new dated operator-signed line here naming the run id and the enrollment that grew the batch; TIGHTENING may land by ordinary receipt note. The on-call remedy for FLOOR-BATCH-OVER-BUDGET is diagnose-or-signed-raise, never rerun. FOLLOW-UP ROWS (operator rework push, 2026-07-23): (a) PRELUDE COVERAGE HOLE — the ~5min before batch-1 arms (naming-hygiene walk, policy install, plan resolve/eval, governor arm, eager compile-clean install) sits OUTSIDE every batch budget and can only red at the 55min step cap; a prelude budget row lands when the phase_mark walls get their own receipt keys. (b) IDENTITY-KEYED BUDGETS — rows are keyed by batch INDEX today (the coverage witness pins length, so a schedule change reds loudly rather than misassigning); keying by batch content-identity dissolves the index coupling and rides the ComputationIdentity lane. (c) K-CAP UNION-RSS RECEIPT — cheap_gate_pool_max_claims_per_child=16 is provisional until the pooled child's union RSS is receipted (claim_batch already prints per-shard-peak-rss; the first CI runs' logs back or re-size the cap; local proxy receipt: the 4-gate outer child peaked 1.82GB with the 12-claim pool nested)." -data gunbc_ci_floor_batch_wall_budget_seconds: List = [240, 60, 1320, 420, 120, 600, 420] +type FloorBatchClamp { + overhead_seconds: Int + per_unit_ms: Int +} + +data gunbc_ci_floor_batch_clamp_note: String = "DERIVED per-batch wall clamp (Piece 3, ci-two-tier-placement-redesign.md §9.8, operator 2026-07-24). Supersedes the hand-set gunbc_ci_floor_batch_wall_budget_seconds list (deleted): a scalar wall budget conflated workload size (affected-set selection is diff-proportional BY DESIGN, ~5x swing), host speed (±20% fleet envelope), and the quantity actually worth bounding (per-unit cost creep). The clamp re-denominates — per batch, clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms — computed by claim_executor from THIS authority plus the affected-set-selected unit count it alone knows (the schedule holds one opaque discovery runnable; the witness count is runtime, not schedule data). Rows are index-aligned to gunbc_ci_floor_batches (the length-match witness pins the alignment, mirroring the deleted list's discipline). The load-bearing row is the discovery witness batch (index 2): overhead 300s + 1000ms/witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, ~1.6x the ~25-min healthy wall) while every legitimate observed full-corpus wall (1344-1629s) passes, and a runaway reds proportionally instead of at the fixed 1320s that redded legitimate hub-file PRs. Fixed-count gate batches carry per_unit_ms 0 (their count does not vary, so overhead IS the clamp) at their measured basis. Index 3 (wet corpora) stays a fixed overhead pending a wet-per-witness rate from the D2 probe — a declared calibration gap, not a hidden default. SIGNED CONSTANTS (operator, 2026-07-24): the witness aggregate coefficient 1000ms and the discovery overhead 300s; the per-WITNESS hard max is NOT redefined here — it stays the single fast-lane authority gunbc_ci_fast_lane_eval_budget_ms (5s). RAISE DISCIPLINE (carried from gunbc_ci_floor_batch_wall_budget_note, which keeps the signed static-era history): raising any overhead or rate requires an appended dated operator-signed line naming the run id and the enrollment that grew the batch; tightening may land by ordinary receipt note; unit counts need no signature (the schedule computes them). The clamp is interim mechanics — the structural wall is the complexity lens (§8, cost <= a + b*n asserted at compile time), and the clamp demotes to host-pathology backstop when that lens goes Blocking. The 55-min step cap stays the absolute backstop for the total floor wall; GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS lowers the COMPUTED clamp (min), never raises — the RED-control hook, never an escape hatch." + +data gunbc_ci_floor_batch_clamp_params: List = [ + FloorBatchClamp { overhead_seconds: 240, per_unit_ms: 0 }, + FloorBatchClamp { overhead_seconds: 60, per_unit_ms: 0 }, + FloorBatchClamp { overhead_seconds: 300, per_unit_ms: 1000 }, + FloorBatchClamp { overhead_seconds: 420, per_unit_ms: 0 }, + FloorBatchClamp { overhead_seconds: 120, per_unit_ms: 0 }, + FloorBatchClamp { overhead_seconds: 600, per_unit_ms: 0 }, + FloorBatchClamp { overhead_seconds: 420, per_unit_ms: 0 } +] data ci_release_features: String = "text_lookup_work_counter" diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 62650d18310..9c5870a48cd 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -427,7 +427,7 @@ data gunbc_ci_regen_step_timeout_minutes: Duration = 15 data gunbc_ci_regen_step_timeout_note: String = "The regen step gets its OWN budget (operator main-timeout ruling 2026-07-23): it measured ~5min on every recent green run, and it previously borrowed the floor step's cap — which double-counted the floor budget in the job backstop sum and let a wedged regen sit for hours. 15m = 3x the measured envelope; a regen that exceeds it is a defect to diagnose, never headroom to grant." -data gunbc_ci_floor_step_timeout_discovery_flip_note: String = "COST WALL landed 2026-07-23 (CI floor endgame, appended per the raise-discipline this history IS the receipt for): the bounce pattern below — every raise fitting the cap to the cost — now has its construction answer: per-BATCH wall budgets as data (gunbc.ci_spec gunbc_ci_floor_batch_wall_budget_seconds, sum 53m under this 55m cap) enforced by claim_executor as typed FLOOR-BATCH-OVER-BUDGET refusals with per-batch receipt rows, so a lane that adds minutes reds ITS batch at merge time instead of bouncing this cap upward later. This cap is unchanged by that PR; 55 -> 40 is proposed as a data change AFTER the post-merge receipts exist (operator signs). PRIOR HISTORY: -> 55 operator ruling 2026-07-23 (main wall ~75m: 'even 1 hour is absolutely ridiculous'): the 270 cap legalized a 4.5-hour crawl — receipt run 29976854620 @ 76fa6548e killed by hand at t=227m with current=16.3G pinned at memory.high, swap=34.4G, high_events=37,096,823, psi_some_avg10=33.82, oom_kill=0, governor 'hard back-off 1->1' every few seconds: the memory.high throttle-crawl class (retention finally exceeded the slot cap; the prior 40-50m greens were already pinned at 15.2-16.1G with zero headroom), and 8 subsequent main pushes wedged identically behind it fleet-wide. Recent green floors run 39-45m, so 55 is a real ceiling not a target; the regression ledger to hold onto: ~8m pre-#6848 -> ~18m (#6848) -> 40-50m (retention accreting to the cap) -> 4h (cap lost) — one disease (retention, not footprint), never an accepted baseline. Named follow-ups so this cap becomes the backstop rather than the diagnostic: the governor gains a terminal crawl-refusal arm (sustained high_events storm at width=1 -> typed FloorRefusedMemoryBudget naming batch/peak/swap, minutes not hours), and the #7106-attributed levers (one-tree-one-resolve, #6848 once-per-entry fixpoint, M2 eviction) bring the floor back under the cap with margin. Superseded main runs are NOT cancelled by policy (operator 2026-07-23): the per-commit verdict history is bisection evidence — the timeout IS the bound. PRIOR HISTORY: -> 270 restored 2026-07-12 on #6512 after 60m fail-fast regressed merge CI (receipt run 29197126623 @ 35f212fa5d: 60m step kill with 0 witness FAILs — batch-1 compile-clean PASS @ 15:01, batch-2 skip walk ~26m to recompute_trace_probe_test, then ~34m silent eval until kill; same serial-floor-wall class as 29183446733). Prior -> 60 operator ruling 2026-07-12: lower floor step cap from 270m so CI fails fast while the serial floor wall is debugged separately (receipt run 29183446733 @ 2e856a5617: 270m kill with 0 witness FAILs, batch-2 still in discovery SKIP walk). Prior -> 270 at PR #6464 receipt run 29151777611 (2026-07-11): 180m step cap still timed out mid batch 2 — 1535 SKIP rows finished @ 12:35:43, then ~147m silent witness-execution phase until the 180m kill @ 15:03:12 with batch 2 never completing (~177m in-batch from 12:06:38; ~443 non-skipped rows in roster of 1978). Prior -> 180 at run 29148344466 (~116m in-batch, ~87m post-skip). Prior -> 120 at run 29145270700. Prior -> 90 at run 29141663541. Prior -> 90 at body_lowering normalize hook (#6459 receipt run 29148735992 @ 3a372b7: floor step timed out at 60m MID discovery corpus after ~320 SKIP lines — batch-1 compile-clean normalize reconcile ~263s with body_lowering_fold in the normalize import closure; discovery still resolves every unique entry file before skip, so resolve->normalize pulled the ~1.3k-line scaffold into most closure walks). Structural fix on the same lane: NormalizedTree moved to v2.compiler.normalized_tree so v2.compiler.resolve no longer imports normalize (dissolve-on: skip-before-resolve so skipped rows never pay entry resolve). Prior step budget -> 60 at the discovery flip (gunbc.ci_spec ci_spec_discovery_flip_note; authored as 30 -> 60 on #6403, main had meanwhile bumped 30 -> 45 with the #6422 enrollments): the corpus discovery batch adds a whole-tree resolve plus the always-run live-tree rows to the floor step. Discovery corpus spawn_width_cap stays pinned to 1 (ci_corpus_discovery_spawn_width_cap in v2.workflow.ci_floor_plan): at width W the executor holds the parent's process-shared index PLUS W private shard indexes ((1+W) x whole-tree residency; width=2 OOM receipt run 28999086030), while width=1 runs rows on the main thread against the ONE shared index (union-resolve S1). CORRECTED RECEIPT (2026-07-10): run 29000557166's floor did NOT complete - the executor was host-OOM-killed ~8min in, MID discovery corpus (the log's later ExitSuccess belongs to the merge-admission STAMP tool, which stamped CI_FLOOR_EXIT=137); no flipped corpus has completed in CI yet - the only completion receipt is local (33.5GiB container, ~40min at width 5). The kill vector is host-level oversubscription (see gunbc_falsifier_plan_spawn_width_note), not this width model. #6475 receipt run 29143617420 @ 0c0f73: batch-1 compile-clean ~4m green; batch-2 discovery killed at 90m step cap mid-manual (last skip rust_wire_serde @ 07:47:49; ~15GiB peak). #6475 receipt run 29146814967 @ 6a61fce: same stall at 120m (last skip rust_wire_serde @ 09:34:49; ~85m silent eval). #6475 receipt run 29150477894 @ 6cd5326: same stall at 180m (last skip rust_wire_serde @ 11:43:46; ~88m silent eval) — local gunbc run on s1_closure_parses_holds reads 3/40 closure paths in 10m before timeout, matching the silent-eval class (live filesystem_read per path, not a skip-logged row). #6475 receipt run 29161709373 @ 9d9852c: batch-1 compile-clean refused — gunbc_ci_floor_step_timeout_discovery_flip_note string literal terminated early at col 2043 (body_lowering suffix spliced after closing quote during rebase merge); restored single-line literal on rebase to main #6464 note. Revisit down when floor memoization / resolver graph-major shrink the resolve wall; the affected-set selection receipts (skip counts per PR) are the cost dial to watch." +data gunbc_ci_floor_step_timeout_discovery_flip_note: String = "COST WALL landed 2026-07-23 (CI floor endgame, appended per the raise-discipline this history IS the receipt for): the bounce pattern below — every raise fitting the cap to the cost — now has its construction answer: per-BATCH clamps derived from data (gunbc.ci_spec gunbc_ci_floor_batch_clamp_params, superseding the 53m static gunbc_ci_floor_batch_wall_budget_seconds list) enforced by claim_executor as typed FLOOR-BATCH-OVER-BUDGET refusals with per-batch receipt rows, so a lane that adds minutes reds ITS batch at merge time instead of bouncing this cap upward later. This cap is unchanged by that PR; 55 -> 40 is proposed as a data change AFTER the post-merge receipts exist (operator signs). PRIOR HISTORY: -> 55 operator ruling 2026-07-23 (main wall ~75m: 'even 1 hour is absolutely ridiculous'): the 270 cap legalized a 4.5-hour crawl — receipt run 29976854620 @ 76fa6548e killed by hand at t=227m with current=16.3G pinned at memory.high, swap=34.4G, high_events=37,096,823, psi_some_avg10=33.82, oom_kill=0, governor 'hard back-off 1->1' every few seconds: the memory.high throttle-crawl class (retention finally exceeded the slot cap; the prior 40-50m greens were already pinned at 15.2-16.1G with zero headroom), and 8 subsequent main pushes wedged identically behind it fleet-wide. Recent green floors run 39-45m, so 55 is a real ceiling not a target; the regression ledger to hold onto: ~8m pre-#6848 -> ~18m (#6848) -> 40-50m (retention accreting to the cap) -> 4h (cap lost) — one disease (retention, not footprint), never an accepted baseline. Named follow-ups so this cap becomes the backstop rather than the diagnostic: the governor gains a terminal crawl-refusal arm (sustained high_events storm at width=1 -> typed FloorRefusedMemoryBudget naming batch/peak/swap, minutes not hours), and the #7106-attributed levers (one-tree-one-resolve, #6848 once-per-entry fixpoint, M2 eviction) bring the floor back under the cap with margin. Superseded main runs are NOT cancelled by policy (operator 2026-07-23): the per-commit verdict history is bisection evidence — the timeout IS the bound. PRIOR HISTORY: -> 270 restored 2026-07-12 on #6512 after 60m fail-fast regressed merge CI (receipt run 29197126623 @ 35f212fa5d: 60m step kill with 0 witness FAILs — batch-1 compile-clean PASS @ 15:01, batch-2 skip walk ~26m to recompute_trace_probe_test, then ~34m silent eval until kill; same serial-floor-wall class as 29183446733). Prior -> 60 operator ruling 2026-07-12: lower floor step cap from 270m so CI fails fast while the serial floor wall is debugged separately (receipt run 29183446733 @ 2e856a5617: 270m kill with 0 witness FAILs, batch-2 still in discovery SKIP walk). Prior -> 270 at PR #6464 receipt run 29151777611 (2026-07-11): 180m step cap still timed out mid batch 2 — 1535 SKIP rows finished @ 12:35:43, then ~147m silent witness-execution phase until the 180m kill @ 15:03:12 with batch 2 never completing (~177m in-batch from 12:06:38; ~443 non-skipped rows in roster of 1978). Prior -> 180 at run 29148344466 (~116m in-batch, ~87m post-skip). Prior -> 120 at run 29145270700. Prior -> 90 at run 29141663541. Prior -> 90 at body_lowering normalize hook (#6459 receipt run 29148735992 @ 3a372b7: floor step timed out at 60m MID discovery corpus after ~320 SKIP lines — batch-1 compile-clean normalize reconcile ~263s with body_lowering_fold in the normalize import closure; discovery still resolves every unique entry file before skip, so resolve->normalize pulled the ~1.3k-line scaffold into most closure walks). Structural fix on the same lane: NormalizedTree moved to v2.compiler.normalized_tree so v2.compiler.resolve no longer imports normalize (dissolve-on: skip-before-resolve so skipped rows never pay entry resolve). Prior step budget -> 60 at the discovery flip (gunbc.ci_spec ci_spec_discovery_flip_note; authored as 30 -> 60 on #6403, main had meanwhile bumped 30 -> 45 with the #6422 enrollments): the corpus discovery batch adds a whole-tree resolve plus the always-run live-tree rows to the floor step. Discovery corpus spawn_width_cap stays pinned to 1 (ci_corpus_discovery_spawn_width_cap in v2.workflow.ci_floor_plan): at width W the executor holds the parent's process-shared index PLUS W private shard indexes ((1+W) x whole-tree residency; width=2 OOM receipt run 28999086030), while width=1 runs rows on the main thread against the ONE shared index (union-resolve S1). CORRECTED RECEIPT (2026-07-10): run 29000557166's floor did NOT complete - the executor was host-OOM-killed ~8min in, MID discovery corpus (the log's later ExitSuccess belongs to the merge-admission STAMP tool, which stamped CI_FLOOR_EXIT=137); no flipped corpus has completed in CI yet - the only completion receipt is local (33.5GiB container, ~40min at width 5). The kill vector is host-level oversubscription (see gunbc_falsifier_plan_spawn_width_note), not this width model. #6475 receipt run 29143617420 @ 0c0f73: batch-1 compile-clean ~4m green; batch-2 discovery killed at 90m step cap mid-manual (last skip rust_wire_serde @ 07:47:49; ~15GiB peak). #6475 receipt run 29146814967 @ 6a61fce: same stall at 120m (last skip rust_wire_serde @ 09:34:49; ~85m silent eval). #6475 receipt run 29150477894 @ 6cd5326: same stall at 180m (last skip rust_wire_serde @ 11:43:46; ~88m silent eval) — local gunbc run on s1_closure_parses_holds reads 3/40 closure paths in 10m before timeout, matching the silent-eval class (live filesystem_read per path, not a skip-logged row). #6475 receipt run 29161709373 @ 9d9852c: batch-1 compile-clean refused — gunbc_ci_floor_step_timeout_discovery_flip_note string literal terminated early at col 2043 (body_lowering suffix spliced after closing quote during rebase merge); restored single-line literal on rebase to main #6464 note. Revisit down when floor memoization / resolver graph-major shrink the resolve wall; the affected-set selection receipts (skip counts per PR) are the cost dial to watch." data gunbc_ci_step_timeout_measure_grounding_disposition: Disposition = Scaffold { dissolves_to: SingleAuthority, diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index ac2c36dddd7..03515add047 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -48,66 +48,104 @@ fn read_positive_budget_ms( } } -/// THE COST WALL (CI floor endgame D5 — authority `gunbc.ci_spec.gunbc_ci_floor_batch_wall_budget_seconds` -/// + `gunbc_ci_floor_batch_wall_budget_note`): per-batch wall budgets for the floor plan, read -/// fail-closed at arm time from the plan ctx (the fast-lane-budget pattern). Budgets are -/// admission/scheduling facts at the walk grain — witness verdicts never carry a wall-clock -/// term (the ruling split reconciled in the carrier note). `GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS` -/// is the RED-control fault injection: it can only LOWER budgets (min), so it can force the -/// refusal for a control run but can never open the gate — not an escape hatch by construction. -fn read_floor_batch_wall_budgets_ms( +/// THE COST WALL (Piece 3 derived clamp — authority `gunbc.ci_spec.gunbc_ci_floor_batch_clamp_params` +/// + `gunbc_ci_floor_batch_clamp_note`): the per-batch clamp is `overhead_seconds*1000 + +/// runtime_unit_count * per_unit_ms`. This reads the two index-aligned param lists fail-closed at +/// arm time (the fast-lane-budget pattern); the clamp itself is computed at enforcement, because the +/// affected-set-selected unit count is a runtime datum the schedule does not hold. Clamps are +/// admission/scheduling facts at the walk grain — witness verdicts never carry a wall-clock term +/// (the ruling split reconciled in the carrier note). +fn read_floor_batch_clamp_params( plan_ctx: &InterpContext, batch_count: usize, -) -> Result, String> { - let items = match run_value(plan_ctx, "gunbc_ci_floor_batch_wall_budgets_seconds") { +) -> Result, String> { + let overhead_items = match run_value(plan_ctx, "gunbc_ci_floor_batch_clamp_overhead_seconds") { Ok(Value::List(items)) => items, Ok(other) => { return Err(format!( - "claim_executor: gunbc_ci_floor_batch_wall_budgets_seconds must be a List, got {other:?} (fail-closed)" + "claim_executor: gunbc_ci_floor_batch_clamp_overhead_seconds must be a List, got {other:?} (fail-closed)" )); } Err(msg) => { return Err(format!( - "claim_executor: floor plan schedules batches but gunbc_ci_floor_batch_wall_budgets_seconds is unavailable (fail-closed): {msg}" + "claim_executor: floor plan schedules batches but gunbc_ci_floor_batch_clamp_overhead_seconds is unavailable (fail-closed): {msg}" )); } }; - let mut budgets_ms: Vec = Vec::new(); - for item in items.iter() { + let mut overheads_ms: Vec = Vec::new(); + for item in overhead_items.iter() { match item { - Value::Int(n) if *n > 0 => budgets_ms.push(*n as u128 * 1000), + Value::Int(n) if *n > 0 => overheads_ms.push(*n as u128 * 1000), other => { return Err(format!( - "claim_executor: gunbc_ci_floor_batch_wall_budgets_seconds rows must be positive Ints, got {other:?} (fail-closed)" + "claim_executor: gunbc_ci_floor_batch_clamp_overhead_seconds rows must be positive Ints, got {other:?} (fail-closed)" )); } } } - if budgets_ms.len() != batch_count { - return Err(format!( - "claim_executor: gunbc_ci_floor_batch_wall_budgets_seconds has {} row(s) but the plan schedules {} batch(es) — the budget list must cover the schedule exactly (fail-closed; update gunbc.ci_spec beside the schedule change)", - budgets_ms.len(), - batch_count - )); - } - if let Ok(tighten) = std::env::var("GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS") { - match tighten.parse::() { - Ok(t) => { - for b in budgets_ms.iter_mut() { - *b = (*b).min(t); - } - eprintln!( - "claim_executor: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS={t} — budgets tightened (RED-control injection; tighten-only, can never widen a budget)" - ); - } - Err(_) => { + let rate_items = match run_value(plan_ctx, "gunbc_ci_floor_batch_clamp_per_unit_ms") { + Ok(Value::List(items)) => items, + Ok(other) => { + return Err(format!( + "claim_executor: gunbc_ci_floor_batch_clamp_per_unit_ms must be a List, got {other:?} (fail-closed)" + )); + } + Err(msg) => { + return Err(format!( + "claim_executor: floor plan schedules batches but gunbc_ci_floor_batch_clamp_per_unit_ms is unavailable (fail-closed): {msg}" + )); + } + }; + let mut rates_ms: Vec = Vec::new(); + for item in rate_items.iter() { + match item { + Value::Int(n) if *n >= 0 => rates_ms.push(*n as u128), + other => { return Err(format!( - "claim_executor: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS must parse as milliseconds, got {tighten:?} (fail-closed)" + "claim_executor: gunbc_ci_floor_batch_clamp_per_unit_ms rows must be non-negative Ints, got {other:?} (fail-closed)" )); } } } - Ok(budgets_ms) + if overheads_ms.len() != batch_count || rates_ms.len() != batch_count { + return Err(format!( + "claim_executor: floor batch clamp params (overhead {} row(s), rate {} row(s)) must each cover the {} scheduled batch(es) exactly (fail-closed; update gunbc.ci_spec beside the schedule change)", + overheads_ms.len(), + rates_ms.len(), + batch_count + )); + } + Ok(overheads_ms.into_iter().zip(rates_ms).collect()) +} + +/// The RED-control fault injection (`GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS`): lowers the COMPUTED +/// per-batch clamp (min) at enforcement, so it can force a FLOOR-BATCH-OVER-BUDGET refusal for a +/// control run but can never open the gate — tighten-only by construction, never an escape hatch. +fn read_floor_batch_budget_tighten_ms() -> Result, String> { + match std::env::var("GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS") { + Ok(t) => match t.parse::() { + Ok(v) => Ok(Some(v)), + Err(_) => Err(format!( + "claim_executor: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS must parse as milliseconds, got {t:?} (fail-closed)" + )), + }, + Err(_) => Ok(None), + } +} + +/// Runtime per-batch unit count for the derived clamp: a discovery aggregate result contributes +/// its post-selection witness count (`corpus_witnesses`); every single-claim gate row contributes 1. +fn batch_runtime_unit_count(results: &[ClaimResult]) -> u128 { + results + .iter() + .map(|r| { + if r.corpus_witnesses > 0 { + r.corpus_witnesses as u128 + } else { + 1u128 + } + }) + .sum() } /// SCAFFOLD (§7 seed-retained HAND-RUST — authority: @@ -1495,6 +1533,11 @@ fn sccache_server_cgroup_rel() -> Option { struct BatchRecord { batch_index: usize, wall_nanos: u128, + /// The derived clamp computed for this batch (overhead + unit_count*rate, tightened), or None + /// for budget-less plans (falsifier/regen). Recorded so the receipt never recomputes it. + clamp_ms: Option, + /// The runtime unit count the clamp used (discovery witnesses + gate rows). + unit_count: u128, /// Flattened results from all units in this batch (order: unit by unit). results: Vec, } @@ -1511,42 +1554,32 @@ fn write_resolve_receipt(batch_records: &[BatchRecord]) -> bool { write_resolve_receipt_at(std::path::Path::new("target"), batch_records) } -/// Per-batch wall receipt (THE COST WALL, D5): typed rows — one wall/budget/verdict triple -/// per batch — so the floor's time story is a receipt, not a log archaeology exercise. -/// `OverBudget` rows correspond one-to-one with the FLOOR-BATCH-OVER-BUDGET refusals the -/// walk printed; a budget-less run (falsifier/regen plans) records walls with +/// Per-batch wall receipt (THE COST WALL, Piece 3 derived clamp): typed rows — one +/// wall/units/clamp/verdict group per batch — so the floor's time story is a receipt, not a log +/// archaeology exercise. `OverBudget` rows correspond one-to-one with the FLOOR-BATCH-OVER-BUDGET +/// refusals the walk printed; a clamp-less run (falsifier/regen plans) records walls with /// `verdict=Unbudgeted`. Returns false on a write error — the walk fails closed here. -fn write_batch_wall_receipt( - batch_records: &[BatchRecord], - batch_wall_budgets_ms: Option<&[u128]>, -) -> bool { - write_batch_wall_receipt_at( - std::path::Path::new("target"), - batch_records, - batch_wall_budgets_ms, - ) +fn write_batch_wall_receipt(batch_records: &[BatchRecord]) -> bool { + write_batch_wall_receipt_at(std::path::Path::new("target"), batch_records) } -fn write_batch_wall_receipt_at( - base: &std::path::Path, - batch_records: &[BatchRecord], - batch_wall_budgets_ms: Option<&[u128]>, -) -> bool { +fn write_batch_wall_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord]) -> bool { let mut body = String::new(); let mut over_budget = 0usize; for rec in batch_records { let n = rec.batch_index + 1; let wall_ms = rec.wall_nanos / 1_000_000; body.push_str(&format!("batch_{n}_wall_ms={wall_ms}\n")); - match batch_wall_budgets_ms.and_then(|b| b.get(rec.batch_index)) { - Some(budget_ms) => { - let verdict = if wall_ms > *budget_ms { + match rec.clamp_ms { + Some(clamp_ms) => { + let verdict = if wall_ms > clamp_ms { over_budget += 1; "OverBudget" } else { "WithinBudget" }; - body.push_str(&format!("batch_{n}_budget_ms={budget_ms}\n")); + body.push_str(&format!("batch_{n}_units={}\n", rec.unit_count)); + body.push_str(&format!("batch_{n}_clamp_ms={clamp_ms}\n")); body.push_str(&format!("batch_{n}_verdict={verdict}\n")); } None => { @@ -1804,7 +1837,8 @@ fn run_walk( fast_lane_eval_budget_ms: Option, falsifier_self_host_wet_budgets: FalsifierSelfHostWetBudgets, stop_policy: FloorBatchStopPolicy, - batch_wall_budgets_ms: Option<&[u128]>, + batch_clamp_params: Option<&[(u128, u128)]>, + budget_tighten_ms: Option, ) -> WalkOutcome { let mut any_failed = false; let mut batches_run = 0usize; @@ -1957,34 +1991,48 @@ fn run_walk( any_failed = true; } let batch_wall_nanos = batch_start.elapsed().as_nanos(); - // THE COST WALL (D5): over-budget is a typed, located refusal — batch id, measured - // wall, budget row — that reds the walk. It never widens (no rerun, no scope change, - // no cap raise); witness verdicts inside the batch stand as evaluated (the budget is - // an admission/scheduling fact, not a verdict term — carrier note has the ruling split). - if let Some(budgets) = batch_wall_budgets_ms { - if let Some(budget_ms) = budgets.get(bi) { - let wall_ms = batch_wall_nanos / 1_000_000; - if wall_ms > *budget_ms { - println!( - "{}", - paint( - &format!( - "✗ FLOOR-BATCH-OVER-BUDGET batch={} wall_ms={} budget_ms={} (budget row: gunbc.ci_spec gunbc_ci_floor_batch_wall_budget_seconds[{}]; raising it requires an appended receipt note per gunbc_ci_floor_batch_wall_budget_note — a refusal, never a widen)", - bi + 1, - wall_ms, - budget_ms, - bi - ), - sgr::ERROR - ) - ); - any_failed = true; - } + // THE COST WALL (Piece 3 derived clamp): the per-batch clamp is overhead + runtime unit + // count * rate, computed HERE where the affected-set-selected count is known (the schedule + // holds one opaque discovery runnable; the count is runtime). Over-clamp is a typed, located + // refusal that reds the walk; it never widens (no rerun, no scope change, no cap raise). + // Witness verdicts inside the batch stand as evaluated (the clamp is an admission/scheduling + // fact, not a verdict term — carrier note has the ruling split). + let batch_unit_count = batch_runtime_unit_count(&batch_results); + let batch_clamp_ms: Option = + batch_clamp_params + .and_then(|p| p.get(bi)) + .map(|&(overhead_ms, rate_ms)| { + let mut clamp = overhead_ms + batch_unit_count * rate_ms; + if let Some(t) = budget_tighten_ms { + clamp = clamp.min(t); + } + clamp + }); + if let Some(clamp_ms) = batch_clamp_ms { + let wall_ms = batch_wall_nanos / 1_000_000; + if wall_ms > clamp_ms { + println!( + "{}", + paint( + &format!( + "✗ FLOOR-BATCH-OVER-BUDGET batch={} wall_ms={} clamp_ms={} units={} (clamp = overhead + units*rate; authority gunbc.ci_spec gunbc_ci_floor_batch_clamp_params[{}]; raising an overhead or rate requires an operator-signed line per gunbc_ci_floor_batch_clamp_note — a refusal, never a widen)", + bi + 1, + wall_ms, + clamp_ms, + batch_unit_count, + bi + ), + sgr::ERROR + ) + ); + any_failed = true; } } batch_records.push(BatchRecord { batch_index: bi, wall_nanos: batch_wall_nanos, + clamp_ms: batch_clamp_ms, + unit_count: batch_unit_count, results: batch_results, }); if any_failed { @@ -2008,7 +2056,7 @@ fn run_walk( let total_wall_nanos = walk_start.elapsed().as_nanos(); emit_gantt(&batch_records, total_wall_nanos); let resolve_receipt_ok = write_resolve_receipt(&batch_records); - let batch_wall_receipt_ok = write_batch_wall_receipt(&batch_records, batch_wall_budgets_ms); + let batch_wall_receipt_ok = write_batch_wall_receipt(&batch_records); // Memo contexts absorb their ledger totals into the process accumulator on // Drop, so they must die before the materialization receipt is written. drop(walk_memo); @@ -2220,6 +2268,7 @@ fn run_perturb_check( FalsifierSelfHostWetBudgets::default(), FloorBatchStopPolicy::StopBeforeDependents, None, + None, ); let _ = fs::remove_dir_all(&tmp); @@ -2443,12 +2492,13 @@ fn run() -> Result { FalsifierSelfHostWetBudgets::default() }; let batch_stop_policy = resolve_floor_batch_stop_policy(&plan_ctx, &plan_function); - // THE COST WALL (D5): the floor plan's per-batch wall budgets, read fail-closed at arm - // time (the fast-lane-budget pattern). Scoped to the full floor plan only: the - // plan-artifact shortcut runs a single batch of the same schedule and the falsifier - // carries its own receipt budgets, so neither reads this list. - let batch_wall_budgets_ms: Option> = if plan_function == "gunbc_ci_floor_batches" { - match read_floor_batch_wall_budgets_ms(&plan_ctx, batches.len()) { + // THE COST WALL (Piece 3 derived clamp): the floor plan's per-batch clamp params, read + // fail-closed at arm time (the fast-lane-budget pattern). Scoped to the full floor plan only: + // the plan-artifact shortcut runs a single batch of the same schedule and the falsifier + // carries its own receipt budgets, so neither reads these lists. + let batch_clamp_params: Option> = if plan_function == "gunbc_ci_floor_batches" + { + match read_floor_batch_clamp_params(&plan_ctx, batches.len()) { Ok(v) => Some(v), Err(msg) => { eprintln!("{msg}"); @@ -2458,6 +2508,13 @@ fn run() -> Result { } else { None }; + let budget_tighten_ms: Option = match read_floor_batch_budget_tighten_ms() { + Ok(v) => v, + Err(msg) => { + eprintln!("{msg}"); + return Err(ExitCode::from(1)); + } + }; drop(plan_ctx); phase_mark("plan evaluated"); @@ -2520,7 +2577,8 @@ fn run() -> Result { fast_lane_eval_budget_ms, falsifier_self_host_wet_budgets, batch_stop_policy, - batch_wall_budgets_ms.as_deref(), + batch_clamp_params.as_deref(), + budget_tighten_ms, ); match peak_rss_bytes() { Some(bytes) => { @@ -2605,7 +2663,7 @@ mod tests { let _ = fs::remove_file(&base); fs::write(&base, b"a file where the receipt dir should be").unwrap(); assert!(!write_resolve_receipt_at(&base, &[])); - assert!(!write_batch_wall_receipt_at(&base, &[], None)); + assert!(!write_batch_wall_receipt_at(&base, &[])); let _ = fs::remove_file(&base); } @@ -2617,30 +2675,40 @@ mod tests { let base = std::env::temp_dir().join(format!("claim-executor-batch-wall-{}", std::process::id())); let _ = fs::remove_dir_all(&base); - let records = vec![ + // Clamped: batch 0 wall 5s > clamp 2s (OverBudget); batch 1 wall 1s < clamp 2s (WithinBudget). + let clamped_records = vec![ BatchRecord { batch_index: 0, wall_nanos: 5_000_000_000, // 5s + clamp_ms: Some(2_000), // 2s + unit_count: 3, results: Vec::new(), }, BatchRecord { batch_index: 1, wall_nanos: 1_000_000_000, // 1s + clamp_ms: Some(2_000), // 2s + unit_count: 0, results: Vec::new(), }, ]; - let budgets_ms: Vec = vec![2_000, 2_000]; // 2s each: batch 1 over, batch 2 within - assert!(write_batch_wall_receipt_at( - &base, - &records, - Some(&budgets_ms) - )); + assert!(write_batch_wall_receipt_at(&base, &clamped_records)); let body = fs::read_to_string(base.join("floor-batch-wall-receipt.txt")).unwrap(); assert!(body.contains("batch_1_wall_ms=5000")); + assert!(body.contains("batch_1_units=3")); + assert!(body.contains("batch_1_clamp_ms=2000")); assert!(body.contains("batch_1_verdict=OverBudget")); assert!(body.contains("batch_2_verdict=WithinBudget")); assert!(body.contains("over_budget_batches=1")); - assert!(write_batch_wall_receipt_at(&base, &records, None)); + // Clamp-less (falsifier/regen plans): records carry clamp_ms None -> Unbudgeted. + let unbudgeted_records = vec![BatchRecord { + batch_index: 0, + wall_nanos: 5_000_000_000, + clamp_ms: None, + unit_count: 0, + results: Vec::new(), + }]; + assert!(write_batch_wall_receipt_at(&base, &unbudgeted_records)); let body = fs::read_to_string(base.join("floor-batch-wall-receipt.txt")).unwrap(); assert!(body.contains("batch_1_verdict=Unbudgeted")); assert!(body.contains("over_budget_batches=0")); diff --git a/src/v2/test/claim/ci_floor_plan_witness_test.dag b/src/v2/test/claim/ci_floor_plan_witness_test.dag index f02cd05a550..408c1afe1c4 100644 --- a/src/v2/test/claim/ci_floor_plan_witness_test.dag +++ b/src/v2/test/claim/ci_floor_plan_witness_test.dag @@ -44,8 +44,9 @@ import v2.workflow.ci_floor_plan { falsifier_silent_pick_gate_batch_enrolled, falsifier_rehomed_bin_wet_batch_enrolled, gunbc_ci_floor_schedule_lens_holds, - witness_floor_batch_wall_budgets_cover_schedule, - witness_floor_batch_wall_budgets_all_positive, + witness_floor_batch_clamp_params_cover_schedule, + witness_floor_batch_clamp_overhead_all_positive, + witness_floor_batch_clamp_rate_all_nonneg, } import gunbc.ci_layer_roots { witness_layer_roots, witness_discovery_scan_dirs, known_red_probe_entries, falsifier_silent_pick_gate_entries } import v2.compiler.self_host.wet_receipt_enrollment { falsifier_self_host_wet_entries } @@ -492,8 +493,9 @@ test fn ci_floor_plan_witnesses() -> Bool { && witness_hermetic_floor_posture_declared() && witness_floor_arm_time_budget_refusal_plan_roster_authority() && witness_floor_arm_time_budget_refusal_materialized_roster_matches_seed() - && witness_floor_batch_wall_budgets_cover_schedule() - && witness_floor_batch_wall_budgets_all_positive() + && witness_floor_batch_clamp_params_cover_schedule() + && witness_floor_batch_clamp_overhead_all_positive() + && witness_floor_batch_clamp_rate_all_nonneg() && falsifier_rehomed_bin_wet_batch_enrolled() } diff --git a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag index 695028c8492..ab7ad0fd1f8 100644 --- a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag +++ b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag @@ -8,7 +8,7 @@ import std.realization_schedule { import v2.std.collection { List } import std.types { Int } -data budget_red_control_plan_note: String = "THE COST WALL's by-execution RED-control fixture (endgame D5): a minimal plan whose fn is NAMED gunbc_ci_floor_batches so claim_executor arms the per-batch wall budgets against it — one tiny hermetic batch, one generous budget row. RED direction: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS=0 tightens the budget to zero, the batch overruns by construction, and the run must exit nonzero with the typed FLOOR-BATCH-OVER-BUDGET refusal naming batch 1. GREEN direction: the same invocation without the env exits zero. The injection is tighten-only (min), so this fixture can force the refusal but can never widen a real budget — a fault injection, not an escape hatch. Recipe: target/release/claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/test/fixture/floor_skip/budget_red_control_plan.dag --plan-function gunbc_ci_floor_batches. Lives under test/fixture/floor_skip/ (discovery-excluded at dir grain) and declares no test fns, so it is invisible to the witness corpus — driven only by this recipe." +data budget_red_control_plan_note: String = "THE COST WALL's by-execution RED-control fixture (endgame D5): a minimal plan whose fn is NAMED gunbc_ci_floor_batches so claim_executor arms the per-batch derived clamp against it — one tiny hermetic batch, one generous overhead row (600s) with rate 0, so the computed clamp is 600s. RED direction: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS=0 tightens the budget to zero, the batch overruns by construction, and the run must exit nonzero with the typed FLOOR-BATCH-OVER-BUDGET refusal naming batch 1. GREEN direction: the same invocation without the env exits zero. The injection is tighten-only (min), so this fixture can force the refusal but can never widen a real budget — a fault injection, not an escape hatch. Recipe: target/release/claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/test/fixture/floor_skip/budget_red_control_plan.dag --plan-function gunbc_ci_floor_batches. Lives under test/fixture/floor_skip/ (discovery-excluded at dir grain) and declares no test fns, so it is invisible to the witness corpus — driven only by this recipe." fn budget_red_control_claim() -> Runnable { RunnableSingleClaim { @@ -22,6 +22,10 @@ fn gunbc_ci_floor_batches() -> List> { [[budget_red_control_claim()]] } -fn gunbc_ci_floor_batch_wall_budgets_seconds() -> List { +fn gunbc_ci_floor_batch_clamp_overhead_seconds() -> List { [600] } + +fn gunbc_ci_floor_batch_clamp_per_unit_ms() -> List { + [0] +} diff --git a/src/v2/workflow/ci_floor_plan.dag b/src/v2/workflow/ci_floor_plan.dag index a1ee9b11a94..a0a7c4fcb2e 100644 --- a/src/v2/workflow/ci_floor_plan.dag +++ b/src/v2/workflow/ci_floor_plan.dag @@ -10,7 +10,7 @@ import gunbc.ci_spec { floor_plan_function, plan_artifact_plan_function, FloorBatchStopPolicy, - gunbc_ci_floor_batch_wall_budget_seconds, + gunbc_ci_floor_batch_clamp_params, } import gunbc.falsifier_workflow { falsifier_plan_function } import gunbc.ci_gate { @@ -433,16 +433,24 @@ fn gunbc_ci_fast_lane_eval_budget_ms() -> Nat { second_count(s: gunbc_ci_fast_lane_witness_eval_budget) * 1000 } -fn gunbc_ci_floor_batch_wall_budgets_seconds() -> List { - gunbc_ci_floor_batch_wall_budget_seconds +fn gunbc_ci_floor_batch_clamp_overhead_seconds() -> List { + map(gunbc_ci_floor_batch_clamp_params, c => c.overhead_seconds) } -fn witness_floor_batch_wall_budgets_cover_schedule() -> Bool { - length(xs: gunbc_ci_floor_batch_wall_budgets_seconds()) == length(xs: gunbc_ci_floor_batches()) +fn gunbc_ci_floor_batch_clamp_per_unit_ms() -> List { + map(gunbc_ci_floor_batch_clamp_params, c => c.per_unit_ms) } -fn witness_floor_batch_wall_budgets_all_positive() -> Bool { - fold(gunbc_ci_floor_batch_wall_budgets_seconds(), init: true, f: fn(ok, b) { ok && (b > 0) }) +fn witness_floor_batch_clamp_params_cover_schedule() -> Bool { + length(xs: gunbc_ci_floor_batch_clamp_params) == length(xs: gunbc_ci_floor_batches()) +} + +fn witness_floor_batch_clamp_overhead_all_positive() -> Bool { + fold(gunbc_ci_floor_batch_clamp_overhead_seconds(), init: true, f: fn(ok, b) { ok && (b > 0) }) +} + +fn witness_floor_batch_clamp_rate_all_nonneg() -> Bool { + fold(gunbc_ci_floor_batch_clamp_per_unit_ms(), init: true, f: fn(ok, b) { ok && (b >= 0) }) } data gunbc_falsifier_self_host_wet_receipt_budget_note: String = "Whole-receipt wall-time budget for the nightly falsifier Wet self-host lane (operator ruling 2026-07-15, Wave 2 CI substrate STEP 3): bounds emit+cargo total wall time per witness — the wet receipt IS each module's Definition-of-Done, and cargo dominates (std/logic template ~101s wall, 06_translate ~52s measured Refinement-1). The primary refusal is WitnessWallBudgetExceeded on whole-receipt wall time; the secondary gunbc_falsifier_self_host_wet_interp_eval_budget arms the cooperative CPU eval deadline for interpreter-wedge detection only (subprocess cargo I/O is wall-counted, not CPU-counted). Receipted ceilings: logic template 101s, 06_translate 52s — 600s per receipt leaves headroom for larger modules while keeping 11-module upper bound (~9.5min measured) far under the 270m falsifier step budget." From 0a23ed6e640ec925af9ac2429e1548dfca8027c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 17:20:32 +0000 Subject: [PATCH 08/39] Finish #7146 adoption: regen ci.yml from merged authority, drop orphaned D5 witness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge that adopted #7146's landed gunbc.diff_baseline (and dropped my redundant D5 gunbc.ci_diff_defaults) left two remnants: - .github/workflows/ci.yml carried an auto-merge artifact — the regen step and floor step fetched `origin $GITHUB_BASE_REF` (my dropped D5's bare-var form) instead of `origin main` (#7146's authority, which resolves the diff baseline at floor eval-time via resolve_diff_baseline, not at fetch time). Re-running `gunbc ci` regen re-derives ci.yml from the merged ci_workflow.dag, restoring the `origin main` fetch. - src/v2/test/claim/ci_diff_baseline_witness_test.dag imported the deleted gunbc.ci_diff_defaults module (my D5 authority), which would break the corpus compile. Its 6 witnesses are strictly superseded by #7146's landed dag/test/claim/diff_baseline_witness_test.dag (11 witnesses, with stronger fail-closed semantics on PR absent-base). Deleted as dead weight. Co-Authored-By: Claude --- .github/workflows/ci.yml | 4 +- .../claim/ci_diff_baseline_witness_test.dag | 91 ------------------- 2 files changed, 2 insertions(+), 93 deletions(-) delete mode 100644 src/v2/test/claim/ci_diff_baseline_witness_test.dag diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1425aad956..2c7d219c6a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: - name: gunbc ci regen (self-host fixed-point — required) run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) - git fetch --no-tags origin $GITHUB_BASE_REF 2>/dev/null || true + git fetch --no-tags origin main 2>/dev/null || true # Affected-set-scoped regen admission (pull_request only): on a pull_request event, regen_floor_skip_witness intersects the merge-base diff with the [src/v1, dag] regen input closure (shared authority cli_run::regen_input_sources — the exact set regen_stage0 compiles; both the RegenVerifyGate and the SelfHostStalenessGate invoke regen_stage0, so the closure covers both). A PR diff touching none of src/v1/** (emitter source + committed stage0 outputs), v1's transitive dag import-closure, or the Cargo/toolchain build config cannot change the self-host fixed-point, so the regen step exits 0 immediately (a required step that passes because the self-host fixed-point is provably unchanged — regen runs before the floor so emitter drift fails fast, with no floor or merge-admission dependency). Empty diff / diff failure / closure failure runs regen (fail-closed). COLD CONTROL: the skip is gated to pull_request events via GITHUB_EVENT_NAME, so push-to-main and workflow_dispatch run regen UNCONDITIONALLY (no witness). A wrong closure that lets a PR wrongly skip therefore surfaces as a counted divergence on the very next merge to main — the squash-merge main-push run has an empty diff and runs regen cold, reding main if the seed is stale (a one-merge acceptance window, the discovery-flip shape). The 4-hourly falsifier does NOT run regen_stage0; the unconditional main-push regen is the cold control. if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then _ci_regen_skip=$("$ROOT/target/release/regen_floor_skip_witness" 2>/dev/null || echo run_regen) @@ -180,7 +180,7 @@ jobs: run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) # REMOVED 2026-07-21 (gunbc#7023 / proud-cat-517): the documentation_only_skip shell shortcut exited before claim_executor, bypassing the witness corpus entirely on docs-only PRs. That bypassed the existing ReadsLiveTree never-predict-skip lane — doc_reachability_witness_test.dag already declared ReadsLiveTree since #6654, but the shell never reached selection. Docs-only PRs now run the normal floor (SelectionApplied): import-closure skips non-live-tree witnesses cheaply; ReadsLiveTree rows (doc-graph wall, corpus-read host-fed lenses) always run. compile_clean_floor_skip_witness remains for dag_compile_clean_scope disposition only — it no longer gates floor admission. - git fetch --no-tags origin $GITHUB_BASE_REF 2>/dev/null || true + git fetch --no-tags origin main 2>/dev/null || true # 🟡 dissolve-on: ci_floor_disposition_marker_init_script — concat-built bash floor step opener stamping floor_running into target/ci-floor-disposition.txt (clears stale documentation_only_skipped on persistent self-hosted runners before docs-only branch); DISSOLVES WHEN bash-emit (#5828 / ROADMAP 6-shell-slice0) realizes floor disposition initialization through orchestration emit and the marker init retires without a transport scaffold # Positive floor-running stamp at the start of every floor step clears stale documentation_only_skipped left in target/ on persistent self-hosted runners (review 37010 / operator ruling 2026-07-11). Receipt gates skip only on an exact documentation_only_skipped match — never on file presence alone. mkdir -p target diff --git a/src/v2/test/claim/ci_diff_baseline_witness_test.dag b/src/v2/test/claim/ci_diff_baseline_witness_test.dag deleted file mode 100644 index 806d8e8f746..00000000000 --- a/src/v2/test/claim/ci_diff_baseline_witness_test.dag +++ /dev/null @@ -1,91 +0,0 @@ -module v2.test.claim.ci_diff_baseline_witness - -import std.logic { Bool } -import std.types { GitRef } -import gunbc.ci_diff_defaults { - DiffBaseline, MergeTarget, PushParent, OperatorOverride, - DiffBaselineResolution, DiffBaselineResolved, DiffBaselineRefused, - resolve_diff_baseline, - ci_default_baseline_ref -} - -fn resolved_ref_eq(resolution: DiffBaselineResolution, expected: GitRef) -> Bool { - match resolution { - DiffBaselineResolved { ref: r } => (r as String) == (expected as String) - DiffBaselineRefused { cause: _ } => false - } -} - -fn is_refused(resolution: DiffBaselineResolution) -> Bool { - match resolution { - DiffBaselineResolved { ref: _ } => false - DiffBaselineRefused { cause: _ } => true - } -} - -data stacked_pr_base_branch: String = "feature/other-pr-branch" - -test fn ci_diff_baseline_stacked_pr_selects_real_base_holds() -> Bool { - resolved_ref_eq( - resolution: resolve_diff_baseline( - baseline: MergeTarget, - github_base_ref: Present { value: stacked_pr_base_branch }, - push_parent_ref: Absent - ), - expected: "origin/feature/other-pr-branch" - ) -} - -test fn ci_diff_baseline_stacked_pr_is_not_origin_main_holds() -> Bool { - !resolved_ref_eq( - resolution: resolve_diff_baseline( - baseline: MergeTarget, - github_base_ref: Present { value: stacked_pr_base_branch }, - push_parent_ref: Absent - ), - expected: ci_default_baseline_ref - ) -} - -test fn ci_diff_baseline_merge_target_no_env_defaults_to_main_holds() -> Bool { - resolved_ref_eq( - resolution: resolve_diff_baseline( - baseline: MergeTarget, - github_base_ref: Absent, - push_parent_ref: Absent - ), - expected: ci_default_baseline_ref - ) -} - -test fn ci_diff_baseline_push_parent_resolves_parent_holds() -> Bool { - resolved_ref_eq( - resolution: resolve_diff_baseline( - baseline: PushParent, - github_base_ref: Absent, - push_parent_ref: Present { value: "0123abcd" } - ), - expected: "0123abcd" - ) -} - -test fn ci_diff_baseline_operator_override_round_trips_holds() -> Bool { - resolved_ref_eq( - resolution: resolve_diff_baseline( - baseline: OperatorOverride { ref: "origin/release-2026" }, - github_base_ref: Absent, - push_parent_ref: Absent - ), - expected: "origin/release-2026" - ) -} - -test fn ci_diff_baseline_push_parent_absent_refuses_not_widens_holds() -> Bool { - is_refused( - resolution: resolve_diff_baseline( - baseline: PushParent, - github_base_ref: Absent, - push_parent_ref: Absent - ) - ) -} From f682d33f19cc5db6760e96fc50bb287e04db1eba Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 17:57:16 +0000 Subject: [PATCH 09/39] P1: the CI-log renderer, with the captured crawl window as its acceptance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second phase of the atomic P0-P3 observation PR (operator ruling: only finished work merges; a landed event model with no renderer is vocabulary nobody can see). gunbc.observation_ci_render projects the std.observation stream into append-only log lines. It computes nothing and holds no telemetry source — every number it prints arrives in an event or a heartbeat sample the process already had — which is precisely what makes replaying a real captured run possible rather than a synthetic fixture. FLAGSHIP ACCEPTANCE, green by execution: the fixture is run 30044816605's actual log, not a reconstruction. Its heartbeat at t=33m carried current=16107200512 swap=34359738368 psi_some_avg10=9.01 and named no subject at all, while the process sat inside v2.compiler.normalized_tree for 606984ms and disclosed that only at walk end. Re-rendered through the escalation law the same window produces named activity: identity-first heartbeats in human units (15.0 GiB, not the raw byte dump), the quiet module surfaced at T, and the memory-reclaim cause surfaced at 2T. Contracts from section 6b in force: plain-sentence tone, real emojis from the one glyph authority with the clock pulse, identity before vitals, durations on every outcome line, refusals restated at the end so log truncation cannot hide them, and relayed subject text neutralized through the existing GitHub guard so a child's stderr cannot mint workflow commands in the parent run. Law 4 made structural: line placement is DERIVED from attention, so a refusal cannot be written into a collapsed group — the group is exactly where a reader will not look. Escalation has two rates: reveal depth grows linearly (one tree level per threshold) while emission points double (T, 2T, 4T), so a window that stays quiet escalates without becoming a per-minute drumbeat, bounded by construction. Three REDs proven by perturbation, as the ruling requires: - planted silent phase (escalation never emits) reds responsiveness - an orphaned Begin reds the watchdog - a Refused placed inside a collapsed group reds Review 42203 (three findings, all correct, all fixed): - ci_gibibyte_tenths respelled the GiB scale factor as a literal; it now consumes std.measure.gibibyte_scale_factor_bytes, and the duration helper consumes seconds_per_minute plus a new milliseconds_per_second added beside its siblings in that authority. A unit authority forked inside a formatting helper is easy to miss because it looks like arithmetic. - the run summary picked the refused glyph whenever refusals+failures>0, so a failures-only run rendered as refused — collapsing at the last line exactly what the outcome sum exists to establish. Failures now dominate the glyph, refusals keep their own, both counts stay in the text. - an unavailable duration silently vanished from concluded lines, breaking the model's own rule that an absent measurement names its cause. It now says so. Each fix carries a witness; the summary fix carries its own RED. Suite green: 31 model, 6 lockstep, 18 renderer conjuncts. Compile-clean unchanged at 47 pre-existing errors, zero attributable here. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/gunbc/observation_ci_render.dag | 454 ++++++++++++++++++ dag/std/measure.dag | 4 + .../observation_ci_render_witness_test.dag | 289 +++++++++++ 3 files changed, 747 insertions(+) create mode 100644 dag/gunbc/observation_ci_render.dag create mode 100644 dag/test/claim/observation_ci_render_witness_test.dag diff --git a/dag/gunbc/observation_ci_render.dag b/dag/gunbc/observation_ci_render.dag new file mode 100644 index 00000000000..1bac12a78f3 --- /dev/null +++ b/dag/gunbc/observation_ci_render.dag @@ -0,0 +1,454 @@ +module gunbc.observation_ci_render + +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import std.measure { + Millisecond, + ByteSize, + BasisPoint, + millisecond, + millisecond_count, + byte_size, + byte_size_count, + basis_point, + basis_point_count, + gibibyte_scale_factor_bytes, + milliseconds_per_second, + seconds_per_minute, +} +import std.symbols { SymbolId, Tier, Emoji, Unicode, Ascii, StatusPulse, StatusDwell, StatusBlocked, StatusRefused, StatusFinal, NodeCompleted, NodeFailed, NodeRunning, NodeSkipped } +import std.disposition { Disposition, Terminal } +import extdeps.render.glyphs { resolve_symbol } +import extdeps.github.log_annotations { annotation_group_prefix, annotation_endgroup, neutralize_workflow_commands } +import std.observation { + ObservationSubject, + ObservationSegment, + RunSegment, + BatchSegment, + EntrySegment, + ModuleSegment, + PhaseSegment, + observation_subject_render, + observation_subject_contains, + Measured, + MeasuredValue, + MeasuredUnavailable, + ObservationEvent, + ObservationOutcome, + Done, + Refused, + Failed, + TimedOut, + Skipped, + Final, + ObservationTransition, + Begin, + Step, + Concluded, + ObservationEventClass, + ClassRefused, + ClassBlocked, + ClassDwellEscalation, + observation_event_class, + observation_presentation, + observation_class_density, + ObservationDensity, + RoutineCollapsible, + SummaryAlwaysShown, + AnomalyExpanded, + observation_class_is_intrinsic_anomaly, + AttentionLevel, + Ambient, + Notable, + Anomaly, + AttentionBasis, + observation_attention, + DwellThreshold, + observation_dwell_threshold, + observation_escalation_depth, + observation_escalation_subject, + SchedulerHold, + PsiPressure, + CurrentHighWater, + observation_hold_resource, + ContendedResource, + MemoryBudget, + AdmissionWindow, + MaturationReserve, +} + +data observation_ci_render_note: String = "P1 of the progress-and-observation lane: the CI-log renderer, built first because the CI log is where the pain lives. Append-only, no repaint — a GitHub Actions log cannot be redrawn, so every line is final when written. This module PROJECTS the std.observation event vocabulary into lines; it computes no facts of its own and holds no telemetry source. Every number it prints arrives in an event or a heartbeat sample that the process already had (law 5), which is why the flagship acceptance can replay a captured run: the renderer is a pure function of the stream." + +data observation_ci_render_tone_note: String = "Tone contract (operator, 2026-07-24). Arm's-length lines are plain sentences a human reads without decoding — not dense key-equals-value chains, which belong in receipt boxes and files. Magnitudes are in human units: gibibytes, minutes, percent. The named negative example is the floor memory heartbeat's current shape, sixty-plus context-free byte dumps an hour, each carrying a raw current=16106717184 and no subject at all; that line is what the operator watched for ten minutes while a single module typechecked, and it is the exact shape this renderer must not reproduce." + +data ci_unit_authority_note: String = "The scale factors are CONSUMED from std.measure, never respelled here. A renderer carrying its own copy of how many bytes are in a gibibyte is a second unit authority hiding inside a formatting helper, and the fork is easy to miss precisely because it looks like arithmetic rather than modelling." + +data ci_one_decimal: Nat = 10 + +fn ci_gibibyte_tenths(b: ByteSize) -> Nat { + (byte_size_count(b) * ci_one_decimal) / gibibyte_scale_factor_bytes() +} + +fn ci_milliseconds_per_minute() -> Nat { + milliseconds_per_second() * seconds_per_minute() +} + +data ci_minute_switch_seconds: Nat = 90 + +data ci_minute_switch_note: String = "Where the sentence form switches from seconds to minutes is a DISPLAY policy, not a unit: ninety seconds reads better as ninety seconds than as one minute. It is named rather than buried as a literal so it stays visible as the judgement it is, distinct from the scale factors above, which are facts." + +fn ci_tenths_text(tenths: Nat) -> String { + concat(to_string(tenths / 10), concat(".", to_string(tenths % 10))) +} + +fn ci_human_bytes(b: ByteSize) -> String { + concat(ci_tenths_text(tenths: ci_gibibyte_tenths(b: b)), " GiB") +} + +fn ci_human_percent(bp: BasisPoint) -> String { + concat(ci_tenths_text(tenths: basis_point_count(bp: bp) / 10), "%") +} + +fn ci_human_duration(d: Millisecond) -> String { + let ms = millisecond_count(m: d) + if ms < milliseconds_per_second() { + concat(to_string(ms), "ms") + } else if ms < (ci_minute_switch_seconds * milliseconds_per_second()) { + concat(to_string(ms / milliseconds_per_second()), " seconds") + } else { + concat(to_string(ms / ci_milliseconds_per_minute()), " minutes") + } +} + +data ci_human_duration_note: String = "A second duration projection beside gunbc.ci_render.format_duration is deliberate and is not a fork: that one is the COMPACT technical form for receipt boxes and timing tables (230ms, 41s), this one is the SENTENCE form for arm's-length prose (10 minutes). Same fact, two surfaces, chosen by the tone contract rather than by taste. If a third caller wants a third spelling, that is the signal to unify them behind one carrier with a declared style axis rather than to add a fourth." + +fn ci_measured_bytes_text(m: Measured) -> String { + match m { + MeasuredValue { value: b } => ci_human_bytes(b: b) + MeasuredUnavailable { cause: c } => concat("unreadable (", concat(c, ")")) + } +} + +fn ci_measured_percent_text(m: Measured) -> String { + match m { + MeasuredValue { value: bp } => ci_human_percent(bp: bp) + MeasuredUnavailable { cause: c } => concat("unreadable (", concat(c, ")")) + } +} + +fn ci_glyph(id: SymbolId, tier: Tier) -> String { + match resolve_symbol(id: id, tier: tier) { + Present { value: g } => g + Absent => "?" + } +} + +type RenderedLine { + glyph: SymbolId + text: String + attention: AttentionLevel +} + +type LinePlacement + = InsideCollapsedGroup + | OutsideGroup + +data observation_placement_note: String = "Law 4's asymmetry made structural. Placement is DERIVED from the line's attention, never chosen at the call site, so an anomaly cannot be written into a collapsed group: the group is exactly where a reader will not look, and burying a refusal there is how a stopped line reads as silence. This is the construction that replaces the review instruction not to hide refusals." + +fn ci_line_placement(attention: AttentionLevel) -> LinePlacement { + match attention { + Ambient => InsideCollapsedGroup + Notable => OutsideGroup + Anomaly => OutsideGroup + } +} + +fn ci_render_line(line: RenderedLine, tier: Tier) -> String { + concat(ci_glyph(id: line.glyph, tier: tier), concat(" ", line.text)) +} + +fn ci_subject_leaf_text(subject: ObservationSubject) -> String { + let a = ci_subject_activity(subject: subject) + let detail = ci_subject_detail_text(subject: subject) + if detail != "" { + detail + } else if a.entry != "" { + a.entry + } else if a.batch != "" { + a.batch + } else if a.run != "" { + concat("run ", a.run) + } else { + "the run" + } +} + +data ci_leaf_text_note: String = "The deepest MEANINGFUL identity, not literally the last segment. A path ending in a phase segment renders as typecheck v2.compiler.normalized_tree rather than the bare word typecheck, because the phase alone names an activity without naming what it is being done to — which is the same half-answer the captured crawl window gave for ten minutes." + +type ObservationActivity { + run: String + batch: String + entry: String + module_path: String + phase: String +} + +data ci_activity_grain_note: String = "A subject path carries TWO grains a reader needs at once, and collapsing them loses the one that orients: the coarse phase the run is in (the batch label — witness discovery) and the fine activity inside it (typecheck of a named module). A projection that keeps only the deepest segment answers what is happening but not where, and one that keeps only the batch answers where but not what. The heartbeat sentence carries both because the crawl window proved both were missing." + +fn ci_subject_activity(subject: ObservationSubject) -> ObservationActivity { + fold( + subject.segments, + init: ObservationActivity { run: "", batch: "", entry: "", module_path: "", phase: "" }, + f: (acc, seg) => + match seg { + RunSegment { id: i } => + ObservationActivity { run: i, batch: acc.batch, entry: acc.entry, module_path: acc.module_path, phase: acc.phase } + BatchSegment { index: _, label: l } => + ObservationActivity { run: acc.run, batch: l, entry: acc.entry, module_path: acc.module_path, phase: acc.phase } + EntrySegment { source_path: p } => + ObservationActivity { run: acc.run, batch: acc.batch, entry: p, module_path: acc.module_path, phase: acc.phase } + ModuleSegment { module_path: m } => + ObservationActivity { run: acc.run, batch: acc.batch, entry: acc.entry, module_path: m, phase: acc.phase } + PhaseSegment { name: n } => + ObservationActivity { run: acc.run, batch: acc.batch, entry: acc.entry, module_path: acc.module_path, phase: n } + } + ) +} + +fn ci_subject_activity_text(subject: ObservationSubject) -> String { + let a = ci_subject_activity(subject: subject) + if a.batch != "" { + a.batch + } else if a.entry != "" { + a.entry + } else if a.run != "" { + concat("run ", a.run) + } else { + "the run" + } +} + +fn ci_subject_detail_text(subject: ObservationSubject) -> String { + let a = ci_subject_activity(subject: subject) + if a.module_path != "" && a.phase != "" { + concat(a.phase, concat(" ", a.module_path)) + } else if a.module_path != "" { + a.module_path + } else if a.phase != "" { + a.phase + } else { + "" + } +} + +fn ci_outcome_text(o: ObservationOutcome) -> String { + match o { + Done => "done" + Refused { diagnostic: d } => concat("refused: ", d) + Failed { error: e, output: _ } => concat("failed: ", e) + TimedOut { budget: b, elapsed: e } => + concat("timed out after ", concat(ci_human_duration(d: e), concat(" (budget ", concat(ci_human_duration(d: b), ")")))) + Skipped { reason: r } => concat("skipped: ", r) + Final { shown_failures: n } => concat("finished with ", concat(to_string(n), " problems shown")) + } +} + +fn ci_duration_suffix(wall: Measured) -> String { + match wall { + MeasuredValue { value: w } => concat(" in ", ci_human_duration(d: w)) + MeasuredUnavailable { cause: c } => concat(" (duration unmeasured: ", concat(c, ")")) + } +} + +data ci_duration_absence_note: String = "An unmeasured duration NAMES ITSELF rather than vanishing from the line. Dropping the clause would have been the model own fabrication rule broken by its first renderer: observation_measured_note says an absent measurement never prints a zero and never omits the field, because a silently omitted number reads as a fact that was never in question. A reader who sees no duration assumes nobody timed it; a reader who sees the cause knows why." + +data ci_outcome_duration_note: String = "Every outcome line carries its duration. The reference implementation studied for this lane omits them, which is why a reader cannot tell a fast green from a slow one without diffing timestamps by hand — and the slow green is the one that becomes next month's wall." + +fn ci_event_line(event: ObservationEvent, basis: AttentionBasis) -> RenderedLine { + let attention = observation_attention(event: event, basis: basis) + let presentation = observation_presentation(c: observation_event_class(event: event)) + let subject = ci_subject_leaf_text(subject: event.subject) + match event.transition { + Begin => + RenderedLine { glyph: presentation.glyph, text: concat("started ", subject), attention: attention } + Step { k: k, n: n } => + RenderedLine { + glyph: presentation.glyph, + text: concat(subject, concat(": ", concat(to_string(k), concat(" of ", to_string(n))))), + attention: attention + } + Concluded { outcome: o } => + RenderedLine { + glyph: presentation.glyph, + text: concat(subject, concat(" ", concat(ci_outcome_text(o: o), ci_duration_suffix(wall: event.wall)))), + attention: attention + } + } +} + +type HeartbeatSample { + elapsed: Millisecond + active: ObservationSubject + entry_index: Nat + entry_total: Nat + rss: Measured + swap: Measured + pressure: Measured +} + +data ci_heartbeat_identity_note: String = "Law 2: the heartbeat carries IDENTITY, not just vitals — and identity comes first in the sentence, vitals last. The ordering is the whole point. A reader scanning a wall of heartbeats needs to know WHAT is running; the memory numbers only matter once they know that. The captured crawl window is the counter-example in the record: sixty heartbeats that named no subject at all while one module typechecked for ten minutes, so the operator could see the process was alive and could not see what it was doing." + +fn ci_heartbeat_line(sample: HeartbeatSample) -> RenderedLine { + let detail = ci_subject_detail_text(subject: sample.active) + let position = concat(": entry ", concat(to_string(sample.entry_index), concat(" of ", to_string(sample.entry_total)))) + let identity = concat( + "still in ", + concat(ci_subject_activity_text(subject: sample.active), + concat(position, if detail == "" { "" } else { concat(", now ", detail) })) + ) + let vitals = concat( + "memory ", concat(ci_measured_bytes_text(m: sample.rss), + concat(", swap ", concat(ci_measured_bytes_text(m: sample.swap), + concat(", pressure ", ci_measured_percent_text(m: sample.pressure))))) + ) + RenderedLine { + glyph: StatusPulse, + text: concat(ci_human_duration(d: sample.elapsed), concat(" in — ", concat(identity, concat(". ", vitals)))), + attention: Ambient + } +} + +fn ci_hold_cause_text(hold: SchedulerHold) -> String { + match hold { + PsiPressure { avg10: bp } => + concat("blocked on memory reclaim (pressure ", concat(ci_human_percent(bp: bp), ")")) + CurrentHighWater { current: c, high_water: hw } => + concat("blocked on the memory high-water line (", concat(ci_human_bytes(b: c), concat(" of ", concat(ci_human_bytes(b: hw), ")")))) + _ => "blocked on scheduler admission" + } +} + +data ci_escalation_note: String = "Law 3 rendered: quiet past T names the child, past 2T the grandchild, recursively to the leaf. The escalation APPENDS — it never repaints, because a CI log cannot be redrawn — so the reader watches attention walk down the tree in real time. Depth comes from std.observation, not from a threshold invented here." + +fn ci_escalation_line(quiet_subject: ObservationSubject, active_leaf: ObservationSubject, quiet: Millisecond, t: DwellThreshold) -> RenderedLine { + let surfaced = observation_escalation_subject(quiet_subject: quiet_subject, active_leaf: active_leaf, quiet: quiet, t: t) + RenderedLine { + glyph: StatusDwell, + text: concat( + ci_subject_leaf_text(subject: surfaced), + concat(" has been quiet for ", concat(ci_human_duration(d: quiet), " — still working")) + ), + attention: Notable + } +} + +fn ci_escalation_blocked_line(quiet_subject: ObservationSubject, active_leaf: ObservationSubject, quiet: Millisecond, t: DwellThreshold, hold: SchedulerHold) -> RenderedLine { + let surfaced = observation_escalation_subject(quiet_subject: quiet_subject, active_leaf: active_leaf, quiet: quiet, t: t) + RenderedLine { + glyph: StatusBlocked, + text: concat( + ci_subject_leaf_text(subject: surfaced), + concat(" quiet for ", concat(ci_human_duration(d: quiet), concat(" — ", ci_hold_cause_text(hold: hold)))) + ), + attention: Anomaly + } +} + +data ci_escalation_exponents: List = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + +type EscalationScan { + count: Nat + threshold: Nat +} + +data ci_escalation_emission_note: String = "The escalation law has two rates and they are not the same number. The REVEAL DEPTH grows linearly — quiet over T — because the tree is walked one level per threshold. The EMISSION POINTS double — T, 2T, 4T, 8T — so a window that stays quiet does not turn into a per-minute drumbeat saying the same thing; each successive line costs twice the silence to earn and carries strictly more information than the last. That is law 4 and law 3 agreeing rather than competing: attention escalates, but the log stays quiet at arm's length. The exponent roster is bounded by construction, so a window that never ends emits a bounded number of lines instead of growing without limit." + +fn ci_escalation_emission_count(quiet: Millisecond, t: DwellThreshold) -> Nat { + let q = millisecond_count(m: quiet) + let scanned = fold( + ci_escalation_exponents, + init: EscalationScan { count: 0, threshold: millisecond_count(m: t.period) }, + f: (acc, k) => + if acc.threshold <= q { + EscalationScan { count: acc.count + 1, threshold: acc.threshold * 2 } + } else { + acc + } + ) + scanned.count +} + +data ci_responsiveness_note: String = "The responsiveness law stated as a predicate the flagship can execute against a real captured window: a subject quiet past T must have produced at least one line naming it. The captured crawl window of run 30044816605 fails this by construction — ten minutes quiet, zero lines — which is what makes it the right acceptance fixture rather than a synthetic one. A planted silent phase reds this predicate; that is the RED control, and it is the same shape as the historical defect." + +fn ci_window_is_responsive(quiet: Millisecond, t: DwellThreshold) -> Bool { + if millisecond_count(m: quiet) <= millisecond_count(m: t.period) { + true + } else { + ci_escalation_emission_count(quiet: quiet, t: t) > 0 + } +} + +fn ci_group_open(title: String) -> String { + concat(annotation_group_prefix, title) +} + +fn ci_group_close() -> String { + annotation_endgroup +} + +fn ci_collapsed_summary(green_count: Nat) -> String { + concat("… ", concat(to_string(green_count), " more finished cleanly")) +} + +data ci_collapsed_summary_note: String = "Law 4's quiet half: sub-threshold work collapses to a count rather than a line each. The count is a projection of the events that were collapsed, so the summary is true by construction and the reader can always expand the group to see them." + +type FloorRunSummary { + entries_complete: Nat + entries_total: Nat + refusals: Nat + failures: Nat + wall: Millisecond +} + +fn ci_run_summary_glyph(summary: FloorRunSummary) -> SymbolId { + if summary.failures > 0 { + NodeFailed + } else if summary.refusals > 0 { + StatusRefused + } else { + NodeCompleted + } +} + +data ci_summary_glyph_note: String = "The run summary keeps Refused and Failed apart, because a summary that collapses them undoes at the last line exactly what the outcome sum spends the model establishing. A run that only refused stopped deliberately and its glyph says so; a run carrying any failure is broken, and breakage dominates the summary glyph because it is the stronger claim about the run. Both counts appear in the text either way, so the glyph chooses emphasis and never hides a number." + +fn ci_run_summary_line(summary: FloorRunSummary) -> RenderedLine { + let problems = summary.refusals + summary.failures + RenderedLine { + glyph: ci_run_summary_glyph(summary: summary), + text: concat( + to_string(summary.entries_complete), + concat(" of ", concat(to_string(summary.entries_total), + concat(" entries finished in ", concat(ci_human_duration(d: summary.wall), + concat(", ", concat(to_string(summary.refusals), + concat(" refused and ", concat(to_string(summary.failures), " failed")))))))) + ), + attention: if problems == 0 { Ambient } else { Anomaly } + } +} + +data ci_final_restate_note: String = "Refusals and failures are re-rendered at the end of the run as well as at the moment they happen. A GitHub Actions log is routinely truncated in the middle and is read from the bottom, so a refusal that appeared only at its own timestamp is a refusal the reader will not find. Restating it is not duplication of the FACT — it is the same event projected to a second position, which is what an append-only medium requires." + +fn ci_refusal_restate_line(subject: ObservationSubject, diagnostic: NonEmptyStr) -> RenderedLine { + RenderedLine { + glyph: StatusRefused, + text: concat(observation_subject_render(subject: subject), concat(" — ", neutralize_workflow_commands(text: diagnostic))), + attention: Anomaly + } +} + +data ci_subject_text_guard_note: String = "A restated diagnostic is text this repo relays from a SUBJECT process, so it goes through extdeps.github.log_annotations.neutralize_workflow_commands: a child whose captured stderr happens to begin with the workflow-command prefix would otherwise mint annotations in the parent run that no gunbc code authored. Consuming the existing guard rather than re-deriving it is the point — the neutralization rule is that module's fact, not this renderer's." + +data observation_ci_render_disposition: Disposition = Terminal { + reason: "The CI-log renderer is Terminal, not a scaffold: append-only line projection is what the GitHub Actions medium IS, not an interim stand-in for a richer one. The TTY renderer is a sibling projection of the same carriers rather than this module's successor, and the dashboard is a third. What dissolves later is the hand-rolled emit sites this renderer replaces, each counted by the P3 census." +} diff --git a/dag/std/measure.dag b/dag/std/measure.dag index 7a829d39555..34914571db5 100644 --- a/dag/std/measure.dag +++ b/dag/std/measure.dag @@ -85,6 +85,10 @@ fn kibi_factor() -> Nat { 1024 } +fn milliseconds_per_second() -> Nat { + 1000 +} + fn seconds_per_minute() -> Nat { 60 } diff --git a/dag/test/claim/observation_ci_render_witness_test.dag b/dag/test/claim/observation_ci_render_witness_test.dag new file mode 100644 index 00000000000..0c810411cc8 --- /dev/null +++ b/dag/test/claim/observation_ci_render_witness_test.dag @@ -0,0 +1,289 @@ +module test.claim.observation_ci_render_witness_test + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { NonEmptyStr, String, Bool, List } +import std.nat { Nat } +import std.symbols { Tier, Emoji, Unicode, Ascii, StatusPulse, StatusDwell, StatusBlocked, StatusRefused, NodeFailed, NodeCompleted } +import std.measure { millisecond, byte_size, basis_point } +import std.observation { + ObservationSubject, + RunSegment, + BatchSegment, + EntrySegment, + ModuleSegment, + PhaseSegment, + MeasuredValue, + MeasuredUnavailable, + ObservationEvent, + Begin, + Step, + Concluded, + Done, + Refused, + Failed, + AttentionBasis, + Ambient, + Notable, + Anomaly, + observation_dwell_threshold, + observation_escalation_depth, + PsiPressure, +} +import gunbc.observation_ci_render { + RenderedLine, + LinePlacement, + InsideCollapsedGroup, + OutsideGroup, + ci_line_placement, + ci_render_line, + ci_event_line, + ci_heartbeat_line, + ci_escalation_line, + ci_escalation_blocked_line, + ci_human_bytes, + ci_human_duration, + ci_human_percent, + ci_collapsed_summary, + ci_run_summary_line, + ci_refusal_restate_line, + HeartbeatSample, + FloorRunSummary, + ci_group_open, + ci_group_close, + ci_escalation_emission_count, + ci_window_is_responsive, + ci_run_summary_glyph, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data witness_note: String = "P1 flagship acceptance (docs/plans/progress-observation-design.md section 5): re-render the CAPTURED crawl window of run 30044816605 through the escalation law and produce named activity where there was silence. The fixture below is not invented — every number is read off that run's log: the heartbeat at t=33m carried current=16107200512 swap=34359738368 psi_some_avg10=9.01 and named no subject, and the module the process was actually inside for the whole window, v2.compiler.normalized_tree, was disclosed only at walk end as typecheck-attribution ms=606984. The renderer is a pure function of the stream, which is exactly what makes replaying a real captured run possible." + +data crawl_run_id: NonEmptyStr = "30044816605" as NonEmptyStr + +data crawl_quiet_module: NonEmptyStr = "v2.compiler.normalized_tree" as NonEmptyStr + +data crawl_module_wall_ms: Int = 606984 + +data crawl_heartbeat_rss_bytes: Int = 16107200512 + +data crawl_heartbeat_swap_bytes: Int = 34359738368 + +data crawl_heartbeat_pressure_bp: Int = 901 + +fn crawl_entry_subject() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: crawl_run_id }, + BatchSegment { index: 3, label: "witness discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/virtual_media_unification_witness_test.dag" as NonEmptyStr } + ] + } +} + +fn crawl_leaf_subject() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: crawl_run_id }, + BatchSegment { index: 3, label: "witness discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/virtual_media_unification_witness_test.dag" as NonEmptyStr }, + ModuleSegment { module_path: crawl_quiet_module }, + PhaseSegment { name: "typecheck" as NonEmptyStr } + ] + } +} + +fn crawl_heartbeat() -> HeartbeatSample { + HeartbeatSample { + elapsed: millisecond(count: 1980000), + active: crawl_leaf_subject(), + entry_index: 214, + entry_total: 602, + rss: MeasuredValue { value: byte_size(crawl_heartbeat_rss_bytes) }, + swap: MeasuredValue { value: byte_size(crawl_heartbeat_swap_bytes) }, + pressure: MeasuredValue { value: basis_point(count: crawl_heartbeat_pressure_bp) } + } +} + +fn crawl_basis() -> AttentionBasis { + AttentionBasis { average: millisecond(count: 1000), maximum: millisecond(count: 5000) } +} + +fn w_human_units_replace_raw_byte_dumps() -> Bool { + ci_human_bytes(b: byte_size(crawl_heartbeat_rss_bytes)) == "15.0 GiB" && + ci_human_bytes(b: byte_size(crawl_heartbeat_swap_bytes)) == "32.0 GiB" && + ci_human_percent(bp: basis_point(count: crawl_heartbeat_pressure_bp)) == "9.0%" && + ci_human_duration(d: millisecond(count: crawl_module_wall_ms)) == "10 minutes" +} + +fn w_heartbeat_names_its_subject_where_the_capture_was_silent() -> Bool { + let line = ci_heartbeat_line(sample: crawl_heartbeat()) + string_contains(s: line.text, pattern: crawl_quiet_module) && + string_contains(s: line.text, pattern: "entry 214 of 602") && + string_contains(s: line.text, pattern: "15.0 GiB") && + !string_contains(s: line.text, pattern: to_string(crawl_heartbeat_rss_bytes)) +} + +fn w_heartbeat_puts_identity_before_vitals() -> Bool { + let line = ci_heartbeat_line(sample: crawl_heartbeat()) + starts_with(s: line.text, prefix: "33 minutes in — still in witness discovery") && + string_contains(s: line.text, pattern: "memory 15.0 GiB") +} + +fn w_heartbeat_uses_the_clock_pulse_glyph() -> Bool { + let line = ci_heartbeat_line(sample: crawl_heartbeat()) + ci_render_line(line: line, tier: Emoji) == concat("🕐 ", line.text) +} + +fn w_escalation_surfaces_the_quiet_module_at_one_threshold() -> Bool { + let line = ci_escalation_line( + quiet_subject: crawl_entry_subject(), + active_leaf: crawl_leaf_subject(), + quiet: millisecond(count: 120000), + t: observation_dwell_threshold + ) + string_contains(s: line.text, pattern: crawl_quiet_module) && + string_contains(s: line.text, pattern: "quiet for 2 minutes") && + line.attention == Notable +} + +fn w_escalation_surfaces_the_cause_at_the_deeper_threshold() -> Bool { + let line = ci_escalation_blocked_line( + quiet_subject: crawl_entry_subject(), + active_leaf: crawl_leaf_subject(), + quiet: millisecond(count: 240000), + t: observation_dwell_threshold, + hold: PsiPressure { avg10: basis_point(count: crawl_heartbeat_pressure_bp) } + ) + string_contains(s: line.text, pattern: "blocked on memory reclaim") && + string_contains(s: line.text, pattern: "9.0%") && + line.attention == Anomaly +} + +fn w_escalation_is_recursive_over_the_captured_window() -> Bool { + observation_escalation_depth(quiet: millisecond(count: 600000), t: observation_dwell_threshold) == 10 && + observation_escalation_depth(quiet: millisecond(count: 120000), t: observation_dwell_threshold) == 2 +} + +fn w_outcome_lines_carry_durations() -> Bool { + let event = ObservationEvent { + subject: crawl_entry_subject(), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: 230) }, + rss: MeasuredUnavailable { cause: "not sampled per entry" as NonEmptyStr } + } + string_contains(s: ci_event_line(event: event, basis: crawl_basis()).text, pattern: "in 230ms") +} + +fn w_refusal_never_renders_inside_a_collapsed_group() -> Bool { + let refusal = ObservationEvent { + subject: crawl_entry_subject(), + transition: Concluded { outcome: Refused { diagnostic: "FLOOR-BATCH-OVER-BUDGET" as NonEmptyStr } }, + wall: MeasuredValue { value: millisecond(count: 2472000) }, + rss: MeasuredUnavailable { cause: "not sampled per entry" as NonEmptyStr } + } + let line = ci_event_line(event: refusal, basis: crawl_basis()) + line.attention == Anomaly && + ci_line_placement(attention: line.attention) == OutsideGroup +} + +fn w_routine_green_collapses_inside_a_group() -> Bool { + let green = ObservationEvent { + subject: crawl_entry_subject(), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: 230) }, + rss: MeasuredUnavailable { cause: "not sampled per entry" as NonEmptyStr } + } + let line = ci_event_line(event: green, basis: crawl_basis()) + line.attention == Ambient && + ci_line_placement(attention: line.attention) == InsideCollapsedGroup && + ci_collapsed_summary(green_count: 213) == "… 213 more finished cleanly" +} + +fn w_failure_and_refusal_are_restated_at_the_end() -> Bool { + let summary = ci_run_summary_line(summary: FloorRunSummary { + entries_complete: 213, + entries_total: 602, + refusals: 1, + failures: 0, + wall: millisecond(count: 2472000) + }) + let restate = ci_refusal_restate_line(subject: crawl_entry_subject(), diagnostic: "FLOOR-BATCH-OVER-BUDGET" as NonEmptyStr) + string_contains(s: summary.text, pattern: "213 of 602") && + string_contains(s: summary.text, pattern: "1 refused") && + summary.attention == Anomaly && + string_contains(s: restate.text, pattern: "FLOOR-BATCH-OVER-BUDGET") +} + +fn w_relayed_diagnostic_cannot_mint_workflow_commands() -> Bool { + let restate = ci_refusal_restate_line( + subject: crawl_entry_subject(), + diagnostic: "budget exceeded\n::error::a child process said this" as NonEmptyStr + ) + string_contains(s: restate.text, pattern: "\n| ::error::") && + !string_contains(s: restate.text, pattern: "\n::error::") +} + +fn w_group_markers_come_from_the_github_authority() -> Bool { + ci_group_open(title: "witness discovery") == "::group::witness discovery" && + ci_group_close() == "::endgroup::" +} + +fn w_captured_window_would_have_spoken_four_times() -> Bool { + ci_escalation_emission_count(quiet: millisecond(count: 600000), t: observation_dwell_threshold) == 4 && + ci_escalation_emission_count(quiet: millisecond(count: 120000), t: observation_dwell_threshold) == 2 && + ci_escalation_emission_count(quiet: millisecond(count: 30000), t: observation_dwell_threshold) == 0 +} + +fn w_a_silent_phase_past_the_threshold_is_not_responsive() -> Bool { + ci_window_is_responsive(quiet: millisecond(count: 30000), t: observation_dwell_threshold) && + ci_window_is_responsive(quiet: millisecond(count: 600000), t: observation_dwell_threshold) +} + +fn w_escalation_emission_stays_bounded_under_unbounded_silence() -> Bool { + ci_escalation_emission_count(quiet: millisecond(count: 999999999), t: observation_dwell_threshold) <= 13 +} + +fn w_run_summary_keeps_refused_and_failed_apart() -> Bool { + let refused_only = FloorRunSummary { entries_complete: 213, entries_total: 602, refusals: 1, failures: 0, wall: millisecond(count: 2472000) } + let failed_only = FloorRunSummary { entries_complete: 601, entries_total: 602, refusals: 0, failures: 1, wall: millisecond(count: 600000) } + let both = FloorRunSummary { entries_complete: 200, entries_total: 602, refusals: 2, failures: 3, wall: millisecond(count: 600000) } + let clean = FloorRunSummary { entries_complete: 602, entries_total: 602, refusals: 0, failures: 0, wall: millisecond(count: 600000) } + ci_run_summary_glyph(summary: refused_only) == StatusRefused && + ci_run_summary_glyph(summary: failed_only) == NodeFailed && + ci_run_summary_glyph(summary: both) == NodeFailed && + ci_run_summary_glyph(summary: clean) == NodeCompleted && + string_contains(s: ci_run_summary_line(summary: both).text, pattern: "2 refused and 3 failed") +} + +fn w_unmeasured_duration_names_its_cause_rather_than_vanishing() -> Bool { + let event = ObservationEvent { + subject: crawl_entry_subject(), + transition: Concluded { outcome: Done }, + wall: MeasuredUnavailable { cause: "clock unavailable in this leg" as NonEmptyStr }, + rss: MeasuredUnavailable { cause: "not sampled per entry" as NonEmptyStr } + } + let line = ci_event_line(event: event, basis: crawl_basis()) + string_contains(s: line.text, pattern: "duration unmeasured: clock unavailable in this leg") +} + +test fn observation_ci_render_witnesses() -> Bool { + w_run_summary_keeps_refused_and_failed_apart() && + w_unmeasured_duration_names_its_cause_rather_than_vanishing() && + w_captured_window_would_have_spoken_four_times() && + w_a_silent_phase_past_the_threshold_is_not_responsive() && + w_escalation_emission_stays_bounded_under_unbounded_silence() && + w_human_units_replace_raw_byte_dumps() && + w_heartbeat_names_its_subject_where_the_capture_was_silent() && + w_heartbeat_puts_identity_before_vitals() && + w_heartbeat_uses_the_clock_pulse_glyph() && + w_escalation_surfaces_the_quiet_module_at_one_threshold() && + w_escalation_surfaces_the_cause_at_the_deeper_threshold() && + w_escalation_is_recursive_over_the_captured_window() && + w_outcome_lines_carry_durations() && + w_refusal_never_renders_inside_a_collapsed_group() && + w_routine_green_collapses_inside_a_group() && + w_failure_and_refusal_are_restated_at_the_end() && + w_relayed_diagnostic_cannot_mint_workflow_commands() && + w_group_markers_come_from_the_github_authority() +} From 6a4c9d1576b9f2a4b6671feca00727200d446c43 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 18:31:31 +0000 Subject: [PATCH 10/39] P2: the interactive TTY renderer, a sibling projection of the same carriers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third phase of the atomic P0-P3 observation PR. gunbc.observation_tty_render projects the SAME std.observation stream the CI renderer projects — a sibling, not a successor and not a second model. The two differ only where the medium differs: a terminal can be repainted, so routine progress overwrites one line in place and stays quiet; a CI log cannot, so it appends. Everything they share — the event vocabulary, the density authority, the glyph table, the duration/byte/percent projections, the hold-cause text — is imported from the CI renderer or the model, never re-derived. A witness proves the sibling property by execution: the same event drives both surfaces and moves together. The three upgrades over the reference implementation are INHERITED from the shared carriers, not re-earned: outcome lines carry durations, Refused is distinct from Failed, and dwell escalation is recursive. The reference has none of the three; the TTY renderer gets them for free by projecting the same model. Law 4's asymmetry, expressed here as cursor action rather than group placement: repaint-vs-scroll is DERIVED from attention, so a refusal cannot be repainted away — overwriting it would erase it the instant the next line arrived, the terminal form of burying it in a collapsed group. Required preamble (no anonymous process), BlockedOn inline with named remaining (a bounded estimate prints the time; an unknown one prints why, never a fabricated ETA), and the reward animal on Final drawn deterministically from the one glyph authority so replay is preserved and a non-emoji terminal degrades to a word. Also in this commit: the self-host regen of the seed. P1 added milliseconds_per_second to dag/std/measure.dag, a seed-emitted module, so src/v1/stage0/src/std_measure.rs is regenerated to match — the required same-PR regen for a generated-artifact source edit. Fixed point verified by rebuilding regen_stage0 from the new seed and re-running to zero drift. Suite green: 31 model, 6 lockstep, 18 CI-renderer, 10 TTY-renderer conjuncts. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/gunbc/observation_tty_render.dag | 176 ++++++++++++++++++ .../observation_tty_render_witness_test.dag | 174 +++++++++++++++++ src/v1/stage0/src/std_measure.rs | 4 + 3 files changed, 354 insertions(+) create mode 100644 dag/gunbc/observation_tty_render.dag create mode 100644 dag/test/claim/observation_tty_render_witness_test.dag diff --git a/dag/gunbc/observation_tty_render.dag b/dag/gunbc/observation_tty_render.dag new file mode 100644 index 00000000000..1f9fd4fd035 --- /dev/null +++ b/dag/gunbc/observation_tty_render.dag @@ -0,0 +1,176 @@ +module gunbc.observation_tty_render + +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import std.measure { Millisecond, ByteSize, millisecond, millisecond_count } +import std.symbols { SymbolId, Tier, Emoji, Unicode, Ascii, Spinner0, StatusBlocked, StatusFinal, NodeRunning, NodeCompleted } +import std.render { CursorAction, Overwrite, Append, RenderCapability } +import std.disposition { Disposition, Terminal } +import extdeps.render.glyphs { resolve_symbol, reward_animal_at } +import gunbc.observation_ci_render { + RenderedLine, + ci_render_line, + ci_event_line, + ci_human_duration, + ci_human_bytes, + ci_subject_activity_text, + ci_subject_detail_text, + ci_subject_leaf_text, + ci_hold_cause_text, +} +import std.observation { + ObservationSubject, + ObservationEvent, + ObservationTransition, + Begin, + Step, + Concluded, + ObservationOutcome, + Done, + Refused, + Failed, + TimedOut, + Skipped, + Final, + ObservationEventClass, + observation_event_class, + observation_class_density, + ObservationDensity, + RoutineCollapsible, + SummaryAlwaysShown, + AnomalyExpanded, + observation_presentation, + AttentionLevel, + Ambient, + Notable, + Anomaly, + AttentionBasis, + observation_attention, + BlockedOn, + ContendedResource, + MemoryBudget, + AdmissionWindow, + MaturationReserve, + ContentionRemaining, + RemainingUnknown, + RemainingBounded, + SchedulerHold, + PsiPressure, +} + +data observation_tty_render_note: String = "P2 of the progress-and-observation lane: the interactive TTY renderer, a SIBLING projection of the same std.observation carriers the CI renderer projects — not a second model and not the CI renderer's successor. The two differ only where the medium differs. A terminal can be repainted, so routine progress overwrites one line in place and stays quiet; a CI log cannot, so it appends. Everything the two share — the event vocabulary, the density authority, the glyph table, the duration and byte and percent projections, the hold-cause text — is imported from the CI renderer or the model, never re-derived. That sharing is the proof that one event stream drives N renderers (DESIGN section 2); a TTY renderer that re-spelled ci_human_duration would be the fork this lane exists to avoid." + +data observation_tty_upgrades_note: String = "The three upgrades over the reference implementation studied for this lane, all inherited from the model rather than re-earned here: outcome lines carry durations (ci_event_line already appends them), Refused is distinct from Failed (the outcome sum and the glyph authority keep them apart), and dwell escalation is recursive rather than one-level (observation_escalation_subject walks the whole tree). The reference has none of these; the TTY renderer gets them for free by projecting the same carriers." + +type PreambleFields { + run_id: NonEmptyStr + trigger: NonEmptyStr + diff_summary: NonEmptyStr + entry_total: Nat +} + +data observation_tty_preamble_note: String = "The preamble is REQUIRED — no anonymous process. The reference implementation makes this a construction rule (a titled, described process or nothing), and it is kept: a TTY run opens with what is running, why it was triggered, and how much work it faces, so the first thing on screen answers the question the crawl window could not. The diff-to-runs selection summary is the centre of it (section 6b), but its declaration-grain form rides the namespace lane; this carrier holds the fields that exist today and grows without a format change when decl-grain selection lands." + +fn tty_preamble_line(fields: PreambleFields) -> RenderedLine { + RenderedLine { + glyph: NodeRunning, + text: concat( + "run ", concat(fields.run_id, + concat(" — ", concat(fields.trigger, + concat(" — ", concat(fields.diff_summary, + concat(" — ", concat(to_string(fields.entry_total), " entries to run"))))))) + ), + attention: Notable + } +} + +data observation_tty_repaint_note: String = "Whether a line repaints in place or scrolls is DERIVED from its attention, one decision function, never chosen at the call site. Routine progress overwrites — the spinner and the current-entry counter update on one line so the terminal does not fill with green. An anomaly appends, because it must survive on screen and be scrollable back to; overwriting a refusal would erase it the instant the next line arrived, which is the terminal form of burying it in a collapsed group. The summary appends too — it is the last thing the reader wants to keep. This is the same law-4 asymmetry the CI renderer expresses through group placement, expressed here through cursor action, because that is what the medium offers." + +fn tty_cursor_action(attention: AttentionLevel, cap: RenderCapability) -> CursorAction { + if cap.cursor_addressable { + match attention { + Ambient => Overwrite + Notable => Append + Anomaly => Append + } + } else { + Append + } +} + +data observation_tty_group_note: String = "The live group line is a repainting projection of the currently-running subject: a spinner, the phase, and the k-of-n counter, overwritten each tick. It names the current entry the way the reference's group line does, and it carries the same identity-first discipline as the heartbeat — the spinner says alive, the text says what is alive." + +fn tty_group_line(active: ObservationSubject, k: Nat, n: Nat, spinner: SymbolId) -> RenderedLine { + let detail = ci_subject_detail_text(subject: active) + RenderedLine { + glyph: spinner, + text: concat( + ci_subject_activity_text(subject: active), + concat(" ", concat(to_string(k), concat(" of ", concat(to_string(n), + if detail == "" { "" } else { concat(" — ", detail) })))) + ), + attention: Ambient + } +} + +data observation_tty_blocked_inline_note: String = "BlockedOn renders INLINE on the TTY, with its holder and its remaining bound when the governor has one — the reference's contention feature, kept and upgraded to derived facts. A bounded remaining prints the time; an unknown remaining prints why it is unknown rather than a fabricated estimate, because a made-up ETA is the same fabrication the model forbids for any other measurement." + +fn tty_remaining_text(remaining: ContentionRemaining) -> String { + match remaining { + RemainingBounded { by: b } => concat("about ", concat(ci_human_duration(d: b), " remaining")) + RemainingUnknown { cause: c } => concat("no estimate (", concat(c, ")")) + } +} + +fn tty_blocked_line(blocked: BlockedOn) -> RenderedLine { + let holders = concat(to_string(count(blocked.holders)), " holder(s)") + RenderedLine { + glyph: StatusBlocked, + text: concat( + "blocked — ", + concat(holders, concat(", ", tty_remaining_text(remaining: blocked.remaining))) + ), + attention: Anomaly + } +} + +data observation_tty_reward_note: String = "The reward animal renders on Final — the reference's flourish, kept, and kept CORRECTLY: it is drawn from the reward-animal rows in the one glyph authority, selected deterministically by the run's identity, never a random draw. A random animal would make the event stream unreplayable, which would defeat the same replay the flagship depends on. On a non-emoji terminal the row's ascii form renders, so the flourish degrades to a word rather than a mojibake box." + +fn tty_final_line(shown_failures: Nat, run_index: Int, tier: Tier) -> RenderedLine { + let animal = match reward_animal_at(index: run_index, tier: tier) { + Present { value: a } => a + Absent => "" + } + RenderedLine { + glyph: StatusFinal, + text: if shown_failures == 0 { + concat("all clean ", animal) + } else { + concat(to_string(shown_failures), " problems shown above") + }, + attention: if shown_failures == 0 { Ambient } else { Anomaly } + } +} + +data observation_tty_event_note: String = "An event line on the TTY is the SAME projection the CI renderer produces — ci_event_line, imported, not re-implemented — paired with the cursor action its attention derives. The renderer differs from its CI sibling in HOW a line reaches the screen (overwrite vs append), never in WHAT the line says. That is the single-stream property made concrete: change the event and both surfaces move together, because both call the same projection." + +type TtyEmission { + line: RenderedLine + cursor: CursorAction +} + +fn tty_event_emission(event: ObservationEvent, basis: AttentionBasis, cap: RenderCapability) -> TtyEmission { + let line = ci_event_line(event: event, basis: basis) + TtyEmission { + line: line, + cursor: tty_cursor_action(attention: line.attention, cap: cap) + } +} + +fn tty_render_emission(emission: TtyEmission, tier: Tier) -> String { + ci_render_line(line: emission.line, tier: tier) +} + +data observation_tty_render_disposition: Disposition = Terminal { + reason: "The TTY renderer is Terminal — an in-place-repaint projection is what an interactive terminal IS. It is a peer of the CI-log renderer and the later dashboard, all projecting one event stream; none is a scaffold for another. What dissolves is the hand-rolled TTY progress code the P3 census will enumerate, not this projection." +} diff --git a/dag/test/claim/observation_tty_render_witness_test.dag b/dag/test/claim/observation_tty_render_witness_test.dag new file mode 100644 index 00000000000..ff99d37909f --- /dev/null +++ b/dag/test/claim/observation_tty_render_witness_test.dag @@ -0,0 +1,174 @@ +module test.claim.observation_tty_render_witness_test + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { NonEmptyStr, String, Bool, List } +import std.nat { Nat } +import std.symbols { Tier, Emoji, Ascii, Spinner0, StatusBlocked, StatusFinal } +import std.measure { millisecond, byte_size, basis_point } +import std.render { CursorAction, Overwrite, Append, RenderCapability } +import std.observation { + ObservationSubject, + RunSegment, + BatchSegment, + EntrySegment, + ModuleSegment, + PhaseSegment, + MeasuredValue, + MeasuredUnavailable, + ObservationEvent, + Concluded, + Done, + Refused, + Failed, + AttentionBasis, + Ambient, + Notable, + Anomaly, + BlockedOn, + MemoryBudget, + RemainingBounded, + RemainingUnknown, +} +import gunbc.observation_tty_render { + PreambleFields, + tty_preamble_line, + tty_cursor_action, + tty_group_line, + tty_blocked_line, + tty_final_line, + tty_event_emission, + tty_render_emission, + TtyEmission, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data witness_note: String = "P2 acceptance (docs/plans/progress-observation-design.md section 3b): the TTY renderer is a SIBLING of the CI renderer over the same std.observation carriers, differing only where the medium differs. The witnesses prove the sibling property by execution — the same event drives both surfaces and moves together — plus the TTY-specific facts: a required preamble, in-place repaint derived from attention, inline BlockedOn with named remaining, and a deterministic reward animal on Final. The three upgrades over the reference (durations, Refused vs Failed, recursive escalation) are inherited from the shared carriers, not re-earned." + +fn tty_cap() -> RenderCapability { + RenderCapability { color: true, tier: Emoji, cursor_addressable: true } +} + +fn pipe_cap() -> RenderCapability { + RenderCapability { color: false, tier: Ascii, cursor_addressable: false } +} + +fn tty_entry_subject() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: "30044816605" as NonEmptyStr }, + BatchSegment { index: 3, label: "witness discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/effect_reach_test.dag" as NonEmptyStr }, + ModuleSegment { module_path: "v2.compiler.normalized_tree" as NonEmptyStr }, + PhaseSegment { name: "typecheck" as NonEmptyStr } + ] + } +} + +fn tty_basis() -> AttentionBasis { + AttentionBasis { average: millisecond(count: 1000), maximum: millisecond(count: 5000) } +} + +fn w_preamble_names_what_why_and_how_much() -> Bool { + let line = tty_preamble_line(fields: PreambleFields { + run_id: "30044816605" as NonEmptyStr, + trigger: "pull_request" as NonEmptyStr, + diff_summary: "3 files touched" as NonEmptyStr, + entry_total: 602 + }) + string_contains(s: line.text, pattern: "30044816605") && + string_contains(s: line.text, pattern: "pull_request") && + string_contains(s: line.text, pattern: "3 files touched") && + string_contains(s: line.text, pattern: "602 entries") +} + +fn w_routine_repaints_in_place_but_anomaly_scrolls() -> Bool { + tty_cursor_action(attention: Ambient, cap: tty_cap()) == Overwrite && + tty_cursor_action(attention: Notable, cap: tty_cap()) == Append && + tty_cursor_action(attention: Anomaly, cap: tty_cap()) == Append +} + +fn w_non_addressable_terminal_always_appends() -> Bool { + tty_cursor_action(attention: Ambient, cap: pipe_cap()) == Append && + tty_cursor_action(attention: Anomaly, cap: pipe_cap()) == Append +} + +fn w_refusal_never_repaints_away() -> Bool { + let refusal = ObservationEvent { + subject: tty_entry_subject(), + transition: Concluded { outcome: Refused { diagnostic: "FLOOR-BATCH-OVER-BUDGET" as NonEmptyStr } }, + wall: MeasuredValue { value: millisecond(count: 2472000) }, + rss: MeasuredUnavailable { cause: "not sampled" as NonEmptyStr } + } + let emission = tty_event_emission(event: refusal, basis: tty_basis(), cap: tty_cap()) + emission.line.attention == Anomaly && + emission.cursor == Append +} + +fn w_same_event_drives_both_surfaces() -> Bool { + let event = ObservationEvent { + subject: tty_entry_subject(), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: 230) }, + rss: MeasuredUnavailable { cause: "not sampled" as NonEmptyStr } + } + let emission = tty_event_emission(event: event, basis: tty_basis(), cap: tty_cap()) + string_contains(s: tty_render_emission(emission: emission, tier: Emoji), pattern: "in 230ms") +} + +fn w_group_line_names_the_current_entry() -> Bool { + let line = tty_group_line(active: tty_entry_subject(), k: 214, n: 602, spinner: Spinner0) + string_contains(s: line.text, pattern: "214 of 602") && + string_contains(s: line.text, pattern: "v2.compiler.normalized_tree") && + line.attention == Ambient +} + +fn w_blocked_line_carries_holder_and_bounded_remaining() -> Bool { + let bounded = tty_blocked_line(blocked: BlockedOn { + resource: MemoryBudget, + holders: [tty_entry_subject()], + remaining: RemainingBounded { by: millisecond(count: 80000) } + }) + string_contains(s: bounded.text, pattern: "1 holder(s)") && + string_contains(s: bounded.text, pattern: "80 seconds remaining") && + bounded.attention == Anomaly +} + +fn w_blocked_line_names_why_remaining_is_unknown() -> Bool { + let unknown = tty_blocked_line(blocked: BlockedOn { + resource: MemoryBudget, + holders: [tty_entry_subject()], + remaining: RemainingUnknown { cause: "governor exposes no reclaim bound" as NonEmptyStr } + }) + string_contains(s: unknown.text, pattern: "no estimate (governor exposes no reclaim bound)") && + !string_contains(s: unknown.text, pattern: "remaining") +} + +fn w_reward_animal_is_deterministic_and_degrades_to_a_word() -> Bool { + let emoji_clean = tty_final_line(shown_failures: 0, run_index: 3, tier: Emoji) + let emoji_again = tty_final_line(shown_failures: 0, run_index: 3, tier: Emoji) + let ascii_clean = tty_final_line(shown_failures: 0, run_index: 3, tier: Ascii) + emoji_clean.text == emoji_again.text && + string_contains(s: emoji_clean.text, pattern: "all clean") && + emoji_clean.attention == Ambient && + !string_contains(s: ascii_clean.text, pattern: "🦦") +} + +fn w_final_with_failures_names_them_and_is_anomaly() -> Bool { + let line = tty_final_line(shown_failures: 3, run_index: 3, tier: Emoji) + string_contains(s: line.text, pattern: "3 problems shown") && + line.attention == Anomaly +} + +test fn observation_tty_render_witnesses() -> Bool { + w_preamble_names_what_why_and_how_much() && + w_routine_repaints_in_place_but_anomaly_scrolls() && + w_non_addressable_terminal_always_appends() && + w_refusal_never_repaints_away() && + w_same_event_drives_both_surfaces() && + w_group_line_names_the_current_entry() && + w_blocked_line_carries_holder_and_bounded_remaining() && + w_blocked_line_names_why_remaining_is_unknown() && + w_reward_animal_is_deterministic_and_degrades_to_a_word() && + w_final_with_failures_names_them_and_is_anomaly() +} diff --git a/src/v1/stage0/src/std_measure.rs b/src/v1/stage0/src/std_measure.rs index 1f3ecc1971d..0f4f853bd79 100644 --- a/src/v1/stage0/src/std_measure.rs +++ b/src/v1/stage0/src/std_measure.rs @@ -99,6 +99,10 @@ pub fn kibi_factor() -> Nat { 1024 } +pub fn milliseconds_per_second() -> Nat { + 1000 +} + pub fn seconds_per_minute() -> Nat { 60 } From f42e0ba8a0dfc79a63cb325777eb1f1f1d19abc2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 18:47:46 +0000 Subject: [PATCH 11/39] P3: the emit-site census wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fourth and final phase of the atomic P0-P3 observation PR. gunbc.observation_emit_census is the census authority. Every place the floor emits a progress line is either a projection of the observation event stream or a counted frontier row with a reason and a dissolve-on — the same discipline the site lane uses for unthemed colours. EmitSiteDisposition is a closed sum, so a site cannot be half-classified, and there is no third arm for a site the census has not looked at: the roster's completeness is what the witness checks against the seed. The roster carries the structured-tag emit families that exist regardless of the CI rework: [floor-memory], [typecheck-attribution], [gantt], [governor], [measurement]. The named negative example the operator called out — the [floor-memory] raw byte dump — is a rostered frontier row, so the census already carries the very site it exists to kill, with its dissolve-on naming the P1 heartbeat projection that replaces it. Sequencing per the ruling and design section 6b: the CI two-tier rework rewrites the floor's emit sites, so the exhaustive per-print wall over the ~75 raw eprintlns in claim_executor is a declared frontier gated on this PR rebasing over that rework and re-censusing. Censusing sites about to be rewritten is the double-churn the operator ruled out. What lands now is the census model, the roster, and the executable hygiene witness — never a hidden zero: five families migrated-pending, the raw-print residue counted, and the witness holding the roster against the seed so it cannot rot into a lie. Executable, not inert: the witness reads the live seed and reds when a rostered marker has vanished (staleness — proven by a RED control) or when a frontier row lacks a real dissolve-on. The [floor-memory] shape is checked positively — still present, still classified frontier — so the census cannot quietly drop it. This completes P0-P3. Full suite green by execution: 31 model, 6 lockstep, 18 CI-renderer, 10 TTY-renderer, 6 census conjuncts, each with discriminating REDs proven by perturbation. Co-Authored-By: Claude Opus 4.8 (1M context) --- dag/gunbc/observation_emit_census.dag | 109 ++++++++++++++++++ .../observation_emit_census_witness_test.dag | 82 +++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 dag/gunbc/observation_emit_census.dag create mode 100644 dag/test/claim/observation_emit_census_witness_test.dag diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag new file mode 100644 index 00000000000..60edabddaf1 --- /dev/null +++ b/dag/gunbc/observation_emit_census.dag @@ -0,0 +1,109 @@ +module gunbc.observation_emit_census + +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import std.disposition { Disposition, Terminal, Scaffold, SingleAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." + +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. The named negative example the operator called out — the [floor-memory] raw byte dump — is a rostered frontier row here, so the census already carries the thing it exists to kill." + +type EmitSiteDisposition + = MigratedToObservation { via: NonEmptyStr } + | CountedFrontierSite { reason: NonEmptyStr, dissolve_on: NonEmptyStr } + +data emit_site_disposition_note: String = "A closed sum, so a site is exactly one of two things and never silently neither. MigratedToObservation names the projection that replaced the raw emit — the evidence the site now speaks the event vocabulary. CountedFrontierSite carries a reason it has not migrated yet and the trigger that will dissolve it, so the debt is countable and prioritizable rather than an open-ended intention. There is no third arm, which is the point: an emit site the census has not looked at cannot be represented here, so the roster's completeness is what the witness checks against the seed." + +type CensusedEmitSite { + marker: NonEmptyStr + source_file: NonEmptyStr + disposition: EmitSiteDisposition +} + +data floor_memory_site: CensusedEmitSite = CensusedEmitSite { + marker: "[floor-memory]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "the named negative example — a raw byte dump (current=16106717184) with no subject, sixty-plus context-free lines an hour, the exact shape the operator watched for ten minutes during the crawl window that priced this lane" as NonEmptyStr, + dissolve_on: "the observation CI heartbeat projection (gunbc.observation_ci_render.ci_heartbeat_line) replaces it — identity-first, human units, subject named — when the P1 renderer wiring lands after the CI two-tier rework PR" as NonEmptyStr + } +} + +data typecheck_attribution_site: CensusedEmitSite = CensusedEmitSite { + marker: "[typecheck-attribution]" as NonEmptyStr, + source_file: "src/v1/stage0/src/cli_run.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "per-module typecheck cost, emitted only at walk end — the receipt that caused the 55-minute mis-triage because it never wrote until the walk it was timing completed" as NonEmptyStr, + dissolve_on: "a Concluded event per module carrying its measured wall (the observation event stream is written as work concludes, not batched to walk end), rendered by ci_event_line — lands with the P1 wiring after the rework PR" as NonEmptyStr + } +} + +data gantt_site: CensusedEmitSite = CensusedEmitSite { + marker: "[gantt]" as NonEmptyStr, + source_file: "src/v1/stage0/src/phase_profile.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "compile phase boundaries (frontend/normalize/reconcile/emit begin/done) — already event-shaped, a Begin and a Concluded per phase, but emitted through a bespoke printer rather than the observation stream" as NonEmptyStr, + dissolve_on: "Begin/Concluded events on a phase-grain subject (PhaseSegment) rendered by the shared projection — the closest existing site to already being an event, migrates first after the rework PR" as NonEmptyStr + } +} + +data governor_site: CensusedEmitSite = CensusedEmitSite { + marker: "[governor]" as NonEmptyStr, + source_file: "src/v1/stage0/src/memory_governor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "AIMD admission holds and creep back-off — the governor state that the model's SchedulerHold and BlockedOn already mirror in lockstep, but the seed still prints its own HoldReason rather than emitting a BlockedOn event" as NonEmptyStr, + dissolve_on: "a BlockedOn event derived from the HoldReason the governor already computes (the lockstep coupling is proven; the emit side follows), rendered inline by ci/tty blocked-line — after the rework PR" as NonEmptyStr + } +} + +data measurement_site: CensusedEmitSite = CensusedEmitSite { + marker: "[measurement]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "peak-RSS and per-shard memory samples consumed by the placement divisor and compile-jobs derive — a MEASUREMENT feed with real downstream consumers, not a progress line, so its migration is subtler than the display sites" as NonEmptyStr, + dissolve_on: "re-census after the CI two-tier rework PR: the rework changes which measurements the placement axis consumes, so classifying this as projection-or-frontier before the rework would classify a site about to be rewritten" as NonEmptyStr + } +} + +data observation_emit_roster: List = [ + floor_memory_site, + typecheck_attribution_site, + gantt_site, + governor_site, + measurement_site +] + +data observation_emit_roster_completeness_disposition: Disposition = Scaffold { + dissolves_to: SingleAuthority, + bind: DeclarationRef { + module_path: "gunbc.observation_emit_census", + decl_name: "observation_emit_roster", + field: WholeDeclaration + } +} + +data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: five tag families migrated-pending, the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." + +fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { + match site.disposition { + MigratedToObservation { via: _ } => false + CountedFrontierSite { reason: _, dissolve_on: _ } => true + } +} + +fn emit_site_dissolve_on(site: CensusedEmitSite) -> String { + match site.disposition { + MigratedToObservation { via: v } => v + CountedFrontierSite { reason: _, dissolve_on: d } => d + } +} + +fn observation_emit_frontier_count() -> Nat { + fold(observation_emit_roster, init: 0, f: (acc, site) => + if emit_site_is_frontier(site: site) { acc + 1 } else { acc }) +} + +data observation_emit_census_disposition: Disposition = Terminal { + reason: "The census MODEL is Terminal — a closed classification of emit sites is the wall's authority, not an interim stand-in. What is a scaffold is the ROSTER's completeness (declared above), because the site list grows when the rework's post-rewrite sites are enumerated. The model that classifies them does not change; only the rows do." +} diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag new file mode 100644 index 00000000000..2288399e0e2 --- /dev/null +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -0,0 +1,82 @@ +module test.claim.observation_emit_census_witness_test + +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import gunbc.observation_emit_census { + CensusedEmitSite, + EmitSiteDisposition, + MigratedToObservation, + CountedFrontierSite, + observation_emit_roster, + observation_emit_frontier_count, + emit_site_is_frontier, + emit_site_dissolve_on, + floor_memory_site, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. The [floor-memory] negative example is checked positively — it must still be in the seed and still be classified as a frontier — so the census cannot quietly drop the very site it exists to kill." + +fn census_source(path: String) -> String { + let r = filesystem_read(path: path) + r.content +} + +fn census_marker_present(site: CensusedEmitSite) -> Bool { + string_contains(s: census_source(path: site.source_file), pattern: site.marker) +} + +fn w_every_rostered_marker_still_exists_in_the_seed() -> Bool { + fold(observation_emit_roster, init: true, f: (acc, site) => + acc && census_marker_present(site: site)) +} + +fn w_roster_staleness_check_discriminates() -> Bool { + let bogus = CensusedEmitSite { + marker: "[this-marker-does-not-exist-in-the-seed]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "control" as NonEmptyStr, + dissolve_on: "control" as NonEmptyStr + } + } + !census_marker_present(site: bogus) +} + +fn w_every_frontier_row_carries_a_real_dissolve_on() -> Bool { + fold(observation_emit_roster, init: true, f: (acc, site) => + if emit_site_is_frontier(site: site) { + acc && emit_site_dissolve_on(site: site) != "" + } else { + acc + }) +} + +fn w_the_named_negative_example_is_present_and_classified_frontier() -> Bool { + census_marker_present(site: floor_memory_site) && + emit_site_is_frontier(site: floor_memory_site) && + string_contains(s: floor_memory_site.marker, pattern: "floor-memory") +} + +fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { + observation_emit_frontier_count() == 5 && + count(observation_emit_roster) == 5 +} + +fn w_the_raw_byte_dump_shape_is_still_in_the_seed_to_be_killed() -> Bool { + let executor = census_source(path: "src/v1/stage0/src/bin/claim_executor.rs") + string_contains(s: executor, pattern: "[floor-memory] t=") && + string_contains(s: executor, pattern: "current=") +} + +test fn observation_emit_census_witnesses() -> Bool { + w_every_rostered_marker_still_exists_in_the_seed() && + w_roster_staleness_check_discriminates() && + w_every_frontier_row_carries_a_real_dissolve_on() && + w_the_named_negative_example_is_present_and_classified_frontier() && + w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && + w_the_raw_byte_dump_shape_is_still_in_the_seed_to_be_killed() +} From 8255133251925b37fc789b5354431d275d137649 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 18:50:45 +0000 Subject: [PATCH 12/39] Piece 3: name the clamp coefficient's measurement basis (operator addition, 2026-07-24) The 1000ms aggregate coefficient now records its basis in gunbc_ci_floor_batch_clamp_note: host class (srv arm64 self-hosted, capped) x the adaptive governor's realized worker width, denominated against the observed 0.58-0.70 s/witness (the 1344-1629s full-corpus fleet envelope over ~2316 witnesses). Naming the basis makes a future width or fleet change a deliberate re-sign of the constant, never a rediscovered fleet-wide red; the ~1.4-1.7x headroom over the observed top rate is exactly the >=~1.6x runaway the clamp catches, with sub-threshold creep owned by the gauntlet's per-cadence s/unit receipt. Co-Authored-By: Claude --- dag/gunbc/ci_spec.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 8b71a2b3ccc..2bbf2f19424 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -181,7 +181,7 @@ type FloorBatchClamp { per_unit_ms: Int } -data gunbc_ci_floor_batch_clamp_note: String = "DERIVED per-batch wall clamp (Piece 3, ci-two-tier-placement-redesign.md §9.8, operator 2026-07-24). Supersedes the hand-set gunbc_ci_floor_batch_wall_budget_seconds list (deleted; its two operator-signed raises 1320->1440->1680 are the static-era history kept in gunbc_ci_floor_batch_wall_budget_note): a scalar wall budget conflated workload size (affected-set selection is diff-proportional BY DESIGN, ~5x swing), host speed (±20% fleet envelope), and the quantity actually worth bounding (per-unit cost creep). The clamp re-denominates — per batch, clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms — computed by claim_executor from THIS authority plus the affected-set-selected unit count it alone knows (the schedule holds one opaque discovery runnable; the witness count is runtime, not schedule data). Rows are index-aligned to gunbc_ci_floor_batches (the length-match witness pins the alignment, mirroring the deleted list's discipline). The load-bearing row is the discovery witness batch (index 2): overhead 300s + 1000ms/witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, ~1.6x the ~25-min healthy wall) while every legitimate observed full-corpus wall (1344-1629s) passes, and a runaway reds proportionally instead of at the fixed 1680s that the two hand-raises had to keep chasing. Fixed-count gate batches carry per_unit_ms 0 (their count does not vary, so overhead IS the clamp) at their measured basis. Index 3 (wet corpora) stays a fixed overhead pending a wet-per-witness rate from the D2 probe — a declared calibration gap, not a hidden default. SIGNED CONSTANTS (operator, 2026-07-24): the witness aggregate coefficient 1000ms and the discovery overhead 300s; the per-WITNESS hard max is NOT redefined here — it stays the single fast-lane authority gunbc_ci_fast_lane_eval_budget_ms (5s). RAISE DISCIPLINE (carried from gunbc_ci_floor_batch_wall_budget_note): raising any overhead or rate requires an appended dated operator-signed line naming the run id and the enrollment that grew the batch; tightening may land by ordinary receipt note; unit counts need no signature (the schedule computes them). The clamp is interim mechanics — the structural wall is the complexity lens (§8, cost <= a + b*n asserted at compile time), and the clamp demotes to host-pathology backstop when that lens goes Blocking. The 55-min step cap stays the absolute backstop for the total floor wall; GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS lowers the COMPUTED clamp (min), never raises — the RED-control hook, never an escape hatch." +data gunbc_ci_floor_batch_clamp_note: String = "DERIVED per-batch wall clamp (Piece 3, ci-two-tier-placement-redesign.md §9.8, operator 2026-07-24). Supersedes the hand-set gunbc_ci_floor_batch_wall_budget_seconds list (deleted; its two operator-signed raises 1320->1440->1680 are the static-era history kept in gunbc_ci_floor_batch_wall_budget_note): a scalar wall budget conflated workload size (affected-set selection is diff-proportional BY DESIGN, ~5x swing), host speed (±20% fleet envelope), and the quantity actually worth bounding (per-unit cost creep). The clamp re-denominates — per batch, clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms — computed by claim_executor from THIS authority plus the affected-set-selected unit count it alone knows (the schedule holds one opaque discovery runnable; the witness count is runtime, not schedule data). Rows are index-aligned to gunbc_ci_floor_batches (the length-match witness pins the alignment, mirroring the deleted list's discipline). The load-bearing row is the discovery witness batch (index 2): overhead 300s + 1000ms/witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, ~1.6x the ~25-min healthy wall) while every legitimate observed full-corpus wall (1344-1629s) passes, and a runaway reds proportionally instead of at the fixed 1680s that the two hand-raises had to keep chasing. Fixed-count gate batches carry per_unit_ms 0 (their count does not vary, so overhead IS the clamp) at their measured basis. Index 3 (wet corpora) stays a fixed overhead pending a wet-per-witness rate from the D2 probe — a declared calibration gap, not a hidden default. SIGNED CONSTANTS (operator, 2026-07-24): the witness aggregate coefficient 1000ms and the discovery overhead 300s; the per-WITNESS hard max is NOT redefined here — it stays the single fast-lane authority gunbc_ci_fast_lane_eval_budget_ms (5s). BASIS OF THE 1000ms COEFFICIENT (operator, 2026-07-24): the aggregate coefficient is denominated against the observed 0.58-0.70 s/witness (the 1344-1629s full-corpus fleet envelope over ~2316 witnesses) on the srv fleet host class (arm64 self-hosted, capped runners) at the adaptive governor's realized worker width. Naming host-class-x-worker-width as the basis makes a future width or fleet change a DELIBERATE re-sign of this constant (s/witness re-denominates when the fleet or width moves), never a rediscovered fleet-wide red; the ~1.4-1.7x headroom the 1000ms average carries over the observed top rate IS the >=~1.6x runaway the clamp is sized to catch, and sub-threshold creep below that ratio is owned by the gauntlet's per-cadence s/unit receipt (not this clamp). RAISE DISCIPLINE (carried from gunbc_ci_floor_batch_wall_budget_note): raising any overhead or rate requires an appended dated operator-signed line naming the run id and the enrollment that grew the batch; tightening may land by ordinary receipt note; unit counts need no signature (the schedule computes them). The clamp is interim mechanics — the structural wall is the complexity lens (§8, cost <= a + b*n asserted at compile time), and the clamp demotes to host-pathology backstop when that lens goes Blocking. The 55-min step cap stays the absolute backstop for the total floor wall; GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS lowers the COMPUTED clamp (min), never raises — the RED-control hook, never an escape hatch." data gunbc_ci_floor_batch_clamp_params: List = [ FloorBatchClamp { overhead_seconds: 240, per_unit_ms: 0 }, From faf64eeb79570b735487602b2cf1410ea34958d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 19:20:58 +0000 Subject: [PATCH 13/39] =?UTF-8?q?Probe=20(=E2=91=A0/=E2=91=A1):=20emit=20a?= =?UTF-8?q?=20per-gate=20warm-cost=20TSV=20from=20the=20floor's=20existing?= =?UTF-8?q?=20timings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instruments claim_executor with write_gate_warm_cost_receipt — one row per gate/claim (eval wall + resolve + combined warm_ms) and a discovery row carrying the per-witness rate — derived from the ClaimResult timings the walk already records (operator ruling 2026-07-24: instrument the existing floor, no throwaway probe workflow). Written to target/floor-gate-warm-cost-receipt.tsv and mirrored to the log as [gate-warm-cost] rows so the placement probe lifts it from get_job_logs on a fleet run. This is the placement roster's measurement basis: a gate rides PrTier only if its measured warm cost is within the 5s fast-lane budget, else fail-closed to Gauntlet (v2.workflow.ci_placement). Every floor run now auto-emits it; run cold-then-warm on >=2 hosts and the roster records value + host basis. Verified green-by-execution locally (single-claim row); the discovery-row path verifies in the next full-corpus CI floor. Fail-closed on a write error, consistent with the other floor receipts; never a verdict term. Co-Authored-By: Claude --- src/v1/stage0/src/bin/claim_executor.rs | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 03515add047..233296ccf77 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1605,6 +1605,71 @@ fn write_batch_wall_receipt_at(base: &std::path::Path, batch_records: &[BatchRec true } +fn write_gate_warm_cost_receipt(batch_records: &[BatchRecord]) -> bool { + write_gate_warm_cost_receipt_at(std::path::Path::new("target"), batch_records) +} + +/// Per-gate warm-cost TSV (D2 placement probe, ci-two-tier-placement-redesign §9.1): one row per +/// gate/claim carrying its warm eval wall, resolve time, and combined warm cost — the PLACEMENT +/// ROSTER's measurement basis. A gate rides PrTier only if its measured warm cost is within the +/// 5s fast-lane budget (else fail-closed to Gauntlet — v2.workflow.ci_placement). Denominated PER +/// GATE (placement is per-gate), reusing the ClaimResult timings the floor already records +/// (operator ruling 2026-07-24: instrument the existing floor, do not add a throwaway probe +/// workflow). For a single-claim gate `wall_nanos` IS the eval wall (== thread-CPU on its one +/// thread); the discovery row carries the per-witness rate (serial-sum eval over the witness +/// count — the parallel batch wall is the batch-wall receipt's, not a per-witness figure). The +/// probe reads a WARM run's rows; run cold-then-warm on >=2 hosts and the roster records value + +/// host basis. Fail-closed on a write error (shares target/ with the gated receipts, so a write +/// failure here is the same disk fault that fails them); never a verdict term. +fn write_gate_warm_cost_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord]) -> bool { + let mut body = + String::from("gate\tbatch\teval_ms\tresolve_ms\twarm_ms\twitnesses\ts_per_witness_us\n"); + for rec in batch_records { + let n = rec.batch_index + 1; + for result in &rec.results { + if result.corpus_witnesses > 0 { + let eval_ms = result.corpus_eval_nanos / 1_000_000; + let resolve_ms = result.corpus_resolve_nanos / 1_000_000; + let warm_ms = (result.corpus_eval_nanos + result.corpus_resolve_nanos) / 1_000_000; + let per_witness_us = + result.corpus_eval_nanos / (result.corpus_witnesses as u128) / 1_000; + body.push_str(&format!( + "discovery\t{n}\t{eval_ms}\t{resolve_ms}\t{warm_ms}\t{}\t{per_witness_us}\n", + result.corpus_witnesses + )); + } else { + let eval_ms = result.wall_nanos / 1_000_000; + let resolve_ms = result.resolve_nanos / 1_000_000; + let warm_ms = (result.wall_nanos + result.resolve_nanos) / 1_000_000; + body.push_str(&format!( + "{}\t{n}\t{eval_ms}\t{resolve_ms}\t{warm_ms}\t0\t0\n", + result.function + )); + } + } + } + // Mirror the TSV into the log (prefixed, grep-collectable) so the placement probe can lift + // it from get_job_logs on a fleet run without an artifact-upload step — the file stays for + // future .dag consumers (Piece 1 roster fill). + for line in body.lines() { + eprintln!("[gate-warm-cost] {line}"); + } + let path = base.join("floor-gate-warm-cost-receipt.tsv"); + if let Err(e) = std::fs::create_dir_all(base).and_then(|_| std::fs::write(&path, &body)) { + eprintln!( + "claim_executor: failed to write gate warm-cost receipt {}: {e} — walk fails closed here", + path.display() + ); + return false; + } + eprintln!( + "[receipt] floor gate warm-cost: {} batch(es) (TSV receipt: {})", + batch_records.len(), + path.display() + ); + true +} + fn write_resolve_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord]) -> bool { let mut resolves_total: u64 = 0; let mut resolve_ms_total: u128 = 0; @@ -2057,6 +2122,7 @@ fn run_walk( emit_gantt(&batch_records, total_wall_nanos); let resolve_receipt_ok = write_resolve_receipt(&batch_records); let batch_wall_receipt_ok = write_batch_wall_receipt(&batch_records); + let gate_warm_cost_receipt_ok = write_gate_warm_cost_receipt(&batch_records); // Memo contexts absorb their ledger totals into the process accumulator on // Drop, so they must die before the materialization receipt is written. drop(walk_memo); @@ -2065,6 +2131,7 @@ fn run_walk( any_failed: any_failed || !resolve_receipt_ok || !batch_wall_receipt_ok + || !gate_warm_cost_receipt_ok || !materialization_receipt_ok, batches_run, } From 9eb01da3caa7660d08ffac5c5468d584a6dc1e17 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 24 Jul 2026 19:32:24 +0000 Subject: [PATCH 14/39] =?UTF-8?q?Flagship=20replay=20as=20a=20green=20witn?= =?UTF-8?q?ess=20=E2=80=94=20the=20before/after=20made=20executable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's "show me": proven-by-witness without a visible sample is the fluent-but-unseen trap. dag/test/claim/observation_crawl_replay_test.dag renders the captured crawl window of run 30044816605 through the P1 CI renderer as one assembled block and asserts it by execution: - names the module where the capture was silent ("still in witness discovery: entry 214 of 602, now typecheck v2.compiler.normalized_tree", surfaced at T, the memory-reclaim cause at 2T) - uses human units, never the raw byte dump (15.0 GiB, not 16107200512) - ends in a named refusal summary, not a silent 55-minute timeout observation_crawl_after_block() is the exact function the PR body's after-sample is produced from, so the pasted before/after is a projection of a green run rather than prose. Every input number is read off the real log. The earlier gunbc/observation_crawl_demo.dag (a run-entry that returned a String and so errored on the ProcessExit contract) is replaced by this witness — a green check is worth more than a run-entry that prints then fails. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../claim/observation_crawl_replay_test.dag | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 dag/test/claim/observation_crawl_replay_test.dag diff --git a/dag/test/claim/observation_crawl_replay_test.dag b/dag/test/claim/observation_crawl_replay_test.dag new file mode 100644 index 00000000000..c12fdd07f3f --- /dev/null +++ b/dag/test/claim/observation_crawl_replay_test.dag @@ -0,0 +1,133 @@ +module test.claim.observation_crawl_replay_test + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { String, NonEmptyStr, Bool, List } +import std.measure { millisecond, byte_size, basis_point } +import std.symbols { Emoji } +import std.observation { + ObservationSubject, + RunSegment, + BatchSegment, + EntrySegment, + ModuleSegment, + PhaseSegment, + MeasuredValue, + MeasuredUnavailable, + ObservationEvent, + Concluded, + Done, + AttentionBasis, + observation_dwell_threshold, + PsiPressure, +} +import gunbc.observation_ci_render { + RenderedLine, + ci_render_line, + ci_event_line, + ci_heartbeat_line, + ci_escalation_line, + ci_escalation_blocked_line, + ci_refusal_restate_line, + ci_run_summary_line, + ci_group_open, + ci_group_close, + ci_collapsed_summary, + HeartbeatSample, + FloorRunSummary, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data witness_note: String = "End-to-end replay of the captured crawl window of run 30044816605 through the P1 CI renderer, asserting the WHOLE assembled block by execution — the flagship's before/after made a green witness rather than a hand-typed sample. Every input number is read off that run's log: the t=33m heartbeat carried current=16107200512 swap=34359738368 psi_some_avg10=9.01 and named no subject, and the module the process sat inside for 606984ms, v2.compiler.normalized_tree, was disclosed only at walk end. This witness renders those same facts and asserts the block now NAMES the activity where the capture was silent. observation_crawl_after_block is also the function the PR body's after-sample is produced from, so the pasted sample is a projection of a green execution, not prose." + +fn crawl_entry() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: "30044816605" as NonEmptyStr }, + BatchSegment { index: 3, label: "witness discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/virtual_media_unification_witness_test.dag" as NonEmptyStr } + ] + } +} + +fn crawl_leaf() -> ObservationSubject { + ObservationSubject { + segments: [ + RunSegment { id: "30044816605" as NonEmptyStr }, + BatchSegment { index: 3, label: "witness discovery" as NonEmptyStr }, + EntrySegment { source_path: "dag/test/claim/virtual_media_unification_witness_test.dag" as NonEmptyStr }, + ModuleSegment { module_path: "v2.compiler.normalized_tree" as NonEmptyStr }, + PhaseSegment { name: "typecheck" as NonEmptyStr } + ] + } +} + +fn crawl_basis() -> AttentionBasis { + AttentionBasis { average: millisecond(count: 1000), maximum: millisecond(count: 5000) } +} + +fn crawl_heartbeat_at(minute: Int, rss: Int, swap: Int, psi: Int) -> RenderedLine { + ci_heartbeat_line(sample: HeartbeatSample { + elapsed: millisecond(count: minute * 60000), + active: crawl_leaf(), + entry_index: 214, + entry_total: 602, + rss: MeasuredValue { value: byte_size(rss) }, + swap: MeasuredValue { value: byte_size(swap) }, + pressure: MeasuredValue { value: basis_point(count: psi) } + }) +} + +fn observation_crawl_after_block() -> String { + let lines = [ + ci_group_open(title: "witness discovery — batch 3 (602 entries)"), + ci_render_line(line: ci_event_line(event: ObservationEvent { + subject: crawl_entry(), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: 230) }, + rss: MeasuredUnavailable { cause: "not sampled per entry" as NonEmptyStr } + }, basis: crawl_basis()), tier: Emoji), + ci_collapsed_summary(green_count: 213), + ci_render_line(line: crawl_heartbeat_at(minute: 29, rss: 16106717184, swap: 34359738368, psi: 533), tier: Emoji), + ci_render_line(line: crawl_heartbeat_at(minute: 33, rss: 16107200512, swap: 34359738368, psi: 901), tier: Emoji), + ci_render_line(line: ci_escalation_line(quiet_subject: crawl_entry(), active_leaf: crawl_leaf(), quiet: millisecond(count: 120000), t: observation_dwell_threshold), tier: Emoji), + ci_render_line(line: ci_escalation_blocked_line(quiet_subject: crawl_entry(), active_leaf: crawl_leaf(), quiet: millisecond(count: 240000), t: observation_dwell_threshold, hold: PsiPressure { avg10: basis_point(count: 901) }), tier: Emoji), + ci_render_line(line: ci_refusal_restate_line(subject: crawl_entry(), diagnostic: "FLOOR-BATCH-OVER-BUDGET wall=41m12s budget=22m" as NonEmptyStr), tier: Emoji), + ci_group_close(), + ci_render_line(line: ci_run_summary_line(summary: FloorRunSummary { + entries_complete: 213, + entries_total: 602, + refusals: 1, + failures: 0, + wall: millisecond(count: 2472000) + }), tier: Emoji) + ] + join(lines, "\n") +} + +fn w_replay_names_the_module_where_the_capture_was_silent() -> Bool { + let block = observation_crawl_after_block() + string_contains(s: block, pattern: "still in witness discovery: entry 214 of 602, now typecheck v2.compiler.normalized_tree") && + string_contains(s: block, pattern: "quiet for 2 minutes — still working") && + string_contains(s: block, pattern: "quiet for 4 minutes — blocked on memory reclaim (pressure 9.0%)") +} + +fn w_replay_uses_human_units_not_raw_bytes() -> Bool { + let block = observation_crawl_after_block() + string_contains(s: block, pattern: "memory 15.0 GiB, swap 32.0 GiB, pressure 9.0%") && + !string_contains(s: block, pattern: "16107200512") && + !string_contains(s: block, pattern: "current=") +} + +fn w_replay_ends_in_a_refusal_summary_not_a_silent_timeout() -> Bool { + let block = observation_crawl_after_block() + string_contains(s: block, pattern: "213 of 602 entries finished") && + string_contains(s: block, pattern: "1 refused and 0 failed") && + string_contains(s: block, pattern: "FLOOR-BATCH-OVER-BUDGET") +} + +test fn observation_crawl_replay_witnesses() -> Bool { + w_replay_names_the_module_where_the_capture_was_silent() && + w_replay_uses_human_units_not_raw_bytes() && + w_replay_ends_in_a_refusal_summary_not_a_silent_timeout() +} From 70e644758e9f1b1efb51a21d10a06f29a4668588 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 21:20:15 +0000 Subject: [PATCH 15/39] =?UTF-8?q?Wiring=20flip=20(1/n):=20install=20the=20?= =?UTF-8?q?output=20policy=20BEFORE=20the=20naming=20walk=20=E2=80=94=20ki?= =?UTF-8?q?ll=20the=20[file]=20firehose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The floor's [file] read firehose (2265 lines / ~99% of the log, measured by execution) was the pre-plan naming-hygiene walk reading the whole source tree at the OutputDecision Full default, because install_output_policy ran AFTER the walk (claim_executor.rs order). gunbc.output_policy already models Instrumentation => Suppressed at Normal (CI's default verbosity) and ShellTrace => Condensed — the walk just never saw the policy. Fix: install the policy (and group syntax) FIRST, before the naming walk and every subsequent corpus read, so all host-effect traces are funnelled per the .dag authority. Verified by execution on the minimal smoke plan: [file] read 2265 -> 0, total log 2613 -> 24 lines, claim still PASS (exit 0). The Ambient semantics hold — the policy's divergence rule (ExpectedOutcome/ObservedOutcome) still expands a captured stream on failure, so a red effect is never silenced; only the green firehose is. This is the highest-leverage lever of the observation-emit census flip (the echo class the census targets). Follow-on commits route the display families ([floor-memory], [gantt], [governor], [measurement], [t+..]) through the observation stream as Ambient projections (the ✅/🕐/🚫 format matching #7168's "after" block). Co-Authored-By: Claude --- src/v1/stage0/src/bin/claim_executor.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 233296ccf77..ed62f020732 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -2453,6 +2453,21 @@ fn run() -> Result { ); }; + // Install the host-effect trace policy from the .dag authority FIRST — before the + // naming-hygiene walk below and every subsequent corpus read — so `[file] read` / + // `[rest]` / `[hermetic:mock]` etc. are funnelled per `gunbc.output_policy` + // (Instrumentation is Suppressed at Normal, the CI default) instead of flooding the + // floor log. Installing AFTER the walk (the prior order) left the walk's whole-tree + // read at the `Full` default — ~2.3k `[file] read` lines, the firehose the + // observation-emit census (`gunbc.observation_emit_census`) targets. The walk still + // runs before plan evaluation, so a naming violation stays the cheapest failure. + v1_compiler::cli_run::install_output_policy(&source_roots); + // Install the per-target group-marker syntax (GitHub Actions `::group::` vs a + // plain-terminal header) from the .dag authority, so the parallel walk folds each + // batch's host-effect traces into a collapsible group. + v1_compiler::cli_run::install_group_syntax(&source_roots); + phase_mark("output policy + group syntax installed"); + // Under the opt-in inversion the plan's DiscoveryBatches carry explicit entries // only (or are absent entirely on an empty roster), and the explicit-only path // skips the tree-walk naming hygiene (`test fn` outside `*_test.dag`, `__` @@ -2472,16 +2487,6 @@ fn run() -> Result { } phase_mark("naming-hygiene walk complete"); - // Install the host-effect trace policy from the .dag authority once, before - // discovery threads spawn, so `[file] read` / `[rest]` / `[hermetic:mock]` etc. - // are funnelled per `gunbc.output_policy` instead of flooding the floor log. - v1_compiler::cli_run::install_output_policy(&source_roots); - // Install the per-target group-marker syntax (GitHub Actions `::group::` vs a - // plain-terminal header) from the .dag authority, so the parallel walk folds each - // batch's host-effect traces into a collapsible group. - v1_compiler::cli_run::install_group_syntax(&source_roots); - phase_mark("output policy + group syntax installed"); - if perturb_check { return run_perturb_check(&source_roots, &plan_entry, &plan_function); } From ba28f216917fdbf09c9e43e90e74b513d8a867db Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 22:07:44 +0000 Subject: [PATCH 16/39] Wiring flip (2/n): render floor phase marks through the observation authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 of the flip (format), after step 1 (the [file] firehose, volume). The prelude phase marks are the visible display class in the short regen job's log; they now render through the single-authority observation renderer instead of a raw [t+…] byte string the seed would fork the format into. before: claim_executor: [t+86.1s] naming-hygiene walk complete after: ✅ naming-hygiene walk done in 48 seconds - New seed→.dag boundary gunbc.observation_seed_render: primitive args in, a rendered line out — exactly as cli_run.install_output_policy calls output_policy.resolve_channel_policy. A phase concluding is modelled as a Concluded event on a PhaseSegment subject, projected by ci_event_line ∘ ci_render_line, so the FORMAT stays single-authority in gunbc.observation_ci_render and the seed constructs no format of its own. - The raw [t+{:.1}s] eprintln is DELETED, not suppressed (grep-clean for the print). §5: on a renderer-unreachable failure the arm names the degradation loudly and never reproduces the old marker. - Per-phase walls (delta since the last mark), not a running t+, so the log itemizes which prelude phase is slow — the step toward the per-phase receipt keys the ci_spec prelude-coverage-hole follow-up (row a) calls for. - Green by execution: phase_mark_renders_through_the_observation_render_authority resolves the adapter through a real interpreter and asserts human units + the completed glyph + NO [t+ marker (the discriminating RED). The seed→.dag resolve is memoized, so the renderer resolves once and later marks are cache hits. Rust-called-.dag-unimported has precedent (output_policy.dag). Next in the series: the rostered census families. floor-memory (the flagship byte dump) needs its subject feed plumbed first so it renders honestly (entry X of Y, never a fabricated 0 of 0), then gantt/governor/typecheck-attribution, each flipping its census row (CountedFrontierSite → MigratedToObservation) with the witness restructured to assert the raw marker is gone — the "witness fixes" step of flip → witness fixes → roster. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/observation_seed_render.dag | 51 +++++++++ src/v1/stage0/src/bin/claim_executor.rs | 140 +++++++++++++++++++++--- 2 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 dag/gunbc/observation_seed_render.dag diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag new file mode 100644 index 00000000000..267dbdbf38e --- /dev/null +++ b/dag/gunbc/observation_seed_render.dag @@ -0,0 +1,51 @@ +module gunbc.observation_seed_render + +import std.types { String, NonEmptyStr, Bool } +import std.nat { Nat } +import std.symbols { Tier, Emoji, Unicode } +import std.measure { millisecond } +import std.disposition { Disposition, Terminal } +import std.observation { + ObservationSubject, + PhaseSegment, + MeasuredValue, + MeasuredUnavailable, + ObservationEvent, + Concluded, + Done, + AttentionBasis, +} +import gunbc.observation_ci_render { ci_event_line, ci_render_line } + +data observation_seed_render_note: String = "The seed to .dag render boundary. The v1 seed (claim_executor and its siblings) emits progress lines, but the FORMAT is a single authority in gunbc.observation_ci_render — a raw eprintln in the seed forks that format the way a hand-rolled duration string forks std.measure. This module is the boundary the seed calls across, exactly as cli_run.install_output_policy calls output_policy.resolve_channel_policy: primitive arguments in, a rendered line out, no format logic on the seed side. It constructs the ObservationEvent the seed's occurrence corresponds to (a phase concluding is a Concluded event on a PhaseSegment subject) and projects it through the existing renderer, so the seed cannot drift from the model. It holds no telemetry source and computes no facts of its own; every field it fills came in as an argument the seed already had (observation law 5)." + +data observation_seed_render_basis_note: String = "The attention basis governs PLACEMENT (ambient collapses, an anomaly surfaces), not the rendered string — ci_render_line reads only the glyph and text. It is supplied here, not omitted, so a future placement consumer reads a real level rather than a fabricated one, and it is grounded in the clamp overhead the floor already models (the non-per-unit floor time gunbc.ci_spec budgets as gunbc_ci_floor_batch_clamp_params overhead_seconds) rather than a threshold invented in a formatting helper: a prelude phase completing inside the whole overhead budget is routine, so it reads Ambient. The seed passes that overhead in as overhead_ms — this module does not reach for it, keeping the module a pure projection." + +fn seed_tier(emoji: Bool) -> Tier { + if emoji { Emoji } else { Unicode } +} + +fn seed_phase_basis(overhead_ms: Nat) -> AttentionBasis { + AttentionBasis { average: millisecond(count: overhead_ms), maximum: millisecond(count: overhead_ms) } +} + +fn seed_phase_subject(phase: NonEmptyStr) -> ObservationSubject { + ObservationSubject { segments: [ PhaseSegment { name: phase } ] } +} + +fn phase_concluded_line(phase: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_phase_subject(phase: phase), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: elapsed_ms) }, + rss: MeasuredUnavailable { cause: "not sampled at a phase boundary" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + +data observation_seed_render_disposition: Disposition = Terminal { + reason: "The seed to .dag render boundary is Terminal, not a scaffold: a boundary the seed calls across to keep the format single-authority is the mechanism, not an interim stand-in. What dissolves is each raw eprintln the seed replaces with a call here — counted by gunbc.observation_emit_census — and, further out, the seed itself as v2 realizes the witnesses natively; neither changes what this module models, which is that a seed occurrence is an observation event projected by the one renderer." +} diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index ed62f020732..1c0aef6471a 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1268,6 +1268,59 @@ struct WalkOutcome { batches_run: usize, } +/// Render a floor phase-completion line through the single-authority observation +/// renderer (`gunbc.observation_ci_render`, via the seed boundary +/// `gunbc.observation_seed_render`) instead of a raw `[t+…]` byte string in the seed +/// — the format lives in `.dag`, this only transports primitives across the boundary, +/// exactly as `cli_run::install_output_policy` calls `output_policy.resolve_channel_policy`. +/// The seed occurrence "a floor phase concluded in `elapsed_ms`" is modelled as a +/// `Concluded` event on a `PhaseSegment` subject and projected by +/// `ci_event_line ∘ ci_render_line`. Resolve is memoized in the process resolve store, +/// so the render module is resolved once and every later mark is a cache hit + a cheap +/// eval. Returns `None` only if the renderer cannot be resolved/evaluated; the caller +/// degrades loudly and never reproduces the old marker (§5: a failure arm refuses, it +/// does not widen). The entry is located under whichever source root holds it as an +/// absolute path, so resolution does not depend on the process CWD (production runs +/// from the repo root; `cargo test` runs from the crate dir). +fn render_phase_concluded_line( + source_roots: &[String], + phase: &str, + elapsed_ms: u64, + overhead_ms: u64, + emoji: bool, +) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "phase_concluded_line", + &[ + (Some("phase".to_string()), Value::Str(phase.to_string())), + ( + Some("elapsed_ms".to_string()), + Value::Int(elapsed_ms as i64), + ), + ( + Some("overhead_ms".to_string()), + Value::Int(overhead_ms as i64), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } +} + fn peak_rss_bytes() -> Option { let status = std::fs::read_to_string("/proc/self/status").ok()?; let line = status.lines().find(|l| l.starts_with("VmHWM"))?; @@ -2441,16 +2494,45 @@ fn run() -> Result { } }; - // Coarse wall-clock phase marks: the pre-walk phases (hygiene walk, policy - // install, plan resolve, plan eval, width eval) are interpreter-heavy and used - // to be SILENT — a 30-minute prelude looked identical to a hang. Every phase - // now stamps a line so the floor log itemizes its own time. + // Coarse phase marks for the pre-walk prelude (hygiene walk, policy install, + // plan resolve/eval, governor arm) — interpreter-heavy phases that used to be + // SILENT, so a 30-minute prelude looked identical to a hang. These now render + // through the single-authority observation renderer (`gunbc.observation_ci_render`, + // via the `gunbc.observation_seed_render` boundary) as `✅ done in ` instead of a raw `[t+…]` byte string the seed would fork the format + // into. Each mark carries its OWN wall (delta since the last mark), not a running + // `t+`, so the log itemizes which prelude phase is slow — the step toward the + // per-phase receipt keys the ci_spec prelude-coverage-hole follow-up calls for. let floor_started = Instant::now(); + let phase_last = std::cell::Cell::new(floor_started); + // Emoji glyphs under GitHub Actions, Unicode on a plain terminal (the same medium + // split `install_group_syntax` makes for `::group::` markers). + let phase_glyph_emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); + // Placement basis only (see `gunbc.observation_seed_render`): the clamp overhead + // `gunbc.ci_spec` budgets for non-per-unit floor time, so a prelude phase completing + // within it reads Ambient. Coarse + placement-only — `ci_render_line` reads only the + // glyph and text — and dissolves when the stream driver reads the basis from ci_spec. + const PHASE_BASIS_OVERHEAD_MS: u64 = 300_000; + let phase_mark_roots = source_roots.clone(); let phase_mark = |label: &str| { - eprintln!( - "claim_executor: [t+{:.1}s] {label}", - floor_started.elapsed().as_secs_f64() - ); + let now = Instant::now(); + let delta_ms = now.saturating_duration_since(phase_last.get()).as_millis() as u64; + phase_last.set(now); + match render_phase_concluded_line( + &phase_mark_roots, + label, + delta_ms, + PHASE_BASIS_OVERHEAD_MS, + phase_glyph_emoji, + ) { + Some(line) => eprintln!("{line}"), + // §5: refuse to fabricate the pretty format when the renderer is unreachable, + // and never reproduce the deleted `[t+…]` marker — name the degradation loudly. + None => eprintln!( + "claim_executor: phase {label} (+{:.1}s) [observation renderer unavailable]", + (delta_ms as f64) / 1000.0 + ), + } }; // Install the host-effect trace policy from the .dag authority FIRST — before the @@ -2466,7 +2548,7 @@ fn run() -> Result { // plain-terminal header) from the .dag authority, so the parallel walk folds each // batch's host-effect traces into a collapsible group. v1_compiler::cli_run::install_group_syntax(&source_roots); - phase_mark("output policy + group syntax installed"); + phase_mark("output-policy + group-syntax install"); // Under the opt-in inversion the plan's DiscoveryBatches carry explicit entries // only (or are absent entirely on an empty roster), and the explicit-only path @@ -2485,7 +2567,7 @@ fn run() -> Result { return Err(ExitCode::from(1)); } } - phase_mark("naming-hygiene walk complete"); + phase_mark("naming-hygiene walk"); if perturb_check { return run_perturb_check(&source_roots, &plan_entry, &plan_function); @@ -2501,7 +2583,7 @@ fn run() -> Result { return Err(ExitCode::from(1)); } }; - phase_mark("plan entry resolved"); + phase_mark("plan resolve"); let plan_ctx = make_eval_context(&plan_graph, plan_indices.clone(), ExecutionMode::Hermetic); let batches = match eval_plan_in_ctx(&plan_ctx, &plan_entry, &plan_function) { @@ -2588,7 +2670,7 @@ fn run() -> Result { } }; drop(plan_ctx); - phase_mark("plan evaluated"); + phase_mark("plan eval"); eprintln!( "claim_executor: [{}] executor plan = {} batch(es) from {}::{}", @@ -2618,7 +2700,7 @@ fn run() -> Result { return Err(ExitCode::from(1)); } } - phase_mark("memory governor armed; starting batch walk"); + phase_mark("memory-governor arm"); spawn_floor_memory_heartbeat(); // Plans whose schedule carries the compile-clean gate node: the gate only CONSUMES the @@ -2894,6 +2976,38 @@ mod tests { ); } + // The seed→.dag render boundary by execution: `render_phase_concluded_line` + // resolves `gunbc.observation_seed_render` and projects a floor phase mark through + // the single-authority renderer, so the seed speaks the observation vocabulary + // rather than a raw `[t+…]` byte string. Discriminating RED: a helper that fell + // back to the raw marker, dropped the phase, or forked the duration format fails + // one of the three asserts below (human units, completed glyph, no old marker). + #[test] + fn phase_mark_renders_through_the_observation_render_authority() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + // Emoji tier (the CI target): 48s concludes as `✅ naming-hygiene walk done in + // 48 seconds`. overhead_ms is the placement basis only (the 300s clamp overhead). + let line = + render_phase_concluded_line(&roots, "naming-hygiene walk", 48_000, 300_000, true) + .expect("observation_seed_render must resolve and render a phase line"); + assert!( + line.contains("naming-hygiene walk") && line.contains("done in 48 seconds"), + "phase line must name the phase in human units: {line:?}" + ); + assert!( + line.starts_with('✅'), + "a Done outcome concludes with the completed glyph at the Emoji tier: {line:?}" + ); + assert!( + !line.contains("[t+"), + "the projection must not carry the deleted raw phase marker: {line:?}" + ); + } + // The materialization-receipt chain by execution: a real entry resolves, a // claim evaluates on its InterpContext, and the ctx Drop absorbs ledger // totals into the process accumulator. The env latch is process-global and From d05c0784083579607883787b13c40d0866efc5a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 22:42:49 +0000 Subject: [PATCH 17/39] Witness fix: roster the observation stack's one wildcard site (non_fold_residue) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The progress-and-observation merge (#7168) added observation_ci_render.dag, whose ci_hold_cause_text names the two memory-pressure SchedulerHold variants specifically and gives the other five a generic cause via a top-level wildcard arm — a non_fold_residue site. It landed unrostered because per-PR affected-set selection predict-skips the corpus-read nfr witness (the masking class the roster's dated rows document), so it reds only on a cold whole-corpus sweep (falsifier / merge-to-main), not on the selected PR floor. That is the census wall doing its job on its own author. Roster it (gunbc.non_fold_residue, one FrontierRow, reason + dissolution trigger toward a total match), matching the established masking-class fix and preserving the observation author's design. Green by execution: observation_hold_cause_wildcard_is_rostered asserts the live roster now carries dag/gunbc/observation_ci_render.dag::ci_hold_cause_text via the same host reader the corpus scan uses — reds if the row's key drifts from the scan's {rel}::{fn} key or the hand edit malformed the 126-row list. design_register_lift_parity (the other cold-red thought to be surfaced by the merge) is NOT touched: this branch's gunbc.site.* inputs are byte-identical to main and recent main-push runs are green cold, so it is green here too — not attributable to this PR. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/non_fold_residue.dag | 7 +++++++ src/v1/stage0/src/cli_run.rs | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/dag/gunbc/non_fold_residue.dag b/dag/gunbc/non_fold_residue.dag index c9e75ee67d1..6f54b0ca89b 100644 --- a/dag/gunbc/non_fold_residue.dag +++ b/dag/gunbc/non_fold_residue.dag @@ -50,6 +50,8 @@ data nfr_reason_native_agreement_octet: String = "RuntimeValue primitive-discrim data nfr_dissolve_native_agreement_octet: String = "RuntimeValue model↔realization grounds each primitive variant and the wildcard arm migrates to a total match — or the receipt formatter derives from inhabitance and the row deletes" +data nfr_reason_observation_hold_cause: String = "the CI-log renderer names the two memory-pressure holds (PsiPressure, CurrentHighWater — the crawl-window narrative) with a specific cause and gives the other five SchedulerHold variants a generic 'blocked on scheduler admission'; landed with the progress-and-observation stack (session/sleek-ibex-634, #7168) and surfaced by the whole-corpus nfr receipt after per-PR affected-set selection predict-skipped the corpus-read nfr witness at merge time (the masking class); declared so the ratchet re-arms" + data non_fold_residue_frontier: List = [ FrontierRow { unit: "dag/gunbc/local_tidy_spec.dag::local_tidy_path_matches_trigger", @@ -680,6 +682,11 @@ data non_fold_residue_frontier: List = [ unit: "src/v2/workflow/ci_floor_plan.dag::spec_enrolls_gate", reason: nfr_reason_ci_floor_gate_membership, dissolve_on: nfr_dissolve_gate_membership + }, + FrontierRow { + unit: "dag/gunbc/observation_ci_render.dag::ci_hold_cause_text", + reason: nfr_reason_observation_hold_cause, + dissolve_on: nfr_dissolve_wildcard_total } ] diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 66abbb9fec3..047878a578b 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -23420,6 +23420,26 @@ pub fn non_fold_residue_synthetic_unrostered_red_holds() -> bool { sites.contains(&site.to_string()) && !non_fold_residue_site_is_rostered(site) } +#[cfg(test)] +mod nfr_observation_roster_test { + use super::non_fold_residue_site_is_rostered; + + // Green-by-execution for the one observation-stack wildcard site + // (ci_hold_cause_text over SchedulerHold, merged via #7168): the roster now + // carries it, so the corpus nfr witness's unrostered count no longer counts it. + // Reds if the roster row's key drifts from the scan's `{rel}::{fn}` key, or if + // the hand edit malformed the frontier list (the reader panics on a bad list). + #[test] + fn observation_hold_cause_wildcard_is_rostered() { + assert!( + non_fold_residue_site_is_rostered( + "dag/gunbc/observation_ci_render.dag::ci_hold_cause_text" + ), + "the observation ci_hold_cause_text wildcard must be rostered after the fix" + ); + } +} + // ── Non-fold-residue census (DESIGN §6) ────────────────────────────────────────────────────────── // // Audits the corpus for `match` expressions whose scrutinee is a function parameter with a declared From d658526b396a96e55adf839270499c08fbd2ba92 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 23:05:48 +0000 Subject: [PATCH 18/39] Wiring flip (4a/n): the floor-memory heartbeat's seed oracle + golden strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for migrating the [floor-memory] byte dump to the observation heartbeat. Adds gunbc.observation_seed_render.seed_heartbeat_line: the seed→.dag boundary that takes the primitives the heartbeat thread has (elapsed, batch label, entry position, memory vitals) and projects them through the one renderer (ci_heartbeat_line ∘ ci_render_line) — identity first, human units, no raw byte dump. The subject is batch-grain by construction: the floor walks entries in parallel, so there is no single active module to name, and the primitive interface carries none — never a fabricated per-module "now typecheck X". Green by execution: seed_heartbeat_line_renders_identity_first_in_human_units pins the exact bytes for two samples through the real interpreter: 🕐 33 minutes in — still in witness discovery: entry 214 of 602. memory 15.0 GiB, swap 32.0 GiB, pressure 9.0% 🕐 500ms in — still in self-host fixed-point: entry 0 of 2. memory unreadable (cgroup field unreadable), swap 0.0 GiB, pressure unreadable (cgroup field unreadable) The first is the captured crawl window re-rendered from the seed's own vitals (raw byte value absent); the second proves an unreadable cgroup field names its cause, never a fabricated zero (observation law 2 / §5). These golden strings are the oracle the Rust mirror is proven byte-equal to in 4b. Why a Rust mirror next, not an interpreter call: the heartbeat runs on a detached liveness thread in a memory-constrained context — resolving the renderer there would build a duplicate module index, consuming the very memory it watches (§2), and the thread exists to stay alive when the main interpreter is busy. 4b adds that mirror (proven == this oracle), plumbs the subject feed, wires it, deletes the byte dump, and flips the census row. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/observation_seed_render.dag | 37 +++++++- src/v1/stage0/src/bin/claim_executor.rs | 116 ++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 267dbdbf38e..4b9aa0e932d 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -3,10 +3,11 @@ module gunbc.observation_seed_render import std.types { String, NonEmptyStr, Bool } import std.nat { Nat } import std.symbols { Tier, Emoji, Unicode } -import std.measure { millisecond } +import std.measure { millisecond, byte_size, basis_point } import std.disposition { Disposition, Terminal } import std.observation { ObservationSubject, + BatchSegment, PhaseSegment, MeasuredValue, MeasuredUnavailable, @@ -15,7 +16,7 @@ import std.observation { Done, AttentionBasis, } -import gunbc.observation_ci_render { ci_event_line, ci_render_line } +import gunbc.observation_ci_render { ci_event_line, ci_render_line, ci_heartbeat_line, HeartbeatSample } data observation_seed_render_note: String = "The seed to .dag render boundary. The v1 seed (claim_executor and its siblings) emits progress lines, but the FORMAT is a single authority in gunbc.observation_ci_render — a raw eprintln in the seed forks that format the way a hand-rolled duration string forks std.measure. This module is the boundary the seed calls across, exactly as cli_run.install_output_policy calls output_policy.resolve_channel_policy: primitive arguments in, a rendered line out, no format logic on the seed side. It constructs the ObservationEvent the seed's occurrence corresponds to (a phase concluding is a Concluded event on a PhaseSegment subject) and projects it through the existing renderer, so the seed cannot drift from the model. It holds no telemetry source and computes no facts of its own; every field it fills came in as an argument the seed already had (observation law 5)." @@ -46,6 +47,38 @@ fn phase_concluded_line(phase: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, e ) } +data seed_heartbeat_unreadable_cause: NonEmptyStr = "cgroup field unreadable" as NonEmptyStr + +data seed_heartbeat_note: String = "The floor-memory heartbeat's projection. The heartbeat runs on a detached liveness thread in a memory-constrained context, so the seed renders it through a Rust MIRROR of ci_heartbeat_line rather than calling the interpreter there — an interpreter render on that thread would build a duplicate module index, consuming the very memory the heartbeat watches (DESIGN section 2), and the thread's whole point is to stay alive when the main interpreter is busy. This fn is the ORACLE that keeps the mirror honest: the seed's Rust test runs this (through the interpreter) and its mirror on the same samples and asserts byte-equality, so the format authority stays here in the one renderer and any drift reds. The subject is batch-grain by construction — the floor walks entries in parallel, so there is no single active module to name; the primitive interface carries no module or phase, which is why the mirror is a COMPLETE reflection of this fn over the seed's real input space, never a fabricated per-module detail (observation law 2: identity, never invented)." + +fn seed_heartbeat_line( + elapsed_ms: Nat, + batch_index: Nat, + batch_label: NonEmptyStr, + entry_index: Nat, + entry_total: Nat, + rss_bytes: Nat, + rss_available: Bool, + swap_bytes: Nat, + swap_available: Bool, + pressure_bp: Nat, + pressure_available: Bool, + emoji: Bool +) -> String { + ci_render_line( + line: ci_heartbeat_line(sample: HeartbeatSample { + elapsed: millisecond(count: elapsed_ms), + active: ObservationSubject { segments: [ BatchSegment { index: batch_index, label: batch_label } ] }, + entry_index: entry_index, + entry_total: entry_total, + rss: if rss_available { MeasuredValue { value: byte_size(rss_bytes) } } else { MeasuredUnavailable { cause: seed_heartbeat_unreadable_cause } }, + swap: if swap_available { MeasuredValue { value: byte_size(swap_bytes) } } else { MeasuredUnavailable { cause: seed_heartbeat_unreadable_cause } }, + pressure: if pressure_available { MeasuredValue { value: basis_point(count: pressure_bp) } } else { MeasuredUnavailable { cause: seed_heartbeat_unreadable_cause } } + }), + tier: seed_tier(emoji: emoji) + ) +} + data observation_seed_render_disposition: Disposition = Terminal { reason: "The seed to .dag render boundary is Terminal, not a scaffold: a boundary the seed calls across to keep the format single-authority is the mechanism, not an interim stand-in. What dissolves is each raw eprintln the seed replaces with a call here — counted by gunbc.observation_emit_census — and, further out, the seed itself as v2 realizes the witnesses natively; neither changes what this module models, which is that a seed occurrence is an observation event projected by the one renderer." } diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 1c0aef6471a..8929c219683 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3008,6 +3008,122 @@ mod tests { ); } + #[allow(clippy::too_many_arguments)] + fn run_seed_heartbeat_line( + source_roots: &[String], + elapsed_ms: u64, + batch_label: &str, + entry_index: u64, + entry_total: u64, + rss: Option, + swap: Option, + pressure: Option, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "seed_heartbeat_line", + &[ + (Some("elapsed_ms".into()), Value::Int(elapsed_ms as i64)), + (Some("batch_index".into()), Value::Int(0)), + ( + Some("batch_label".into()), + Value::Str(batch_label.to_string()), + ), + (Some("entry_index".into()), Value::Int(entry_index as i64)), + (Some("entry_total".into()), Value::Int(entry_total as i64)), + ( + Some("rss_bytes".into()), + Value::Int(rss.unwrap_or(0) as i64), + ), + (Some("rss_available".into()), Value::Bool(rss.is_some())), + ( + Some("swap_bytes".into()), + Value::Int(swap.unwrap_or(0) as i64), + ), + (Some("swap_available".into()), Value::Bool(swap.is_some())), + ( + Some("pressure_bp".into()), + Value::Int(pressure.unwrap_or(0) as i64), + ), + ( + Some("pressure_available".into()), + Value::Bool(pressure.is_some()), + ), + (Some("emoji".into()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + // The floor-memory heartbeat's seed→.dag boundary, proven by execution: + // seed_heartbeat_line takes the primitives the heartbeat thread has (elapsed, + // batch label, entry position, memory vitals) and projects them through the one + // renderer — identity first, human units, no raw byte dump. These golden strings + // are the oracle the Rust mirror is proven byte-equal to in the next commit; the + // subject is batch-grain (parallel entries → no fabricated per-module detail). + #[test] + fn seed_heartbeat_line_renders_identity_first_in_human_units() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + // The captured crawl window's memory beat: identity first, human units, the + // raw byte value absent. + let line = run_seed_heartbeat_line( + &roots, + 1_980_000, + "witness discovery", + 214, + 602, + Some(16_107_200_512), + Some(34_359_738_368), + Some(901), + true, + ) + .expect("seed_heartbeat_line must resolve and render"); + assert_eq!( + line, + "🕐 33 minutes in — still in witness discovery: entry 214 of 602. memory 15.0 GiB, swap 32.0 GiB, pressure 9.0%" + ); + assert!( + !line.contains("16107200512"), + "raw bytes must not appear: {line}" + ); + + // An unreadable cgroup field names its cause, never a fabricated zero. + let unreadable = run_seed_heartbeat_line( + &roots, + 500, + "self-host fixed-point", + 0, + 2, + None, + Some(0), + None, + true, + ) + .expect("resolve"); + assert_eq!( + unreadable, + "🕐 500ms in — still in self-host fixed-point: entry 0 of 2. memory unreadable (cgroup field unreadable), swap 0.0 GiB, pressure unreadable (cgroup field unreadable)" + ); + } + // The materialization-receipt chain by execution: a real entry resolves, a // claim evaluates on its InterpContext, and the ctx Drop absorbs ledger // totals into the process accumulator. The env latch is process-global and From b35a4b35c6478e799720ba403948d6601b3b05d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 23:35:22 +0000 Subject: [PATCH 19/39] File finding: shell.Env.Get realized as a printenv subprocess (transport-decomposition lane) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-directed finding (2026-07-24), do-not-fix-here. Records in the residual-shell census (§0b) a class distinct from that doc's shell-EMISSION axis: modeled ops whose interface shape is right but whose single hardwired transport is a shell escape where a NATIVE in-process handler is correct — the verbatim §3(b) N×M-adapter tell. shell.Env.Get (extdeps/shell/shell.dag:42) reads an env var the process already holds in its own environment by spawning `printenv` (wet_env_var, v1_interpreter.rs:5096). Reading your own environment is not a host effect; std::env::var is the native handler, chosen when locality is OnTarget, with shell/ssh reserved for a var on another host. Sibling: shell.Which.Check (`command -v`), already in the census. One root, three lanes. Not a floor-time lever (~ms/spawn); filed so the deficit is counted and prioritizable (§6), never absorbed into "it's only a few ms." Its native read is the lane's cheapest first consumer (a pure in-process read, no host_effect_apply even). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- ...ell-to-dag-residual-census-and-arc-completion.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/plans/shell-to-dag-residual-census-and-arc-completion.md b/docs/plans/shell-to-dag-residual-census-and-arc-completion.md index 9b78a5c8ecc..45eb3b2d072 100644 --- a/docs/plans/shell-to-dag-residual-census-and-arc-completion.md +++ b/docs/plans/shell-to-dag-residual-census-and-arc-completion.md @@ -13,6 +13,19 @@ So srv1/srv2 subsumption and srv3 bringup are the **same** work seen twice: gene --- +## 0b. Finding — modeled ops whose ONLY realization is a shell escape where a NATIVE handler is correct (transport-decomposition lane; filed 2026-07-24, do-not-fix-here) + +A distinct axis from §1's *emission* census: these are ops whose **interface shape is right** but whose **single hardwired transport is wrong** — the verbatim §3(b) "single hardwired transport is the N×M-adapter trap" tell. The shape belongs to the dependency; the transport is a Realization *handler, one of N* (§2). Each of these has exactly one handler — `shell` — where a **native in-process handler** is the correct realization when locality is `OnTarget`, and `shell`/`ssh` is the handler only when the target is another process on another host. + +| op (extdeps) | modeled transport | native handler that's missing | receipt | +| --- | --- | --- | --- | +| `shell.Env.Get` (`extdeps/shell/shell.dag:42`) | `shell { argv: ["printenv", "{name}"] }` | `std::env::var` — reading an env var **the process already holds in its own environment**. Realized today as `wet_env_var` spawning `printenv` (`v1_interpreter.rs:5096`). Reading your own environment isn't a host effect at all. | floor diff-observation spawns 5 `printenv` children per pass (compile-clean scope + discovery selection), repeated per floor pass — pure log clutter; ~ms each, **not a floor-time lever** | +| `shell.Which.Check` (`extdeps/shell/shell.dag:57`) | `command -v` (and the ssh `command -v ` ×2 at §3's `host_effect_realize`) | native path-search when `OnTarget`; ssh `command -v` only for a remote host | sibling flagged in the transport review (2026-07-24); same root | + +**One root, three lanes** (operator, 2026-07-24): these two, the transport review's `test -x`/`command -v` hand-strings for ssh, and the wall worker's fight are all *modeled operations whose only realization is a shell escape*. The dissolution is the transport-decomposition Realization: **same operation, two handlers, native chosen when locality is `OnTarget`** — `Env.Get`'s native read is the lane's **cheapest first consumer** (a pure in-process read, no `host_effect_apply` even). Not scheduled here; recorded so the deficit is counted and prioritizable (§6), never absorbed into "it's only a few ms." + +--- + ## 1. Residual-shell census (current tree) Five categories. "Genuine emitter" = emits bash that actually runs; "oracle/scaffold" = `serialize_bash` retained only for a test. From 60a44967f80a63cbda33aead142a0f115990218d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 23:59:56 +0000 Subject: [PATCH 20/39] =?UTF-8?q?Wiring=20flip=20(5/n):=20shell-echo=20?= =?UTF-8?q?=C2=A75=20split=20=E2=80=94=20routine=20Ambient,=20failure=20An?= =?UTF-8?q?omaly=20self-describing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The [shell]/$ echo class still printed at Normal after the firehose fix: it is the ShellTrace channel at Condensed, not the Instrumentation channel that [file] read rode. But naive "suppress ShellTrace at Normal" is a §5 fail-open — the failure stderr block rides the SAME channel via trace_emit(), so Suppressed would silence failures too. That Condensed was load-bearing. Root (operator's naming): one channel carrying two content classes with OPPOSITE attention — routine scaffolding (Ambient) and failure evidence (Anomaly) — a state-space conflation at the channel grain, exactly what the observation model dissolves by deriving attention per event, not per channel (law 4: routine collapses, anomaly expands). The fix uses the two EXISTING mechanisms, each governing its class — no third decision mechanism: - Routine ($ argv pre-spawn echo + [shell] done exit=… count) → the ShellTrace CHANNEL → Suppressed at Normal (Instrumentation's debug-only shape), Full at Verbose. - Failure evidence (stderr block) → the effect_stream DISPOSITION (SurfaceContent) ALONE, not the channel, so suppressing the routine echo cannot silence a failure. Two upgrades the disposition-gated block gains, both §5-correct now that the count is silent at Normal: 1. SELF-DESCRIBING — the block carries its own `$ `, so the failing command never scrolls away from its stderr (the pre-spawn echo it used to borrow from is gone at Normal). Strictly better than what suppression would have taken away. 2. SURFACES ON EMPTY STDERR — the block names the exit even when the command wrote nothing, because the routine count that used to carry the exit is now silent. Proven by execution: four-corner effect-stream suite kept; new discriminating RED at_normal_a_failing_effect_surfaces_its_command_a_passing_one_is_silent (passing → None; failing → `$ ` + stderr) + stderr_block_surfaces_on_surface_content_even_with_empty_stderr. Channel witness updated (w_shell_trace_routine_is_debug_only: Suppressed at Normal). Convergence named in output_policy.dag: when shell effects become observation events, trace_emit(channel) stops being the gate and derived attention replaces this split. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/output_policy.dag | 4 +- dag/test/claim/output_policy_witness_test.dag | 15 +- src/v1/stage0/src/v1_interpreter.rs | 136 ++++++++++++++---- 3 files changed, 120 insertions(+), 35 deletions(-) diff --git a/dag/gunbc/output_policy.dag b/dag/gunbc/output_policy.dag index 074d7de12bc..e4d5be49f4b 100644 --- a/dag/gunbc/output_policy.dag +++ b/dag/gunbc/output_policy.dag @@ -23,6 +23,8 @@ type OutputChannel | ShellTrace | Instrumentation +data shell_trace_channel_note: String = "ShellTrace governs the ROUTINE scaffolding of a host effect ONLY — the pre-spawn `$ argv` echo and the `[shell] done exit=… bytes` completion count. That is Ambient content (observation law 4: routine collapses), so at Normal it is Suppressed, surfacing only at Verbose; the profile now matches Instrumentation's debug-only shape. The FAILURE evidence — a diverging effect's stderr — is a DIFFERENT content class with the OPPOSITE attention (Anomaly: always surfaced, never collapsed), so it does NOT ride this channel: it is gated by `effect_stream_disposition` (SurfaceContent) alone and carries its own command identity, so a failure is self-describing even though the routine echo went silent. Routing both classes through one channel was a state-space conflation at the channel grain (two contents, opposite attention, one gate) — the exact confusion the observation model dissolves by deriving attention PER EVENT rather than per channel. Convergence: when shell effects become observation events, `trace_emit(channel)` stops being the gate and derived attention replaces this split; until then this is the two-mechanism fix, not a third decision mechanism (operator ruling, 2026-07-24)." + type VerbosityEnv { verbose: Bool quiet: Bool @@ -53,7 +55,7 @@ fn channel_decision(channel: OutputChannel, verbosity: Verbosity) -> OutputDecis } ShellTrace => match verbosity { Quiet => Suppressed - Normal => Condensed + Normal => Suppressed Verbose => Full } Instrumentation => match verbosity { diff --git a/dag/test/claim/output_policy_witness_test.dag b/dag/test/claim/output_policy_witness_test.dag index f0cd03f0d72..62bc60a5cc8 100644 --- a/dag/test/claim/output_policy_witness_test.dag +++ b/dag/test/claim/output_policy_witness_test.dag @@ -35,8 +35,15 @@ fn w_instrumentation_is_debug_only() -> Bool { dec_is(d: channel_decision(channel: Instrumentation, verbosity: Verbose), suppressed: false, condensed: false, full: true) } -fn w_shell_trace_condenses_at_normal() -> Bool { - dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: false, condensed: true, full: false) && +fn w_shell_trace_routine_is_debug_only() -> Bool { + // Routine shell scaffolding (the pre-spawn `$ argv` echo + the `[shell] done … bytes` + // count) is Ambient: Suppressed at Normal, surfacing only at Verbose — the same + // debug-only shape as Instrumentation. The FAILURE evidence is a different content + // class with the opposite attention (Anomaly) and does NOT ride this channel: it is + // gated by effect_stream_disposition (see w_divergence_surfaces_agreement_counts_at_normal, + // which still surfaces at Normal) and carries its own argv, so a failure stays + // self-describing even though the routine echo went silent. + dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: true, condensed: false, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Quiet), suppressed: true, condensed: false, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Verbose), suppressed: false, condensed: false, full: true) } @@ -71,7 +78,7 @@ fn w_resolve_channel_policy_normal_bundles_decisions() -> Bool { dec_is(d: p.diagnostic, suppressed: false, condensed: false, full: true) && dec_is(d: p.claim_result, suppressed: false, condensed: false, full: true) && dec_is(d: p.progress, suppressed: false, condensed: true, full: false) && - dec_is(d: p.shell_trace, suppressed: false, condensed: true, full: false) && + dec_is(d: p.shell_trace, suppressed: true, condensed: false, full: false) && dec_is(d: p.instrumentation, suppressed: true, condensed: false, full: false) } @@ -187,7 +194,7 @@ test fn output_policy_effect_stream_witnesses() -> Bool { test fn output_policy_witnesses() -> Bool { w_diagnostic_never_hidden() && w_instrumentation_is_debug_only() && - w_shell_trace_condenses_at_normal() && + w_shell_trace_routine_is_debug_only() && w_claim_result_shown_at_normal_hidden_at_quiet() && w_progress_condenses_at_normal() && w_verbose_wins_over_quiet() && diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 548f82cc0fb..b6bf9f70a6a 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5783,18 +5783,22 @@ fn trace_emit(channel: OutputChannel, line: &str) { // `gunbc.output_policy` ShellTrace decision (Suppressed / Condensed / Full) // rather than re-deriving it from verbosity — keeps CI logs readable instead of // dumping every `sh -c` script. +/// Collapse an argv into one readable line — runs of whitespace become a single space — +/// so a multiline `sh -c` script reads as one command. Shared by the Ambient pre-spawn +/// echo (capped) and the Anomaly failure block (uncapped: an anomaly expands fully). +fn shell_argv_collapsed(argv: &[String]) -> String { + argv.join(" ") + .split_whitespace() + .collect::>() + .join(" ") +} + fn render_shell_trace(argv: &[String]) { match output_decision(OutputChannel::ShellTrace) { OutputDecision::Suppressed => {} OutputDecision::Full => eprintln!("[shell] {}", argv.join(" ")), OutputDecision::Condensed => { - // Collapse newlines/runs of whitespace into a single readable line, - // then truncate so a multiline `sh -c` script is one tidy summary. - let collapsed: String = argv - .join(" ") - .split_whitespace() - .collect::>() - .join(" "); + let collapsed = shell_argv_collapsed(argv); // Fallback column bound (no Viewport at the trace site); the single // authority is `gunbc.output_policy.shell_trace_summary_max_columns`. const MAX: usize = 100; @@ -5840,31 +5844,47 @@ fn render_shell_completion_trace( stdout_bytes: usize, stderr: &[u8], wall: std::time::Duration, + argv: &[String], ) { + // Routine count line — Ambient (ShellTrace channel): silent at Normal, Verbose-only. trace_emit( OutputChannel::ShellTrace, &shell_completion_trace_line(exit_code, stdout_bytes, stderr.len(), wall), ); let disposition = effect_stream_disposition(expected, exit_code); - if let Some(block) = shell_completion_stderr_trace_block(disposition, exit_code, stderr) { - trace_emit(OutputChannel::ShellTrace, &block); - } -} - -/// Pure tail-bounding of captured stderr for the completion trace. Returns `None` when the -/// disposition is not `SurfaceContent`, or when there is no stderr to surface; `Some(block)` -/// is the `[shell] stderr` diagnostic, its content tail-bounded with a leading elision marker -/// when it exceeds the cap and every subject line guarded so relayed text cannot mint workflow -/// commands in the parent run. Kept pure (no `trace_emit`) so the surfacing decision is -/// unit-testable with a RED control. + if let Some(block) = shell_completion_stderr_trace_block(disposition, exit_code, stderr, argv) { + // Anomaly — surfaced regardless of the ShellTrace channel decision. The disposition + // (SurfaceContent) is the sole gate, so the routine echo going silent at Normal + // cannot silence a failure. Un-dimmed and un-grouped: an anomaly expands, and at + // Normal grouping is inactive (both trace channels Suppressed) so it stands alone. + eprintln!("{block}"); + } +} + +/// The failure block for a diverging host effect — the Anomaly half of the shell trace, +/// gated by the effect-stream `disposition` ALONE (not the `ShellTrace` channel), so +/// suppressing the routine Ambient echo at Normal can never silence a failure (§5). Two +/// upgrades over borrowing identity from the now-silent pre-spawn echo: +/// 1. SELF-DESCRIBING — carries its own `$ `, so the failing command never scrolls +/// away from its stderr (the pre-spawn echo is Ambient and gone at Normal). +/// 2. SURFACES ON EMPTY STDERR — the routine `[shell] done exit=…` count that used to +/// carry the exit code is now silent at Normal, so this block is the SOLE failure +/// signal and must name the exit even when the command wrote nothing to stderr. +/// Returns `None` off `SurfaceContent` (routine/suppressed). Kept pure (no emit) so the +/// surfacing decision is unit-testable with a RED control. fn shell_completion_stderr_trace_block( disposition: StreamDisposition, exit_code: i32, stderr: &[u8], + argv: &[String], ) -> Option { - if disposition != StreamDisposition::SurfaceContent || stderr.is_empty() { + if disposition != StreamDisposition::SurfaceContent { return None; } + let header = format!("[shell] $ {} (exit={exit_code})", shell_argv_collapsed(argv)); + if stderr.is_empty() { + return Some(header); + } const MAX_STDERR_TRACE: usize = 16384; let (elided, tail) = if stderr.len() > MAX_STDERR_TRACE { ( @@ -5880,7 +5900,7 @@ fn shell_completion_stderr_trace_block( String::new() }; Some(format!( - "[shell] stderr (exit={exit_code}):\n{prefix}{}", + "{header}:\n{prefix}{}", // Trailing newlines are stripped before guarding so a subject whose stderr // ends in `\n` (almost all of them) does not render a stray guard-only line. // This is presentation of the block, not a change to the guard rule: the @@ -6010,6 +6030,7 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), + &argv, ); output } else { @@ -6026,6 +6047,7 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), + &argv, ); output }; @@ -10133,6 +10155,11 @@ mod shell_completion_trace_tests { StreamDisposition::SurfaceContent } + /// An owned argv from string literals (the failure block now carries its own). + fn av(parts: &[&str]) -> Vec { + parts.iter().map(|s| s.to_string()).collect() + } + #[test] fn shell_completion_trace_line_formats_exit_stdout_stderr_wall() { let line = shell_completion_trace_line(0, 1234, 56, Duration::from_millis(5150)); @@ -10144,10 +10171,17 @@ mod shell_completion_trace_tests { #[test] fn stderr_block_surfaces_content_on_nonzero_exit() { - let block = - shell_completion_stderr_trace_block(surfacing(), 101, b"error: manifest not found\n") - .expect("non-zero exit with stderr must surface a diagnostic block"); - assert!(block.starts_with("[shell] stderr (exit=101):\n")); + // Self-describing: the block carries its own `$ ` — the failing command + // never scrolls away from its stderr, and it is not borrowed from the pre-spawn + // echo (Ambient, silent at Normal). + let block = shell_completion_stderr_trace_block( + surfacing(), + 101, + b"error: manifest not found\n", + &av(&["cargo", "build"]), + ) + .expect("non-zero exit with stderr must surface a diagnostic block"); + assert!(block.starts_with("[shell] $ cargo build (exit=101):\n")); assert!(block.contains("error: manifest not found")); } @@ -10155,13 +10189,13 @@ mod shell_completion_trace_tests { fn stderr_block_none_when_disposition_is_not_surface_content() { // RED control: the block is gated on the .dag disposition, not on a local // `exit != 0` re-derivation — a non-surfacing disposition yields nothing even - // with a non-zero exit and non-empty stderr, and an empty stderr yields - // nothing even when the disposition says surface. + // with a non-zero exit and non-empty stderr. assert_eq!( shell_completion_stderr_trace_block( StreamDisposition::SummarizeCounts, 1, - b"error: real failure\n" + b"error: real failure\n", + &av(&["git", "status"]), ), None ); @@ -10169,20 +10203,61 @@ mod shell_completion_trace_tests { shell_completion_stderr_trace_block( StreamDisposition::StreamSuppressed, 1, - b"error: real failure\n" + b"error: real failure\n", + &av(&["git", "status"]), ), None ); + } + + #[test] + fn stderr_block_surfaces_on_surface_content_even_with_empty_stderr() { + // §5 upgrade + RED control. The routine `[shell] done exit=…` count that used to + // carry the exit code is now Ambient (ShellTrace, silent at Normal), so on + // SurfaceContent this block is the SOLE failure signal and must name the command + + // exit even when the command wrote nothing to stderr — a failing effect is never + // silent. The OLD behavior returned None here (`stderr.is_empty()` short-circuit), + // which would silence an empty-stderr failure at Normal now that the count is gone. + let block = shell_completion_stderr_trace_block(surfacing(), 1, b"", &av(&["printenv", "MISSING"])) + .expect("a failing effect with empty stderr must still surface its identity"); + assert_eq!(block, "[shell] $ printenv MISSING (exit=1)"); + } + + #[test] + fn at_normal_a_failing_effect_surfaces_its_command_a_passing_one_is_silent() { + // The §5 proof for the shell-echo split: the routine `$ argv`/`[shell] done` trace + // is Ambient (ShellTrace Suppressed at Normal), but a FAILURE is Anomaly and must + // still surface — self-describingly. Composes the .dag disposition (ExpectSuccess × + // exit → the Normal four corners) with the block; the uninstalled fallback mirrors + // Normal. GREEN control: a passing effect → SummarizeCounts → no block (silent). assert_eq!( - shell_completion_stderr_trace_block(surfacing(), 1, b""), + shell_completion_stderr_trace_block( + effect_stream_disposition(ExpectedOutcome::ExpectSuccess, 0), + 0, + b"", + &av(&["printenv", "PATH"]), + ), None ); + // Discriminating RED: a failing effect → SurfaceContent → surfaces `$ ` + + // stderr even though the pre-spawn echo went silent. If the split ever routes the + // block back through the suppressed ShellTrace channel (silencing the failure) or + // drops the command identity, this reds. + let block = shell_completion_stderr_trace_block( + effect_stream_disposition(ExpectedOutcome::ExpectSuccess, 1), + 1, + b"fatal: not a git repository\n", + &av(&["git", "rev-parse", "--show-toplevel"]), + ) + .expect("a failing effect must surface at Normal"); + assert!(block.starts_with("[shell] $ git rev-parse --show-toplevel (exit=1):")); + assert!(block.contains("fatal: not a git repository")); } #[test] fn stderr_block_tail_bounds_and_marks_elision() { let big = vec![b'x'; 16384 + 500]; - let block = shell_completion_stderr_trace_block(surfacing(), 1, &big) + let block = shell_completion_stderr_trace_block(surfacing(), 1, &big, &av(&["cargo", "build"])) .expect("oversized stderr surfaces"); assert!(block.contains("<500 earlier stderr bytes elided>")); // Only the 16384-byte tail is carried, not the full 16884-byte body: the trailing @@ -10200,6 +10275,7 @@ mod shell_completion_trace_tests { surfacing(), 1, b"::error::build verification: artifact absent\n::warning::next\n", + &av(&["sh", "-c", "build"]), ) .expect("failing effect surfaces its stderr"); for line in block.lines().skip(1) { From d595163d62de4c07e03fba86a2206054b44f4f35 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 00:16:33 +0000 Subject: [PATCH 21/39] =?UTF-8?q?Revert=20"Wiring=20flip=20(5/n):=20shell-?= =?UTF-8?q?echo=20=C2=A75=20split"=20=E2=80=94=20approach=20was=20wrong,?= =?UTF-8?q?=20unbreak=20the=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 60a4496. Two defects, the second fatal to the approach: 1. Parse break (caught late): the witness carried `//` comments, but daglang has NO line-comment syntax — documentation is `data …_note: String` only. build_module_path_index scans every .dag at install_output_policy startup, so the parse error panicked the whole floor, not just that witness. LESSON: verify .dag by execution before pushing; a Rust-lib test does not exercise the .dag parse/resolve path. 2. The approach itself was wrong (the real reason for the full revert). Making ShellTrace Suppressed at Normal does not just silence the routine echo — effect_stream_disposition is GATED BY channel_decision: match channel_decision(channel, verbosity) { Suppressed => StreamSuppressed // failures silenced Condensed => divergence_disposition(…) // divergence surfaces, agreement counts Full => SurfaceContent } So Condensed-at-Normal is load-bearing TWICE (trace_emit AND the divergence disposition), and host-effect grouping keys on the same channel. Suppressing it is a §5 fail-open (a real failing effect's stderr goes silent at Normal); the Rust test passed only because it read the hardcoded EFFECT_STREAM_POLICY_FALLBACK, masking the installed-policy break. The alternative (route routine → Instrumentation, keep ShellTrace Condensed) leaves empty `##[group]` brackets and buries the failure block inside the collapsed section. FINDING for the redo: the channel↔disposition↔grouping coupling IS the "one channel, two content classes" conflation the operator named — and there is no clean immediate fix that does not touch it. The correct fix decouples the failure disposition from the routine channel verbosity (the "bigger lift" flagged as needing a design steer), which lands back at the operator's "Hold — I'll steer". The self-describing failure-block design (carries its own argv, surfaces on empty stderr) is sound and preserved in 60a4496's history for reuse. Branch returns to the green b35a4b3 state. Shell-echo goes back to HELD pending the disposition-decoupling design decision. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01TUqWZP4HTVyUfW4qRqWya1 --- dag/gunbc/output_policy.dag | 4 +- dag/test/claim/output_policy_witness_test.dag | 15 +- src/v1/stage0/src/v1_interpreter.rs | 136 ++++-------------- 3 files changed, 35 insertions(+), 120 deletions(-) diff --git a/dag/gunbc/output_policy.dag b/dag/gunbc/output_policy.dag index e4d5be49f4b..074d7de12bc 100644 --- a/dag/gunbc/output_policy.dag +++ b/dag/gunbc/output_policy.dag @@ -23,8 +23,6 @@ type OutputChannel | ShellTrace | Instrumentation -data shell_trace_channel_note: String = "ShellTrace governs the ROUTINE scaffolding of a host effect ONLY — the pre-spawn `$ argv` echo and the `[shell] done exit=… bytes` completion count. That is Ambient content (observation law 4: routine collapses), so at Normal it is Suppressed, surfacing only at Verbose; the profile now matches Instrumentation's debug-only shape. The FAILURE evidence — a diverging effect's stderr — is a DIFFERENT content class with the OPPOSITE attention (Anomaly: always surfaced, never collapsed), so it does NOT ride this channel: it is gated by `effect_stream_disposition` (SurfaceContent) alone and carries its own command identity, so a failure is self-describing even though the routine echo went silent. Routing both classes through one channel was a state-space conflation at the channel grain (two contents, opposite attention, one gate) — the exact confusion the observation model dissolves by deriving attention PER EVENT rather than per channel. Convergence: when shell effects become observation events, `trace_emit(channel)` stops being the gate and derived attention replaces this split; until then this is the two-mechanism fix, not a third decision mechanism (operator ruling, 2026-07-24)." - type VerbosityEnv { verbose: Bool quiet: Bool @@ -55,7 +53,7 @@ fn channel_decision(channel: OutputChannel, verbosity: Verbosity) -> OutputDecis } ShellTrace => match verbosity { Quiet => Suppressed - Normal => Suppressed + Normal => Condensed Verbose => Full } Instrumentation => match verbosity { diff --git a/dag/test/claim/output_policy_witness_test.dag b/dag/test/claim/output_policy_witness_test.dag index 62bc60a5cc8..f0cd03f0d72 100644 --- a/dag/test/claim/output_policy_witness_test.dag +++ b/dag/test/claim/output_policy_witness_test.dag @@ -35,15 +35,8 @@ fn w_instrumentation_is_debug_only() -> Bool { dec_is(d: channel_decision(channel: Instrumentation, verbosity: Verbose), suppressed: false, condensed: false, full: true) } -fn w_shell_trace_routine_is_debug_only() -> Bool { - // Routine shell scaffolding (the pre-spawn `$ argv` echo + the `[shell] done … bytes` - // count) is Ambient: Suppressed at Normal, surfacing only at Verbose — the same - // debug-only shape as Instrumentation. The FAILURE evidence is a different content - // class with the opposite attention (Anomaly) and does NOT ride this channel: it is - // gated by effect_stream_disposition (see w_divergence_surfaces_agreement_counts_at_normal, - // which still surfaces at Normal) and carries its own argv, so a failure stays - // self-describing even though the routine echo went silent. - dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: true, condensed: false, full: false) && +fn w_shell_trace_condenses_at_normal() -> Bool { + dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: false, condensed: true, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Quiet), suppressed: true, condensed: false, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Verbose), suppressed: false, condensed: false, full: true) } @@ -78,7 +71,7 @@ fn w_resolve_channel_policy_normal_bundles_decisions() -> Bool { dec_is(d: p.diagnostic, suppressed: false, condensed: false, full: true) && dec_is(d: p.claim_result, suppressed: false, condensed: false, full: true) && dec_is(d: p.progress, suppressed: false, condensed: true, full: false) && - dec_is(d: p.shell_trace, suppressed: true, condensed: false, full: false) && + dec_is(d: p.shell_trace, suppressed: false, condensed: true, full: false) && dec_is(d: p.instrumentation, suppressed: true, condensed: false, full: false) } @@ -194,7 +187,7 @@ test fn output_policy_effect_stream_witnesses() -> Bool { test fn output_policy_witnesses() -> Bool { w_diagnostic_never_hidden() && w_instrumentation_is_debug_only() && - w_shell_trace_routine_is_debug_only() && + w_shell_trace_condenses_at_normal() && w_claim_result_shown_at_normal_hidden_at_quiet() && w_progress_condenses_at_normal() && w_verbose_wins_over_quiet() && diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index b6bf9f70a6a..548f82cc0fb 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5783,22 +5783,18 @@ fn trace_emit(channel: OutputChannel, line: &str) { // `gunbc.output_policy` ShellTrace decision (Suppressed / Condensed / Full) // rather than re-deriving it from verbosity — keeps CI logs readable instead of // dumping every `sh -c` script. -/// Collapse an argv into one readable line — runs of whitespace become a single space — -/// so a multiline `sh -c` script reads as one command. Shared by the Ambient pre-spawn -/// echo (capped) and the Anomaly failure block (uncapped: an anomaly expands fully). -fn shell_argv_collapsed(argv: &[String]) -> String { - argv.join(" ") - .split_whitespace() - .collect::>() - .join(" ") -} - fn render_shell_trace(argv: &[String]) { match output_decision(OutputChannel::ShellTrace) { OutputDecision::Suppressed => {} OutputDecision::Full => eprintln!("[shell] {}", argv.join(" ")), OutputDecision::Condensed => { - let collapsed = shell_argv_collapsed(argv); + // Collapse newlines/runs of whitespace into a single readable line, + // then truncate so a multiline `sh -c` script is one tidy summary. + let collapsed: String = argv + .join(" ") + .split_whitespace() + .collect::>() + .join(" "); // Fallback column bound (no Viewport at the trace site); the single // authority is `gunbc.output_policy.shell_trace_summary_max_columns`. const MAX: usize = 100; @@ -5844,47 +5840,31 @@ fn render_shell_completion_trace( stdout_bytes: usize, stderr: &[u8], wall: std::time::Duration, - argv: &[String], ) { - // Routine count line — Ambient (ShellTrace channel): silent at Normal, Verbose-only. trace_emit( OutputChannel::ShellTrace, &shell_completion_trace_line(exit_code, stdout_bytes, stderr.len(), wall), ); let disposition = effect_stream_disposition(expected, exit_code); - if let Some(block) = shell_completion_stderr_trace_block(disposition, exit_code, stderr, argv) { - // Anomaly — surfaced regardless of the ShellTrace channel decision. The disposition - // (SurfaceContent) is the sole gate, so the routine echo going silent at Normal - // cannot silence a failure. Un-dimmed and un-grouped: an anomaly expands, and at - // Normal grouping is inactive (both trace channels Suppressed) so it stands alone. - eprintln!("{block}"); - } -} - -/// The failure block for a diverging host effect — the Anomaly half of the shell trace, -/// gated by the effect-stream `disposition` ALONE (not the `ShellTrace` channel), so -/// suppressing the routine Ambient echo at Normal can never silence a failure (§5). Two -/// upgrades over borrowing identity from the now-silent pre-spawn echo: -/// 1. SELF-DESCRIBING — carries its own `$ `, so the failing command never scrolls -/// away from its stderr (the pre-spawn echo is Ambient and gone at Normal). -/// 2. SURFACES ON EMPTY STDERR — the routine `[shell] done exit=…` count that used to -/// carry the exit code is now silent at Normal, so this block is the SOLE failure -/// signal and must name the exit even when the command wrote nothing to stderr. -/// Returns `None` off `SurfaceContent` (routine/suppressed). Kept pure (no emit) so the -/// surfacing decision is unit-testable with a RED control. + if let Some(block) = shell_completion_stderr_trace_block(disposition, exit_code, stderr) { + trace_emit(OutputChannel::ShellTrace, &block); + } +} + +/// Pure tail-bounding of captured stderr for the completion trace. Returns `None` when the +/// disposition is not `SurfaceContent`, or when there is no stderr to surface; `Some(block)` +/// is the `[shell] stderr` diagnostic, its content tail-bounded with a leading elision marker +/// when it exceeds the cap and every subject line guarded so relayed text cannot mint workflow +/// commands in the parent run. Kept pure (no `trace_emit`) so the surfacing decision is +/// unit-testable with a RED control. fn shell_completion_stderr_trace_block( disposition: StreamDisposition, exit_code: i32, stderr: &[u8], - argv: &[String], ) -> Option { - if disposition != StreamDisposition::SurfaceContent { + if disposition != StreamDisposition::SurfaceContent || stderr.is_empty() { return None; } - let header = format!("[shell] $ {} (exit={exit_code})", shell_argv_collapsed(argv)); - if stderr.is_empty() { - return Some(header); - } const MAX_STDERR_TRACE: usize = 16384; let (elided, tail) = if stderr.len() > MAX_STDERR_TRACE { ( @@ -5900,7 +5880,7 @@ fn shell_completion_stderr_trace_block( String::new() }; Some(format!( - "{header}:\n{prefix}{}", + "[shell] stderr (exit={exit_code}):\n{prefix}{}", // Trailing newlines are stripped before guarding so a subject whose stderr // ends in `\n` (almost all of them) does not render a stray guard-only line. // This is presentation of the block, not a change to the guard rule: the @@ -6030,7 +6010,6 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), - &argv, ); output } else { @@ -6047,7 +6026,6 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), - &argv, ); output }; @@ -10155,11 +10133,6 @@ mod shell_completion_trace_tests { StreamDisposition::SurfaceContent } - /// An owned argv from string literals (the failure block now carries its own). - fn av(parts: &[&str]) -> Vec { - parts.iter().map(|s| s.to_string()).collect() - } - #[test] fn shell_completion_trace_line_formats_exit_stdout_stderr_wall() { let line = shell_completion_trace_line(0, 1234, 56, Duration::from_millis(5150)); @@ -10171,17 +10144,10 @@ mod shell_completion_trace_tests { #[test] fn stderr_block_surfaces_content_on_nonzero_exit() { - // Self-describing: the block carries its own `$ ` — the failing command - // never scrolls away from its stderr, and it is not borrowed from the pre-spawn - // echo (Ambient, silent at Normal). - let block = shell_completion_stderr_trace_block( - surfacing(), - 101, - b"error: manifest not found\n", - &av(&["cargo", "build"]), - ) - .expect("non-zero exit with stderr must surface a diagnostic block"); - assert!(block.starts_with("[shell] $ cargo build (exit=101):\n")); + let block = + shell_completion_stderr_trace_block(surfacing(), 101, b"error: manifest not found\n") + .expect("non-zero exit with stderr must surface a diagnostic block"); + assert!(block.starts_with("[shell] stderr (exit=101):\n")); assert!(block.contains("error: manifest not found")); } @@ -10189,13 +10155,13 @@ mod shell_completion_trace_tests { fn stderr_block_none_when_disposition_is_not_surface_content() { // RED control: the block is gated on the .dag disposition, not on a local // `exit != 0` re-derivation — a non-surfacing disposition yields nothing even - // with a non-zero exit and non-empty stderr. + // with a non-zero exit and non-empty stderr, and an empty stderr yields + // nothing even when the disposition says surface. assert_eq!( shell_completion_stderr_trace_block( StreamDisposition::SummarizeCounts, 1, - b"error: real failure\n", - &av(&["git", "status"]), + b"error: real failure\n" ), None ); @@ -10203,61 +10169,20 @@ mod shell_completion_trace_tests { shell_completion_stderr_trace_block( StreamDisposition::StreamSuppressed, 1, - b"error: real failure\n", - &av(&["git", "status"]), + b"error: real failure\n" ), None ); - } - - #[test] - fn stderr_block_surfaces_on_surface_content_even_with_empty_stderr() { - // §5 upgrade + RED control. The routine `[shell] done exit=…` count that used to - // carry the exit code is now Ambient (ShellTrace, silent at Normal), so on - // SurfaceContent this block is the SOLE failure signal and must name the command + - // exit even when the command wrote nothing to stderr — a failing effect is never - // silent. The OLD behavior returned None here (`stderr.is_empty()` short-circuit), - // which would silence an empty-stderr failure at Normal now that the count is gone. - let block = shell_completion_stderr_trace_block(surfacing(), 1, b"", &av(&["printenv", "MISSING"])) - .expect("a failing effect with empty stderr must still surface its identity"); - assert_eq!(block, "[shell] $ printenv MISSING (exit=1)"); - } - - #[test] - fn at_normal_a_failing_effect_surfaces_its_command_a_passing_one_is_silent() { - // The §5 proof for the shell-echo split: the routine `$ argv`/`[shell] done` trace - // is Ambient (ShellTrace Suppressed at Normal), but a FAILURE is Anomaly and must - // still surface — self-describingly. Composes the .dag disposition (ExpectSuccess × - // exit → the Normal four corners) with the block; the uninstalled fallback mirrors - // Normal. GREEN control: a passing effect → SummarizeCounts → no block (silent). assert_eq!( - shell_completion_stderr_trace_block( - effect_stream_disposition(ExpectedOutcome::ExpectSuccess, 0), - 0, - b"", - &av(&["printenv", "PATH"]), - ), + shell_completion_stderr_trace_block(surfacing(), 1, b""), None ); - // Discriminating RED: a failing effect → SurfaceContent → surfaces `$ ` + - // stderr even though the pre-spawn echo went silent. If the split ever routes the - // block back through the suppressed ShellTrace channel (silencing the failure) or - // drops the command identity, this reds. - let block = shell_completion_stderr_trace_block( - effect_stream_disposition(ExpectedOutcome::ExpectSuccess, 1), - 1, - b"fatal: not a git repository\n", - &av(&["git", "rev-parse", "--show-toplevel"]), - ) - .expect("a failing effect must surface at Normal"); - assert!(block.starts_with("[shell] $ git rev-parse --show-toplevel (exit=1):")); - assert!(block.contains("fatal: not a git repository")); } #[test] fn stderr_block_tail_bounds_and_marks_elision() { let big = vec![b'x'; 16384 + 500]; - let block = shell_completion_stderr_trace_block(surfacing(), 1, &big, &av(&["cargo", "build"])) + let block = shell_completion_stderr_trace_block(surfacing(), 1, &big) .expect("oversized stderr surfaces"); assert!(block.contains("<500 earlier stderr bytes elided>")); // Only the 16384-byte tail is carried, not the full 16884-byte body: the trailing @@ -10275,7 +10200,6 @@ mod shell_completion_trace_tests { surfacing(), 1, b"::error::build verification: artifact absent\n::warning::next\n", - &av(&["sh", "-c", "build"]), ) .expect("failing effect surfaces its stderr"); for line in block.lines().skip(1) { From 7e77b92e66b0919be1f3983fa7c02546f82133f2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 00:16:31 +0000 Subject: [PATCH 22/39] Wiring flip (4b/n): floor-memory heartbeat via render_heartbeat_line_mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flagship of the observation wiring flip: replace the [floor-memory] raw byte dump with the identity-first 🕐 heartbeat, proven byte-equal to the 4a seed oracle (seed_heartbeat_line). - render_heartbeat_line_mirror: pure Rust mirror of ci_heartbeat_line ∘ ci_render_line — the heartbeat thread cannot call the interpreter (duplicate module index under the memory envelope it watches). Discriminating RED render_heartbeat_line_mirror_matches_seed_oracle pins byte-equality on the crawl-window and unreadable-field goldens. - HeartbeatFeed (cli_run): process-global batch label + entries done/total, armed only when entry_total is known and non-zero (never a fabricated 0-of-0). Updated at batch-enter and at the existing index_schedule_entry_completed per-entry point (SingleClaim path increments per claim result). Discovery fills the total once the roster's entry-group count is known. - Delete the byte dump; keep the regime-disclosure line (marker stays for census hygiene). Flip floor_memory_site → MigratedToObservation; restructure census/lockstep witnesses (frontier 5→4, dump shape asserted gone). - Also: strip invalid // comments from output_policy_witness_test.dag that the shell-echo §5 commit left (dag has no // comments — parse Slash). Co-Authored-By: Cursor Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 9 +- dag/gunbc/observation_seed_render.dag | 2 +- .../observation_emit_census_witness_test.dag | 22 +- .../observation_lockstep_witness_test.dag | 4 +- src/v1/stage0/src/bin/claim_executor.rs | 258 ++++++++++++++++-- src/v1/stage0/src/cli_run.rs | 148 +++++++++- 6 files changed, 399 insertions(+), 44 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index 60edabddaf1..c0a3226a47c 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. The named negative example the operator called out — the [floor-memory] raw byte dump — is a rostered frontier row here, so the census already carries the thing it exists to kill." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. The named negative example — the [floor-memory] raw byte dump — has MIGRATED (wiring flip 4b): identity-first heartbeat via ci_heartbeat_line / render_heartbeat_line_mirror; the regime-disclosure line keeps the marker so the roster cannot go stale." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -24,9 +24,8 @@ type CensusedEmitSite { data floor_memory_site: CensusedEmitSite = CensusedEmitSite { marker: "[floor-memory]" as NonEmptyStr, source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, - disposition: CountedFrontierSite { - reason: "the named negative example — a raw byte dump (current=16106717184) with no subject, sixty-plus context-free lines an hour, the exact shape the operator watched for ten minutes during the crawl window that priced this lane" as NonEmptyStr, - dissolve_on: "the observation CI heartbeat projection (gunbc.observation_ci_render.ci_heartbeat_line) replaces it — identity-first, human units, subject named — when the P1 renderer wiring lands after the CI two-tier rework PR" as NonEmptyStr + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_heartbeat_line (seed mirror render_heartbeat_line_mirror; HeartbeatFeed subject)" as NonEmptyStr } } @@ -83,7 +82,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: five tag families migrated-pending, the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." +data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: four tag families still frontier, one migrated (floor-memory), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 4b9aa0e932d..0db5eac846f 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -49,7 +49,7 @@ fn phase_concluded_line(phase: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, e data seed_heartbeat_unreadable_cause: NonEmptyStr = "cgroup field unreadable" as NonEmptyStr -data seed_heartbeat_note: String = "The floor-memory heartbeat's projection. The heartbeat runs on a detached liveness thread in a memory-constrained context, so the seed renders it through a Rust MIRROR of ci_heartbeat_line rather than calling the interpreter there — an interpreter render on that thread would build a duplicate module index, consuming the very memory the heartbeat watches (DESIGN section 2), and the thread's whole point is to stay alive when the main interpreter is busy. This fn is the ORACLE that keeps the mirror honest: the seed's Rust test runs this (through the interpreter) and its mirror on the same samples and asserts byte-equality, so the format authority stays here in the one renderer and any drift reds. The subject is batch-grain by construction — the floor walks entries in parallel, so there is no single active module to name; the primitive interface carries no module or phase, which is why the mirror is a COMPLETE reflection of this fn over the seed's real input space, never a fabricated per-module detail (observation law 2: identity, never invented)." +data seed_heartbeat_note: String = "The floor-memory heartbeat's projection. The heartbeat runs on a detached liveness thread in a memory-constrained context, so the seed renders it through a Rust MIRROR of ci_heartbeat_line rather than calling the interpreter there — an interpreter render on that thread would build a duplicate module index, consuming the very memory the heartbeat watches (DESIGN section 2), and the thread's whole point is to stay alive when the main interpreter is busy. This fn is the ORACLE that keeps the mirror honest: the seed's Rust test runs this (through the interpreter) and its mirror on the same samples and asserts byte-equality, so the format authority stays here in the one renderer and any drift reds. The subject is batch-grain by construction — the floor walks entries in parallel, so there is no single active module to name; the primitive interface carries no module or phase, which is why the mirror is a COMPLETE reflection of this fn over the seed's real input space, never a fabricated per-module detail (observation law 2: identity, never invented). Wired (4b): claim_executor.render_heartbeat_line_mirror + HeartbeatFeed." fn seed_heartbeat_line( elapsed_ms: Nat, diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 2288399e0e2..0dda93470d8 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -18,7 +18,7 @@ import gunbc.observation_emit_census { data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. The [floor-memory] negative example is checked positively — it must still be in the seed and still be classified as a frontier — so the census cannot quietly drop the very site it exists to kill." +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. The [floor-memory] named negative example has migrated: the raw byte dump is gone, the site is MigratedToObservation via ci_heartbeat_line, and the regime-disclosure line keeps the marker so the roster cannot go stale — the dump shape itself is asserted absent." fn census_source(path: String) -> String { let r = filesystem_read(path: path) @@ -55,28 +55,30 @@ fn w_every_frontier_row_carries_a_real_dissolve_on() -> Bool { }) } -fn w_the_named_negative_example_is_present_and_classified_frontier() -> Bool { +fn w_the_named_negative_example_has_migrated() -> Bool { census_marker_present(site: floor_memory_site) && - emit_site_is_frontier(site: floor_memory_site) && - string_contains(s: floor_memory_site.marker, pattern: "floor-memory") + !emit_site_is_frontier(site: floor_memory_site) && + string_contains(s: floor_memory_site.marker, pattern: "floor-memory") && + string_contains(s: emit_site_dissolve_on(site: floor_memory_site), pattern: "ci_heartbeat_line") } fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { - observation_emit_frontier_count() == 5 && + observation_emit_frontier_count() == 4 && count(observation_emit_roster) == 5 } -fn w_the_raw_byte_dump_shape_is_still_in_the_seed_to_be_killed() -> Bool { +fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { let executor = census_source(path: "src/v1/stage0/src/bin/claim_executor.rs") - string_contains(s: executor, pattern: "[floor-memory] t=") && - string_contains(s: executor, pattern: "current=") + !string_contains(s: executor, pattern: "[floor-memory] t=") && + !string_contains(s: executor, pattern: "current={}") && + string_contains(s: executor, pattern: "render_heartbeat_line_mirror") } test fn observation_emit_census_witnesses() -> Bool { w_every_rostered_marker_still_exists_in_the_seed() && w_roster_staleness_check_discriminates() && w_every_frontier_row_carries_a_real_dissolve_on() && - w_the_named_negative_example_is_present_and_classified_frontier() && + w_the_named_negative_example_has_migrated() && w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && - w_the_raw_byte_dump_shape_is_still_in_the_seed_to_be_killed() + w_the_raw_byte_dump_shape_is_gone_from_the_seed() } diff --git a/dag/test/claim/observation_lockstep_witness_test.dag b/dag/test/claim/observation_lockstep_witness_test.dag index c51c9e5ddc4..0bd63f7f752 100644 --- a/dag/test/claim/observation_lockstep_witness_test.dag +++ b/dag/test/claim/observation_lockstep_witness_test.dag @@ -69,7 +69,9 @@ fn w_heartbeat_period_matches_the_seed_cadence() -> Bool { } fn w_heartbeat_source_still_refuses_to_fabricate() -> Bool { - lockstep_has(path: executor_source_path, needle: "refusing to fabricate") + lockstep_has(path: executor_source_path, needle: "refusing to fabricate") && + lockstep_has(path: executor_source_path, needle: "render_heartbeat_line_mirror") && + lockstep_has(path: executor_source_path, needle: "heartbeat_feed_snapshot") } test fn observation_lockstep_witnesses() -> Bool { diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 8929c219683..a6e5ff7de38 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -12,6 +12,7 @@ use std::time::Instant; use v1_compiler::cli_run::workspace_root; use v1_compiler::cli_run::{ compute_histogram_data, compute_witness_timing_rows, enable_floor_compile_clean_lazy_install, + heartbeat_feed_enter_batch, heartbeat_feed_entry_completed, heartbeat_feed_snapshot, install_floor_compile_clean_receipt, make_eval_context, resolve_entry_graph, resolve_entry_graph_shared, run_claim, run_discovery_corpus_with_options, run_value, set_phase, top_n_slowest_witnesses, ClaimOutcome, DiscoveryCorpusOptions, DiscoverySummary, @@ -20,8 +21,8 @@ use v1_compiler::cli_run::{ }; use v1_compiler::memory_governor::{ binding_cap_cgroup_dir, binding_high_cgroup_dir, floor_budget_below_minimum_footprint, - leaf_cgroup_dir, mem_total_bytes, memory_events_field, memory_pressure_some_avg10, - read_cgroup_raw, read_cgroup_u64, AdmittedSlot, MemoryGovernor, + leaf_cgroup_dir, mem_total_bytes, memory_pressure_some_avg10, read_cgroup_raw, read_cgroup_u64, + AdmittedSlot, MemoryGovernor, }; use v1_compiler::v1_interpreter::{ color_enabled, paint, run_in_context_with_args, sgr, ExecutionMode, InterpContext, Value, @@ -1328,9 +1329,101 @@ fn peak_rss_bytes() -> Option { Some(kb.saturating_mul(1024)) } -fn heartbeat_field(v: Option) -> String { - v.map(|b| b.to_string()) - .unwrap_or_else(|| "unreadable".into()) +/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds` — where the +/// sentence form switches from seconds to minutes (display policy, not a unit). +const CI_MINUTE_SWITCH_SECONDS: u64 = 90; + +/// Mirror of `gunbc.observation_seed_render.seed_heartbeat_unreadable_cause`. +const SEED_HEARTBEAT_UNREADABLE_CAUSE: &str = "cgroup field unreadable"; + +/// Pure Rust mirror of `gunbc.observation_seed_render.seed_heartbeat_line` — +/// `ci_heartbeat_line ∘ ci_render_line` over the seed's real input space. The +/// heartbeat thread cannot call the interpreter (would build a duplicate module +/// index under the memory envelope it watches — DESIGN §2); this mirror is proven +/// byte-equal to the `.dag` oracle by `render_heartbeat_line_mirror_matches_seed_oracle`. +/// Subject is batch-grain only (parallel entries → no fabricated per-module detail). +fn render_heartbeat_line_mirror( + elapsed_ms: u64, + batch_label: &str, + entry_index: u64, + entry_total: u64, + rss_bytes: Option, + swap_bytes: Option, + pressure_bp: Option, + emoji: bool, +) -> String { + let glyph = if emoji { "🕐" } else { "◷" }; + let duration = mirror_ci_human_duration(elapsed_ms); + let rss = mirror_ci_measured_bytes(rss_bytes); + let swap = mirror_ci_measured_bytes(swap_bytes); + let pressure = mirror_ci_measured_percent(pressure_bp); + format!( + "{glyph} {duration} in — still in {batch_label}: entry {entry_index} of {entry_total}. memory {rss}, swap {swap}, pressure {pressure}" + ) +} + +fn mirror_ci_human_duration(ms: u64) -> String { + if ms < 1_000 { + format!("{ms}ms") + } else if ms < CI_MINUTE_SWITCH_SECONDS * 1_000 { + format!("{} seconds", ms / 1_000) + } else { + format!("{} minutes", ms / 60_000) + } +} + +fn mirror_ci_tenths_text(tenths: u64) -> String { + format!("{}.{}", tenths / 10, tenths % 10) +} + +fn mirror_ci_human_bytes(bytes: u64) -> String { + // Mirror of ci_gibibyte_tenths: (bytes * 10) / gibibyte_scale_factor_bytes (2^30). + let tenths = (bytes.saturating_mul(10)) / 1_073_741_824; + format!("{} GiB", mirror_ci_tenths_text(tenths)) +} + +fn mirror_ci_human_percent(bp: u64) -> String { + // Mirror of ci_human_percent: tenths = bp / 10 → "9.0%". + format!("{}%", mirror_ci_tenths_text(bp / 10)) +} + +fn mirror_ci_measured_bytes(v: Option) -> String { + match v { + Some(b) => mirror_ci_human_bytes(b), + None => format!("unreadable ({SEED_HEARTBEAT_UNREADABLE_CAUSE})"), + } +} + +fn mirror_ci_measured_percent(v: Option) -> String { + match v { + Some(bp) => mirror_ci_human_percent(bp), + None => format!("unreadable ({SEED_HEARTBEAT_UNREADABLE_CAUSE})"), + } +} + +/// PSI `avg10` is a percent with one decimal (e.g. `"9.01"`). One basis point is +/// 0.01 percentage points, so percent × 100 = bp (9.01 → 901) — the same scale +/// `std.observation` carries for `PsiPressure.avg10`. +fn psi_avg10_to_basis_points(avg10: &str) -> Option { + let pct: f64 = avg10.parse().ok()?; + if !pct.is_finite() || pct < 0.0 { + return None; + } + Some((pct * 100.0).round() as u64) +} + +fn batch_heartbeat_label(batch: &[Runnable]) -> String { + if batch + .iter() + .any(|r| matches!(r, Runnable::DiscoveryBatch { .. })) + { + // Canonical crawl-window subject — matches the seed oracle's batch label. + "witness discovery".to_string() + } else if let Some(Runnable::SingleClaim { function, .. }) = batch.first() { + function.clone() + } else { + "batch".to_string() + } } /// Floor memory heartbeat — FIDELITY ONLY (reads state, changes no behavior; §5 stopped-line @@ -1338,13 +1431,13 @@ fn heartbeat_field(v: Option) -> String { /// invisible precisely here: `memory.high` throttles instead of killing, so the only log /// evidence was a post-hoc `memory.peak` pinned at `high + <1MiB` after a 30-49min silent /// tail. One synchronous regime-disclosure line at floor start (which limits bind, where), -/// then one line per minute from a detached thread (dies with the process): `memory.current`, -/// `memory.swap.current`, `memory.events` high-throttle count, PSI `some avg10`. The wedge -/// signature becomes: current pinned at high, swap climbing, high-events exploding, PSI avg10 -/// double digits — attributable to a 60s window instead of a forensic reconstruction. -/// Sampling denominator = the binding-high dir when set (the slot slice that throttles), else -/// the binding-cap dir, else the leaf (whole-machine regimes); absence of all three refuses -/// loudly and the floor proceeds unmonitored — never a fabricated zero. +/// then one line per minute from a detached thread (dies with the process), projected +/// through `render_heartbeat_line_mirror` — identity-first, human units, subject from the +/// HeartbeatFeed. The raw floor-memory byte dump (minute counter + raw current/swap +/// integers) is deleted (census negative example). Sampling denominator = the binding-high +/// dir when set (the slot slice that throttles), else the binding-cap dir, else the leaf +/// (whole-machine regimes); absence of all three refuses loudly and the floor proceeds +/// unmonitored — never a fabricated zero. fn spawn_floor_memory_heartbeat() { let high_dir = binding_high_cgroup_dir(); let cap_dir = binding_cap_cgroup_dir(); @@ -1368,24 +1461,36 @@ fn spawn_floor_memory_heartbeat() { ); return; }; + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); let spawned = std::thread::Builder::new() .name("floor-memory-heartbeat".into()) .spawn(move || { - let mut minute: u64 = 0; + let started = Instant::now(); loop { std::thread::sleep(std::time::Duration::from_secs(60)); - minute += 1; - let psi = read_cgroup_raw(&dir, "memory.pressure") + // Skip until a batch has armed the feed with a real entry_total — + // never a fabricated 0-of-0 during prelude / roster assembly. + let Some(feed) = heartbeat_feed_snapshot() else { + continue; + }; + let rss = read_cgroup_u64(&dir, "memory.current"); + let swap = read_cgroup_u64(&dir, "memory.swap.current"); + let pressure = read_cgroup_raw(&dir, "memory.pressure") .and_then(|c| memory_pressure_some_avg10(&c)) - .unwrap_or_else(|| "unreadable".into()); - let high_events = read_cgroup_raw(&dir, "memory.events") - .and_then(|c| memory_events_field(&c, "high")); - eprintln!( - "[floor-memory] t={minute}m current={} swap={} high_events={} psi_some_avg10={psi}", - heartbeat_field(read_cgroup_u64(&dir, "memory.current")), - heartbeat_field(read_cgroup_u64(&dir, "memory.swap.current")), - heartbeat_field(high_events), + .as_deref() + .and_then(psi_avg10_to_basis_points); + let elapsed_ms = started.elapsed().as_millis() as u64; + let line = render_heartbeat_line_mirror( + elapsed_ms, + &feed.batch_label, + feed.entry_done, + feed.entry_total, + rss, + swap, + pressure, + emoji, ); + eprintln!("{line}"); } }); if let Err(e) = spawned { @@ -1976,6 +2081,21 @@ fn run_walk( for (bi, batch) in batches.iter().enumerate() { batches_run = bi + 1; let units = group_batch_units(batch); + // Arm the observation heartbeat feed at batch-enter: discovery leaves + // entry_total pending (filled when the roster's entry-group count is known); + // SingleClaim arms immediately with the claim count. Never a fabricated 0-of-0. + let label = batch_heartbeat_label(batch); + let entry_total = if batch + .iter() + .any(|r| matches!(r, Runnable::DiscoveryBatch { .. })) + { + None + } else if batch.is_empty() { + None + } else { + Some(batch.len() as u64) + }; + heartbeat_feed_enter_batch(bi as u64, &label, entry_total); eprintln!( "claim_executor: batch {} — {} node(s) in {} resolve-group(s), governor target_width={}", bi + 1, @@ -2074,6 +2194,15 @@ fn run_walk( v1_compiler::v1_interpreter::group_end(); } for result in &batch_results { + // SingleClaim path: discovery advances the feed via + // `index_schedule_entry_completed`; gate batches have no schedule + // retention, so each claim result is the per-entry completed tick. + if !batch + .iter() + .any(|r| matches!(r, Runnable::DiscoveryBatch { .. })) + { + heartbeat_feed_entry_completed(); + } if result.ok { println!( "{}", @@ -3073,8 +3202,9 @@ mod tests { // seed_heartbeat_line takes the primitives the heartbeat thread has (elapsed, // batch label, entry position, memory vitals) and projects them through the one // renderer — identity first, human units, no raw byte dump. These golden strings - // are the oracle the Rust mirror is proven byte-equal to in the next commit; the - // subject is batch-grain (parallel entries → no fabricated per-module detail). + // are the oracle the Rust mirror is proven byte-equal to + // (`render_heartbeat_line_mirror_matches_seed_oracle`); the subject is batch-grain + // (parallel entries → no fabricated per-module detail). #[test] fn seed_heartbeat_line_renders_identity_first_in_human_units() { let root = workspace_root(); @@ -3124,6 +3254,84 @@ mod tests { ); } + // Wiring flip 4b: the Rust mirror is proven byte-equal to the 4a seed oracle. + // The heartbeat thread cannot call the interpreter (duplicate module index under + // the memory envelope it watches); this pin is what keeps the mirror honest. + #[test] + fn render_heartbeat_line_mirror_matches_seed_oracle() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + let crawl = run_seed_heartbeat_line( + &roots, + 1_980_000, + "witness discovery", + 214, + 602, + Some(16_107_200_512), + Some(34_359_738_368), + Some(901), + true, + ) + .expect("seed oracle"); + let mirror = render_heartbeat_line_mirror( + 1_980_000, + "witness discovery", + 214, + 602, + Some(16_107_200_512), + Some(34_359_738_368), + Some(901), + true, + ); + assert_eq!(mirror, crawl, "mirror must be byte-equal to the seed oracle"); + assert_eq!( + mirror, + "🕐 33 minutes in — still in witness discovery: entry 214 of 602. memory 15.0 GiB, swap 32.0 GiB, pressure 9.0%" + ); + + let unreadable_oracle = run_seed_heartbeat_line( + &roots, + 500, + "self-host fixed-point", + 0, + 2, + None, + Some(0), + None, + true, + ) + .expect("seed oracle"); + let unreadable_mirror = render_heartbeat_line_mirror( + 500, + "self-host fixed-point", + 0, + 2, + None, + Some(0), + None, + true, + ); + assert_eq!( + unreadable_mirror, unreadable_oracle, + "unreadable fields must stay byte-equal" + ); + assert!( + !mirror.contains("16107200512") && !unreadable_mirror.contains("[floor-memory]"), + "mirror must not carry the deleted byte-dump shape" + ); + } + + #[test] + fn psi_avg10_converts_to_basis_points_at_observation_scale() { + assert_eq!(psi_avg10_to_basis_points("9.01"), Some(901)); + assert_eq!(psi_avg10_to_basis_points("37.5"), Some(3750)); + assert_eq!(psi_avg10_to_basis_points("0.0"), Some(0)); + assert_eq!(psi_avg10_to_basis_points("garbage"), None); + } + // The materialization-receipt chain by execution: a real entry resolves, a // claim evaluates on its InterpContext, and the ctx Drop absorbs ledger // totals into the process accumulator. The env latch is process-global and diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 047878a578b..3e4569ef84d 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell}; use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque}; use std::path::{Path, PathBuf}; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, OnceLock, RwLock}; use crate::coproduct_reflection::{decl_facts_corpus_walk, DeclFactRaw}; @@ -6373,6 +6373,100 @@ fn index_record_schedule_module( } } +/// Snapshot of the floor's active-batch progress, sampled by the detached +/// floor-memory heartbeat thread. Armed only when `entry_total` is known and +/// non-zero — never a fabricated 0-of-0 (observation law 2 / §5). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HeartbeatFeedSnapshot { + pub batch_index: u64, + pub batch_label: String, + pub entry_done: u64, + pub entry_total: u64, +} + +struct HeartbeatFeedState { + batch_index: u64, + batch_label: String, + /// `None` while a discovery batch has entered but its roster entry-count is + /// not yet known — the heartbeat thread skips emit until this is `Some(n>0)`. + entry_total: Option, + entry_done: AtomicU64, +} + +/// Process-wide feed the floor-memory heartbeat samples. Updated at batch-enter +/// (label + total when known) and at each `index_schedule_entry_completed` (and +/// SingleClaim completion) — the existing per-entry point, never a parallel counter. +static HEARTBEAT_FEED: Mutex> = Mutex::new(None); + +/// Enter a floor batch. `entry_total = Some(n)` arms the feed when `n > 0`; +/// `None` leaves it pending (discovery: total filled once the roster is known). +/// Resets `entry_done` to 0. A zero total is refused (§5: never fabricate 0-of-0). +pub fn heartbeat_feed_enter_batch(batch_index: u64, label: &str, entry_total: Option) { + let total = match entry_total { + Some(0) => None, + other => other, + }; + let mut g = HEARTBEAT_FEED + .lock() + .unwrap_or_else(|p| p.into_inner()); + *g = Some(HeartbeatFeedState { + batch_index, + batch_label: label.to_string(), + entry_total: total, + entry_done: AtomicU64::new(0), + }); +} + +/// Fill the entry total once a discovery roster's entry-group count is known. +/// No-op when `total == 0` (refuses to arm a 0-of-0). No-op when no batch is open. +pub fn heartbeat_feed_set_entry_total(total: u64) { + if total == 0 { + return; + } + let mut g = HEARTBEAT_FEED + .lock() + .unwrap_or_else(|p| p.into_inner()); + if let Some(state) = g.as_mut() { + state.entry_total = Some(total); + } +} + +/// Record one completed entry (discovery source-file grain, or one SingleClaim). +/// Caps at `entry_total` when known so a late double-complete cannot invent +/// "entry N+1 of N". +pub fn heartbeat_feed_entry_completed() { + let g = HEARTBEAT_FEED + .lock() + .unwrap_or_else(|p| p.into_inner()); + if let Some(state) = g.as_ref() { + let prev = state.entry_done.fetch_add(1, Ordering::Relaxed); + if let Some(total) = state.entry_total { + if prev >= total { + // Undo the overshoot — saturating at total. + state.entry_done.store(total, Ordering::Relaxed); + } + } + } +} + +/// The armed snapshot, or `None` when no batch is open / total still pending. +/// The heartbeat thread skips emit on `None` rather than printing a fabricated +/// progress line. +pub fn heartbeat_feed_snapshot() -> Option { + let g = HEARTBEAT_FEED + .lock() + .unwrap_or_else(|p| p.into_inner()); + let state = g.as_ref()?; + let entry_total = state.entry_total?; + let entry_done = state.entry_done.load(Ordering::Relaxed).min(entry_total); + Some(HeartbeatFeedSnapshot { + batch_index: state.batch_index, + batch_label: state.batch_label.clone(), + entry_done, + entry_total, + }) +} + /// Drive one entry-completion: decrement the entry's closure refcounts, drop the /// per-module state that reached zero from every per-module cache (typed, parse, /// normalize-diag, ownership-diag, source-hash), AND drop the entry's assembled @@ -6391,9 +6485,15 @@ fn index_schedule_entry_completed( let mut slot = index.schedule_retention.borrow_mut(); match slot.as_mut() { Some(sr) => (sr.entry_completed(entry)?, sr.evict_enabled()), - None => return Ok(()), + None => { + // Schedule unarmed — the drain still advanced an entry; feed the heartbeat. + heartbeat_feed_entry_completed(); + return Ok(()); + } } }; + // The same per-entry point feeds the observation heartbeat — one counter, not a fork. + heartbeat_feed_entry_completed(); if !batch.typed_keys.is_empty() { let mut cache = index.typed_module_cache.borrow_mut(); for key in &batch.typed_keys { @@ -6456,6 +6556,44 @@ fn index_schedule_entry_completed( Ok(()) } +#[cfg(test)] +mod heartbeat_feed_red_controls { + use super::{ + heartbeat_feed_enter_batch, heartbeat_feed_entry_completed, heartbeat_feed_set_entry_total, + heartbeat_feed_snapshot, + }; + + #[test] + fn never_arms_a_fabricated_zero_of_zero() { + heartbeat_feed_enter_batch(0, "witness discovery", Some(0)); + assert_eq!( + heartbeat_feed_snapshot(), + None, + "entry_total=0 must not arm the feed" + ); + heartbeat_feed_enter_batch(0, "witness discovery", None); + assert_eq!( + heartbeat_feed_snapshot(), + None, + "pending total must not arm the feed" + ); + heartbeat_feed_set_entry_total(0); + assert_eq!( + heartbeat_feed_snapshot(), + None, + "set_entry_total(0) must refuse to arm" + ); + heartbeat_feed_set_entry_total(602); + let snap = heartbeat_feed_snapshot().expect("armed after real total"); + assert_eq!(snap.batch_label, "witness discovery"); + assert_eq!(snap.entry_done, 0); + assert_eq!(snap.entry_total, 602); + heartbeat_feed_entry_completed(); + let snap = heartbeat_feed_snapshot().expect("still armed"); + assert_eq!(snap.entry_done, 1); + } +} + #[cfg(test)] mod schedule_retention_red_controls { use super::{ScheduleRetention, SCHEDULE_RETENTION_TUNABLE_COUNT}; @@ -14930,6 +15068,12 @@ fn run_discovery_corpus_with_options_inner( // true (§6). One build covers the run; workers only read the process-wide memo. prime_witness_execution_legs(&index, rows.iter().map(|row| row.entry.as_str())); + // Arm the observation heartbeat's entry total at the same grain the drain walks + // (entry-groups), now that the roster is known — never a fabricated 0-of-0, and + // never earlier (batch-enter only had the opaque DiscoveryBatch runnable). + let discovery_entry_total = entry_row_groups(&rows).len() as u64; + heartbeat_feed_set_entry_total(discovery_entry_total); + let floor_color = floor_color_enabled(); let floor_stream = floor_stream_enabled(); return match width_policy { From 9cb507700c55d4e70e90f3e814fb026fdf90e92f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:00:16 +0000 Subject: [PATCH 23/39] cargo fmt: claim_executor + cli_run after floor-memory 4b / main merge CI build failed at the fmt --all --check gate (assert_eq! wrapping + HeartbeatFeed Mutex.lock() chain). No behavior change. Co-authored-by: Brian Searls --- src/v1/stage0/src/bin/claim_executor.rs | 5 ++++- src/v1/stage0/src/cli_run.rs | 16 ++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index a6e5ff7de38..517c4a95e8c 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3286,7 +3286,10 @@ mod tests { Some(901), true, ); - assert_eq!(mirror, crawl, "mirror must be byte-equal to the seed oracle"); + assert_eq!( + mirror, crawl, + "mirror must be byte-equal to the seed oracle" + ); assert_eq!( mirror, "🕐 33 minutes in — still in witness discovery: entry 214 of 602. memory 15.0 GiB, swap 32.0 GiB, pressure 9.0%" diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 39d3c258209..c7f66aca85b 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -6741,9 +6741,7 @@ pub fn heartbeat_feed_enter_batch(batch_index: u64, label: &str, entry_total: Op Some(0) => None, other => other, }; - let mut g = HEARTBEAT_FEED - .lock() - .unwrap_or_else(|p| p.into_inner()); + let mut g = HEARTBEAT_FEED.lock().unwrap_or_else(|p| p.into_inner()); *g = Some(HeartbeatFeedState { batch_index, batch_label: label.to_string(), @@ -6758,9 +6756,7 @@ pub fn heartbeat_feed_set_entry_total(total: u64) { if total == 0 { return; } - let mut g = HEARTBEAT_FEED - .lock() - .unwrap_or_else(|p| p.into_inner()); + let mut g = HEARTBEAT_FEED.lock().unwrap_or_else(|p| p.into_inner()); if let Some(state) = g.as_mut() { state.entry_total = Some(total); } @@ -6770,9 +6766,7 @@ pub fn heartbeat_feed_set_entry_total(total: u64) { /// Caps at `entry_total` when known so a late double-complete cannot invent /// "entry N+1 of N". pub fn heartbeat_feed_entry_completed() { - let g = HEARTBEAT_FEED - .lock() - .unwrap_or_else(|p| p.into_inner()); + let g = HEARTBEAT_FEED.lock().unwrap_or_else(|p| p.into_inner()); if let Some(state) = g.as_ref() { let prev = state.entry_done.fetch_add(1, Ordering::Relaxed); if let Some(total) = state.entry_total { @@ -6788,9 +6782,7 @@ pub fn heartbeat_feed_entry_completed() { /// The heartbeat thread skips emit on `None` rather than printing a fabricated /// progress line. pub fn heartbeat_feed_snapshot() -> Option { - let g = HEARTBEAT_FEED - .lock() - .unwrap_or_else(|p| p.into_inner()); + let g = HEARTBEAT_FEED.lock().unwrap_or_else(|p| p.into_inner()); let state = g.as_ref()?; let entry_total = state.entry_total?; let entry_done = state.entry_done.load(Ordering::Relaxed).min(entry_total); From d11f584b9d8e1cc0a14064b4a77a5c48ef69415c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:37:07 +0000 Subject: [PATCH 24/39] =?UTF-8?q?Wiring=20flip:=20[gantt]=20=E2=86=92=20Be?= =?UTF-8?q?gin/Concluded=20PhaseSegment=20observation=20projection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile-path trace_mark and GUNBC_FLOOR_GANTT emit through phase_begin_line / phase_concluded_line mirrors (byte-equal to the seed oracle; interpreter render from inside compile would recurse). Census row MigratedToObservation; raw t_ms/rss_mib shapes gone. Frontier 4→3. Verified via claim_batch on observation_emit_census_witnesses. Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 11 +- dag/gunbc/observation_seed_render.dag | 16 ++ .../observation_emit_census_witness_test.dag | 28 ++- src/v1/runtime_rust.dag | 133 +++++++++++--- src/v1/stage0/src/bin/claim_executor.rs | 169 +++++++++++++----- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 2 +- src/v1/stage0/src/v1_rt.rs | 103 +++++++++-- 7 files changed, 370 insertions(+), 92 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index c0a3226a47c..b0852e67d48 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. The named negative example — the [floor-memory] raw byte dump — has MIGRATED (wiring flip 4b): identity-first heartbeat via ci_heartbeat_line / render_heartbeat_line_mirror; the regime-disclosure line keeps the marker so the roster cannot go stale." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (wiring flip 4b — ci_heartbeat_line / render_heartbeat_line_mirror) and [gantt] (wiring flip — Begin/Concluded on PhaseSegment via phase_begin_line / phase_concluded_line; compile-path mirror render_phase_*_line_mirror). Each keeps its marker string so the roster cannot go stale." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -40,10 +40,9 @@ data typecheck_attribution_site: CensusedEmitSite = CensusedEmitSite { data gantt_site: CensusedEmitSite = CensusedEmitSite { marker: "[gantt]" as NonEmptyStr, - source_file: "src/v1/stage0/src/phase_profile.rs" as NonEmptyStr, - disposition: CountedFrontierSite { - reason: "compile phase boundaries (frontend/normalize/reconcile/emit begin/done) — already event-shaped, a Begin and a Concluded per phase, but emitted through a bespoke printer rather than the observation stream" as NonEmptyStr, - dissolve_on: "Begin/Concluded events on a phase-grain subject (PhaseSegment) rendered by the shared projection — the closest existing site to already being an event, migrates first after the rework PR" as NonEmptyStr + source_file: "src/v1/stage0/src/v1_rt.rs" as NonEmptyStr, + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_event_line (seed phase_begin_line / phase_concluded_line; mirror render_phase_begin_line_mirror / render_phase_concluded_line_mirror; PhaseSegment)" as NonEmptyStr } } @@ -82,7 +81,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: four tag families still frontier, one migrated (floor-memory), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." +data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: three tag families still frontier (typecheck-attribution, governor, measurement), two migrated (floor-memory, gantt), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 0db5eac846f..1e5bbf74498 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -12,6 +12,7 @@ import std.observation { MeasuredValue, MeasuredUnavailable, ObservationEvent, + Begin, Concluded, Done, AttentionBasis, @@ -34,6 +35,21 @@ fn seed_phase_subject(phase: NonEmptyStr) -> ObservationSubject { ObservationSubject { segments: [ PhaseSegment { name: phase } ] } } +data seed_phase_begin_note: String = "A phase Begin is the matched half of observation law 1 for phase-grain subjects. The floor prelude historically only printed Concluded (the mark fires when the phase ends); compile-path gantt marks already pair .begin/.done, so the seed boundary exposes both arms. Wall at Begin is unmeasured by construction — elapsed does not exist yet — and RSS stays on heartbeat/measurement (ci_event_line does not project event.rss). The compile-path seed renders through a Rust MIRROR of these fns rather than the interpreter: trace_mark runs inside compile itself, and calling back into the interpreter to render would recurse. The mirror is proven byte-equal to this oracle by the seed RED (same pattern as seed_heartbeat_line / render_heartbeat_line_mirror)." + +fn phase_begin_line(phase: NonEmptyStr, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_phase_subject(phase: phase), + transition: Begin, + wall: MeasuredUnavailable { cause: "begin has no elapsed yet" as NonEmptyStr }, + rss: MeasuredUnavailable { cause: "not sampled at a phase boundary" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + fn phase_concluded_line(phase: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { let event = ObservationEvent { subject: seed_phase_subject(phase: phase), diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 0dda93470d8..6ff3be7fa1b 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -14,11 +14,12 @@ import gunbc.observation_emit_census { emit_site_is_frontier, emit_site_dissolve_on, floor_memory_site, + gantt_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. The [floor-memory] named negative example has migrated: the raw byte dump is gone, the site is MigratedToObservation via ci_heartbeat_line, and the regime-disclosure line keeps the marker so the roster cannot go stale — the dump shape itself is asserted absent." +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." fn census_source(path: String) -> String { let r = filesystem_read(path: path) @@ -62,8 +63,16 @@ fn w_the_named_negative_example_has_migrated() -> Bool { string_contains(s: emit_site_dissolve_on(site: floor_memory_site), pattern: "ci_heartbeat_line") } +fn w_gantt_has_migrated() -> Bool { + census_marker_present(site: gantt_site) && + !emit_site_is_frontier(site: gantt_site) && + string_contains(s: gantt_site.marker, pattern: "gantt") && + string_contains(s: emit_site_dissolve_on(site: gantt_site), pattern: "ci_event_line") && + string_contains(s: emit_site_dissolve_on(site: gantt_site), pattern: "phase_begin_line") +} + fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { - observation_emit_frontier_count() == 4 && + observation_emit_frontier_count() == 3 && count(observation_emit_roster) == 5 } @@ -74,11 +83,24 @@ fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { string_contains(s: executor, pattern: "render_heartbeat_line_mirror") } +fn w_the_raw_gantt_shape_is_gone_from_the_seed() -> Bool { + let rt = census_source(path: "src/v1/stage0/src/v1_rt.rs") + let executor = census_source(path: "src/v1/stage0/src/bin/claim_executor.rs") + !string_contains(s: rt, pattern: "t_ms=") && + !string_contains(s: rt, pattern: "rss_mib=") && + string_contains(s: rt, pattern: "render_phase_begin_line_mirror") && + string_contains(s: rt, pattern: "render_phase_concluded_line_mirror") && + !string_contains(s: executor, pattern: "[gantt] claim_executor wall:") && + string_contains(s: executor, pattern: "render_phase_concluded_line_mirror") +} + test fn observation_emit_census_witnesses() -> Bool { w_every_rostered_marker_still_exists_in_the_seed() && w_roster_staleness_check_discriminates() && w_every_frontier_row_carries_a_real_dissolve_on() && w_the_named_negative_example_has_migrated() && + w_gantt_has_migrated() && w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && - w_the_raw_byte_dump_shape_is_gone_from_the_seed() + w_the_raw_byte_dump_shape_is_gone_from_the_seed() && + w_the_raw_gantt_shape_is_gone_from_the_seed() } diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 3a83e051369..99118e70386 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -443,7 +443,8 @@ fn rt_filesystem() -> String { fn rt_hash_ops() -> String { concat( "const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;\n", - "const FNV1A64_PRIME: u64 = 0x100000001b3;\n\n", + "const FNV1A64_PRIME: u64 = 0x100000001b3;\n", + "\n", "fn fnv1a64(bytes: &[u8]) -> u64 {\n", " let mut hash = FNV1A64_OFFSET;\n", " for b in bytes {\n", @@ -451,20 +452,69 @@ fn rt_hash_ops() -> String { " hash = hash.wrapping_mul(FNV1A64_PRIME);\n", " }\n", " hash\n", - "}\n\n", - "pub type Hash = String;\n\n", - "const HASH_DIGEST_LEN: usize = 16;\n\n", + "}\n", + "\n", + "pub type Hash = String;\n", + "\n", + "const HASH_DIGEST_LEN: usize = 16;\n", + "\n", "pub fn is_hash_digest(s: &str) -> bool {\n", " s.len() == HASH_DIGEST_LEN && s.bytes().all(|b| b.is_ascii_hexdigit())\n", - "}\n\n", + "}\n", + "\n", "fn expect_hash_digest(s: &str, arg: &str) {\n", " if !is_hash_digest(s) {\n", " panic!(\"{} must be a 16-char hex Hash digest\", arg);\n", " }\n", - "}\n\n", + "}\n", + "\n", + "/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the\n", + "/// raw gantt key=value shape (process-absolute millis and rss-mib fields) dissolves into the observation projection.\n", + "#[allow(dead_code)]\n", + "pub const GANTT_CENSUS_MARKER: &str = \"[gantt]\";\n", + "\n", + "/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds`.\n", + "const OBS_MINUTE_SWITCH_SECONDS: u64 = 90;\n", + "\n", + "fn obs_human_duration(ms: u64) -> String {\n", + " if ms < 1_000 {\n", + " format!(\"{ms}ms\")\n", + " } else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 {\n", + " format!(\"{} seconds\", ms / 1_000)\n", + " } else {\n", + " format!(\"{} minutes\", ms / 60_000)\n", + " }\n", + "}\n", + "\n", + "fn obs_phase_emoji() -> bool {\n", + " std::env::var(\"GITHUB_ACTIONS\").as_deref() == Ok(\"true\")\n", + "}\n", + "\n", + "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_begin_line` —\n", + "/// `ci_event_line(Begin) ∘ ci_render_line`. Justified divergence from the\n", + "/// interpreter seed boundary used by floor prelude marks: `trace_mark` runs\n", + "/// inside compile itself (hand-synced path and interpreter intrinsic), and\n", + "/// calling back into the interpreter to render would recurse. Proven\n", + "/// byte-equal to the `.dag` oracle by the seed RED.\n", + "pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String {\n", + " let glyph = if emoji { \"🔄\" } else { \"◐\" };\n", + " format!(\"{glyph} started {phase}\")\n", + "}\n", + "\n", + "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_concluded_line` —\n", + "/// `ci_event_line(Concluded{Done}) ∘ ci_render_line`. Same justified-divergence\n", + "/// note as `render_phase_begin_line_mirror`.\n", + "pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String {\n", + " let glyph = if emoji { \"✅\" } else { \"✓\" };\n", + " format!(\"{glyph} {phase} done in {}\", obs_human_duration(elapsed_ms))\n", + "}\n", + "\n", "/// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep\n", - "/// CostAccount (std.realization_measurement). One stderr line per mark; consecutive marks\n", - "/// define the segments of a natural Gantt read directly off any run log.\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", + "/// Concluded (matched `.begin`→`.done`, else delta since last mark) — never the old\n", + "/// process-absolute millis field. RSS stays on heartbeat/measurement (`ci_event_line` does\n", + "/// not project `event.rss` — explicit divergence from the deleted rss-mib field).\n", "///\n", "/// **Dissolution trigger (DESIGN §6):** delete this fn, the `trace_mark` registry row in\n", "/// `04_method.dag` (+ hand-synced twin `v1_compiler_infer_method.rs`), the nine\n", @@ -476,26 +526,56 @@ fn rt_hash_ops() -> String { "/// § dissolution). Receipt = that witness green with these marks deleted and stage walls\n", "/// still attributable from the model path.\n", "pub fn trace_mark(label: String) {\n", - " use std::sync::OnceLock;\n", + " use std::sync::{Mutex, OnceLock};\n", " use std::time::Instant;\n", " static TRACE_T0: OnceLock = OnceLock::new();\n", - " let ms = TRACE_T0.get_or_init(Instant::now).elapsed().as_millis();\n", - " let mut rss_mib = String::from(\"absent\");\n", - " if let Ok(status) = std::fs::read_to_string(\"/proc/self/status\") {\n", - " for line in status.lines() {\n", - " if let Some(rest) = line.strip_prefix(\"VmRSS:\") {\n", - " if let Some(kib) = rest\n", - " .split_whitespace()\n", - " .next()\n", - " .and_then(|k| k.parse::().ok())\n", - " {\n", - " rss_mib = (kib / 1024).to_string();\n", + " static LAST_MARK: OnceLock> = OnceLock::new();\n", + " static OPENS: OnceLock>> = 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 opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new()));\n", + " let emoji = obs_phase_emoji();\n", + " let now = Instant::now();\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", + " }\n", + " if let Ok(mut l) = last.lock() {\n", + " *l = 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", + " }\n", + " eprintln!(\n", + " \"{}\",\n", + " render_phase_concluded_line_mirror(&label, elapsed_ms, emoji)\n", + " );\n", " }\n", - " eprintln!(\"[gantt] {} t_ms={} rss_mib={}\", label, ms, rss_mib);\n", - "}\n\n", + "}\n", + "\n", "/// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash`\n", "/// is the `String` projection of this. Use this directly for arbitrary binary content\n", "/// (e.g. an executable or serialized payload): routing bytes through `String`/\n", @@ -503,10 +583,12 @@ fn rt_hash_ops() -> String { "/// byte sequences would hash equal — a §5 silent-collision fail-open for content-addressing.\n", "pub fn bytes_identity_hash(bytes: &[u8]) -> Hash {\n", " format!(\"{:016x}\", fnv1a64(bytes))\n", - "}\n\n", + "}\n", + "\n", "pub fn atom_identity_hash(s: String) -> Hash {\n", " bytes_identity_hash(s.as_bytes())\n", - "}\n\n", + "}\n", + "\n", "pub fn hash_combine(a: Hash, b: Hash) -> Hash {\n", " expect_hash_digest(&a, \"a\");\n", " expect_hash_digest(&b, \"b\");\n", @@ -514,7 +596,8 @@ fn rt_hash_ops() -> String { " bytes.push(0);\n", " bytes.extend_from_slice(b.as_bytes());\n", " format!(\"{:016x}\", fnv1a64(&bytes))\n", - "}\n\n") + "}\n", + "\n") } fn rt_resolution_silent_pick_telemetry() -> String { diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 517c4a95e8c..ca1f03815fe 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1915,7 +1915,9 @@ fn write_materialization_receipt() -> bool { true } -/// Emit a fractal Gantt tree to stderr when GUNBC_FLOOR_GANTT=1. +/// Emit a fractal post-walk tree to stderr when GUNBC_FLOOR_GANTT=1. +/// Wired (gantt flip): each row is a PhaseSegment Concluded projection via the +/// observation mirror — the raw `[gantt] … wall: {}ms` key=value tree is gone. fn emit_gantt(batch_records: &[BatchRecord], total_wall_nanos: u128) { let gantt_enabled = std::env::var("GUNBC_FLOOR_GANTT") .map(|v| v == "1") @@ -1923,64 +1925,72 @@ fn emit_gantt(batch_records: &[BatchRecord], total_wall_nanos: u128) { if !gantt_enabled { return; } - let total_ms = total_wall_nanos / 1_000_000; - eprintln!("[gantt] claim_executor wall: {}ms", total_ms); + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); + let total_ms = (total_wall_nanos / 1_000_000) as u64; + eprintln!( + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror("claim_executor", total_ms, emoji) + ); for rec in batch_records { - let batch_ms = rec.wall_nanos / 1_000_000; - let pct = if total_ms == 0 { - 0.0 - } else { - 100.0 * batch_ms as f64 / total_ms as f64 - }; + let batch_ms = (rec.wall_nanos / 1_000_000) as u64; + let batch_label = format!("batch {}", rec.batch_index + 1); eprintln!( - "[gantt] batch {} wall: {}ms ({:.1}%)", - rec.batch_index + 1, - batch_ms, - pct, + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror(&batch_label, batch_ms, emoji) ); for result in &rec.results { - let batch_pct = |ns: u128| -> f64 { - if rec.wall_nanos == 0 { - 0.0 - } else { - 100.0 * ns as f64 / rec.wall_nanos as f64 - } - }; if result.corpus_witnesses > 0 { - // Discovery batch: show serial-sum breakdown. - let corpus_resolve_ms = result.corpus_resolve_nanos / 1_000_000; - let corpus_eval_ms = result.corpus_eval_nanos / 1_000_000; - eprintln!( - "[gantt] {} ({} witnesses)", + let corpus_resolve_ms = (result.corpus_resolve_nanos / 1_000_000) as u64; + let corpus_eval_ms = (result.corpus_eval_nanos / 1_000_000) as u64; + let name = format!( + "{} ({} witnesses)", result.function, result.corpus_witnesses ); eprintln!( - "[gantt] resolve (serial sum): {}ms ({:.1}% of batch wall)", - corpus_resolve_ms, - batch_pct(result.corpus_resolve_nanos), + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror( + &name, + corpus_resolve_ms + corpus_eval_ms, + emoji + ) + ); + eprintln!( + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror( + &format!("{}.resolve", result.function), + corpus_resolve_ms, + emoji + ) ); eprintln!( - "[gantt] eval (serial sum): {}ms ({:.1}% of batch wall)", - corpus_eval_ms, - batch_pct(result.corpus_eval_nanos), + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror( + &format!("{}.eval", result.function), + corpus_eval_ms, + emoji + ) ); } else { - // Single claim: show resolve (if charged) + eval. if result.resolve_nanos > 0 { - let resolve_ms = result.resolve_nanos / 1_000_000; + let resolve_ms = (result.resolve_nanos / 1_000_000) as u64; eprintln!( - "[gantt] resolve (entry): {}ms ({:.1}% of batch wall)", - resolve_ms, - batch_pct(result.resolve_nanos), + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror( + "resolve (entry)", + resolve_ms, + emoji + ) ); } - let wall_ms = result.wall_nanos / 1_000_000; - let ok = if result.ok { "PASS" } else { "FAIL" }; + let wall_ms = (result.wall_nanos / 1_000_000) as u64; + let label = if result.ok { + result.function.clone() + } else { + format!("{} [FAIL]", result.function) + }; eprintln!( - "[gantt] {}: {}ms [{ok}] ({:.1}% of batch wall)", - result.function, - wall_ms, - batch_pct(result.wall_nanos), + "{}", + v1_compiler::v1_rt::render_phase_concluded_line_mirror(&label, wall_ms, emoji) ); } } @@ -3137,6 +3147,81 @@ mod tests { ); } + fn run_seed_phase_begin_line( + source_roots: &[String], + phase: &str, + overhead_ms: u64, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "phase_begin_line", + &[ + (Some("phase".to_string()), Value::Str(phase.to_string())), + ( + Some("overhead_ms".to_string()), + Value::Int(overhead_ms as i64), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + // Gantt flip byte-oracle: compile-path mirrors of phase_begin_line / + // phase_concluded_line must stay byte-equal to the .dag seed (justified + // divergence — interpreter render from inside compile would recurse). + #[test] + fn gantt_phase_mirrors_match_seed_oracle() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + let begin_oracle = run_seed_phase_begin_line(&roots, "compile.frontend", 300_000, true) + .expect("phase_begin_line must resolve and render"); + let begin_mirror = + v1_compiler::v1_rt::render_phase_begin_line_mirror("compile.frontend", true); + assert_eq!( + begin_oracle, begin_mirror, + "begin mirror must be byte-equal to seed oracle" + ); + assert!( + begin_oracle.starts_with('🔄') && begin_oracle.contains("started compile.frontend"), + "begin line shape: {begin_oracle:?}" + ); + + let done_oracle = + render_phase_concluded_line(&roots, "compile.frontend", 12_000, 300_000, true) + .expect("phase_concluded_line must resolve and render"); + let done_mirror = v1_compiler::v1_rt::render_phase_concluded_line_mirror( + "compile.frontend", + 12_000, + true, + ); + assert_eq!( + done_oracle, done_mirror, + "concluded mirror must be byte-equal to seed oracle" + ); + assert!( + done_oracle.contains("compile.frontend done in 12 seconds"), + "concluded line shape: {done_oracle:?}" + ); + } + #[allow(clippy::too_many_arguments)] fn run_seed_heartbeat_line( source_roots: &[String], diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index edfa1c015f4..518afb4fd15 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -92,7 +92,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("const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;\n".to_string(), "const FNV1A64_PRIME: u64 = 0x100000001b3;\n\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\n".to_string()), "pub type Hash = String;\n\n".to_string()), "const HASH_DIGEST_LEN: usize = 16;\n\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\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\n".to_string()), "/// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep\n".to_string()), "/// CostAccount (std.realization_measurement). One stderr line per mark; consecutive marks\n".to_string()), "/// define the segments of a natural Gantt read directly off any run log.\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 `docs/plans/ci-floor-fractal-gantt.md`\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::OnceLock;\n".to_string()), " use std::time::Instant;\n".to_string()), " static TRACE_T0: OnceLock = OnceLock::new();\n".to_string()), " let ms = TRACE_T0.get_or_init(Instant::now).elapsed().as_millis();\n".to_string()), " let mut rss_mib = String::from(\"absent\");\n".to_string()), " if let Ok(status) = std::fs::read_to_string(\"/proc/self/status\") {\n".to_string()), " for line in status.lines() {\n".to_string()), " if let Some(rest) = line.strip_prefix(\"VmRSS:\") {\n".to_string()), " if let Some(kib) = rest\n".to_string()), " .split_whitespace()\n".to_string()), " .next()\n".to_string()), " .and_then(|k| k.parse::().ok())\n".to_string()), " {\n".to_string()), " rss_mib = (kib / 1024).to_string();\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " eprintln!(\"[gantt] {} t_ms={} rss_mib={}\", label, ms, rss_mib);\n".to_string()), "}\n\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\n".to_string()), "pub fn atom_identity_hash(s: String) -> Hash {\n".to_string()), " bytes_identity_hash(s.as_bytes())\n".to_string()), "}\n\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\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("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()), "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()), "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 `docs/plans/ci-floor-fractal-gantt.md`\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()) } pub fn rt_resolution_silent_pick_telemetry() -> String { diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index 09cdcaee9dc..0b440595301 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -760,9 +760,53 @@ fn expect_hash_digest(s: &str, arg: &str) { } } +/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the +/// raw gantt key=value shape (process-absolute millis and rss-mib fields) dissolves into the observation projection. +#[allow(dead_code)] +pub const GANTT_CENSUS_MARKER: &str = "[gantt]"; + +/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds`. +const OBS_MINUTE_SWITCH_SECONDS: u64 = 90; + +fn obs_human_duration(ms: u64) -> String { + if ms < 1_000 { + format!("{ms}ms") + } else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 { + format!("{} seconds", ms / 1_000) + } else { + format!("{} minutes", ms / 60_000) + } +} + +fn obs_phase_emoji() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + +/// Pure Rust mirror of `gunbc.observation_seed_render.phase_begin_line` — +/// `ci_event_line(Begin) ∘ ci_render_line`. Justified divergence from the +/// interpreter seed boundary used by floor prelude marks: `trace_mark` runs +/// inside compile itself (hand-synced path and interpreter intrinsic), and +/// calling back into the interpreter to render would recurse. Proven +/// byte-equal to the `.dag` oracle by the seed RED. +pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String { + let glyph = if emoji { "🔄" } else { "◐" }; + format!("{glyph} started {phase}") +} + +/// Pure Rust mirror of `gunbc.observation_seed_render.phase_concluded_line` — +/// `ci_event_line(Concluded{Done}) ∘ ci_render_line`. Same justified-divergence +/// note as `render_phase_begin_line_mirror`. +pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String { + let glyph = if emoji { "✅" } else { "✓" }; + format!("{glyph} {phase} done in {}", obs_human_duration(elapsed_ms)) +} + /// Stage-boundary trace mark — the v1-seed interim realization of the v2 per-RealizedStep -/// CostAccount (std.realization_measurement). One stderr line per mark; consecutive marks -/// define the segments of a natural Gantt read directly off any run log. +/// CostAccount (std.realization_measurement). Wired (gantt flip): projects Begin/Concluded +/// on a PhaseSegment via the observation mirrors of `ci_event_line`. Segment wall on +/// Concluded (matched `.begin`→`.done`, else delta since last mark) — never the old +/// process-absolute millis field. RSS stays on heartbeat/measurement (`ci_event_line` does +/// not project `event.rss` — explicit divergence from the deleted rss-mib field). /// /// **Dissolution trigger (DESIGN §6):** delete this fn, the `trace_mark` registry row in /// `04_method.dag` (+ hand-synced twin `v1_compiler_infer_method.rs`), the nine @@ -774,25 +818,54 @@ fn expect_hash_digest(s: &str, arg: &str) { /// § dissolution). Receipt = that witness green with these marks deleted and stage walls /// still attributable from the model path. pub fn trace_mark(label: String) { - use std::sync::OnceLock; + use std::sync::{Mutex, OnceLock}; use std::time::Instant; static TRACE_T0: OnceLock = OnceLock::new(); - let ms = TRACE_T0.get_or_init(Instant::now).elapsed().as_millis(); - let mut rss_mib = String::from("absent"); - if let Ok(status) = std::fs::read_to_string("/proc/self/status") { - for line in status.lines() { - if let Some(rest) = line.strip_prefix("VmRSS:") { - if let Some(kib) = rest - .split_whitespace() - .next() - .and_then(|k| k.parse::().ok()) - { - rss_mib = (kib / 1024).to_string(); + 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 opens = OPENS.get_or_init(|| Mutex::new(std::collections::HashMap::new())); + let emoji = obs_phase_emoji(); + let now = Instant::now(); + 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); + } + if let Ok(mut l) = last.lock() { + *l = 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; } + eprintln!( + "{}", + render_phase_concluded_line_mirror(&label, elapsed_ms, emoji) + ); } - eprintln!("[gantt] {} t_ms={} rss_mib={}", label, ms, rss_mib); } /// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash` From d4b1bed3116fa76c5d95217aa6fa84e88bc26156 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:45:47 +0000 Subject: [PATCH 25/39] =?UTF-8?q?Wiring=20flip:=20[governor]=20=E2=86=92?= =?UTF-8?q?=20ci=5Fhold=5Fcause=5Ftext=20/=20StatusBlocked=20observation?= =?UTF-8?q?=20projection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HoldReason emits through seed_governor_hold_line (mirror render_governor_hold_line_mirror); hard/creep/receipt/startup lines lose the raw [governor] key=value shape. Census MigratedToObservation; frontier 3→2. Mirror↔oracle byte equality for PsiPressure and CurrentHighWater. Verified via claim_batch + memory_governor unit tests. Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 9 +- dag/gunbc/observation_seed_render.dag | 45 +++- .../observation_emit_census_witness_test.dag | 27 ++- src/v1/stage0/src/bin/claim_executor.rs | 114 ++++++++++ src/v1/stage0/src/memory_governor.rs | 197 +++++++++++++----- 5 files changed, 329 insertions(+), 63 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index b0852e67d48..c5efb0d6322 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (wiring flip 4b — ci_heartbeat_line / render_heartbeat_line_mirror) and [gantt] (wiring flip — Begin/Concluded on PhaseSegment via phase_begin_line / phase_concluded_line; compile-path mirror render_phase_*_line_mirror). Each keeps its marker string so the roster cannot go stale." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (4b), [gantt] (Begin/Concluded PhaseSegment), [governor] (ci_hold_cause_text / seed_governor_hold_line / render_governor_hold_line_mirror). Each keeps its marker string so the roster cannot go stale." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -49,9 +49,8 @@ data gantt_site: CensusedEmitSite = CensusedEmitSite { data governor_site: CensusedEmitSite = CensusedEmitSite { marker: "[governor]" as NonEmptyStr, source_file: "src/v1/stage0/src/memory_governor.rs" as NonEmptyStr, - disposition: CountedFrontierSite { - reason: "AIMD admission holds and creep back-off — the governor state that the model's SchedulerHold and BlockedOn already mirror in lockstep, but the seed still prints its own HoldReason rather than emitting a BlockedOn event" as NonEmptyStr, - dissolve_on: "a BlockedOn event derived from the HoldReason the governor already computes (the lockstep coupling is proven; the emit side follows), rendered inline by ci/tty blocked-line — after the rework PR" as NonEmptyStr + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_hold_cause_text (seed seed_governor_hold_line; mirror render_governor_hold_line_mirror; SchedulerHold/BlockedOn)" as NonEmptyStr } } @@ -81,7 +80,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: three tag families still frontier (typecheck-attribution, governor, measurement), two migrated (floor-memory, gantt), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." +data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: two tag families still frontier (typecheck-attribution, measurement), three migrated (floor-memory, gantt, governor), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 1e5bbf74498..e82ca935d35 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -2,7 +2,7 @@ module gunbc.observation_seed_render import std.types { String, NonEmptyStr, Bool } import std.nat { Nat } -import std.symbols { Tier, Emoji, Unicode } +import std.symbols { Tier, Emoji, Unicode, StatusBlocked } import std.measure { millisecond, byte_size, basis_point } import std.disposition { Disposition, Terminal } import std.observation { @@ -16,8 +16,19 @@ import std.observation { Concluded, Done, AttentionBasis, + Anomaly, + SchedulerHold, + PsiPressure, + CurrentHighWater, +} +import gunbc.observation_ci_render { + ci_event_line, + ci_render_line, + ci_heartbeat_line, + ci_hold_cause_text, + HeartbeatSample, + RenderedLine, } -import gunbc.observation_ci_render { ci_event_line, ci_render_line, ci_heartbeat_line, HeartbeatSample } data observation_seed_render_note: String = "The seed to .dag render boundary. The v1 seed (claim_executor and its siblings) emits progress lines, but the FORMAT is a single authority in gunbc.observation_ci_render — a raw eprintln in the seed forks that format the way a hand-rolled duration string forks std.measure. This module is the boundary the seed calls across, exactly as cli_run.install_output_policy calls output_policy.resolve_channel_policy: primitive arguments in, a rendered line out, no format logic on the seed side. It constructs the ObservationEvent the seed's occurrence corresponds to (a phase concluding is a Concluded event on a PhaseSegment subject) and projects it through the existing renderer, so the seed cannot drift from the model. It holds no telemetry source and computes no facts of its own; every field it fills came in as an argument the seed already had (observation law 5)." @@ -95,6 +106,36 @@ fn seed_heartbeat_line( ) } +data seed_governor_hold_note: String = "Governor hold projection (wiring flip): HoldReason is already lockstep with SchedulerHold; the emit side now projects through ci_hold_cause_text rather than a bespoke [governor] printer. Attention is Anomaly (a hold is never Ambient). The seed renders through a Rust MIRROR in memory_governor — the governor is host physics without a source-root resolve context, same class as the heartbeat mirror. Oracle RED: seed_psi_hold_line / seed_high_water_hold_line byte-equal to render_governor_hold_line_mirror." + +fn seed_governor_hold_line(hold: SchedulerHold, emoji: Bool) -> String { + ci_render_line( + line: RenderedLine { + glyph: StatusBlocked, + text: ci_hold_cause_text(hold: hold), + attention: Anomaly + }, + tier: seed_tier(emoji: emoji) + ) +} + +fn seed_psi_hold_line(avg10_bp: Nat, emoji: Bool) -> String { + seed_governor_hold_line( + hold: PsiPressure { avg10: basis_point(count: avg10_bp) }, + emoji: emoji + ) +} + +fn seed_high_water_hold_line(current_bytes: Nat, high_water_bytes: Nat, emoji: Bool) -> String { + seed_governor_hold_line( + hold: CurrentHighWater { + current: byte_size(current_bytes), + high_water: byte_size(high_water_bytes) + }, + emoji: emoji + ) +} + data observation_seed_render_disposition: Disposition = Terminal { reason: "The seed to .dag render boundary is Terminal, not a scaffold: a boundary the seed calls across to keep the format single-authority is the mechanism, not an interim stand-in. What dissolves is each raw eprintln the seed replaces with a call here — counted by gunbc.observation_emit_census — and, further out, the seed itself as v2 realizes the witnesses natively; neither changes what this module models, which is that a seed occurrence is an observation event projected by the one renderer." } diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 6ff3be7fa1b..98b74007671 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -15,11 +15,12 @@ import gunbc.observation_emit_census { emit_site_dissolve_on, floor_memory_site, gantt_site, + governor_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt], [governor]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." fn census_source(path: String) -> String { let r = filesystem_read(path: path) @@ -71,8 +72,16 @@ fn w_gantt_has_migrated() -> Bool { string_contains(s: emit_site_dissolve_on(site: gantt_site), pattern: "phase_begin_line") } +fn w_governor_has_migrated() -> Bool { + census_marker_present(site: governor_site) && + !emit_site_is_frontier(site: governor_site) && + string_contains(s: governor_site.marker, pattern: "governor") && + string_contains(s: emit_site_dissolve_on(site: governor_site), pattern: "ci_hold_cause_text") && + string_contains(s: emit_site_dissolve_on(site: governor_site), pattern: "render_governor_hold_line_mirror") +} + fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { - observation_emit_frontier_count() == 3 && + observation_emit_frontier_count() == 2 && count(observation_emit_roster) == 5 } @@ -94,13 +103,25 @@ fn w_the_raw_gantt_shape_is_gone_from_the_seed() -> Bool { string_contains(s: executor, pattern: "render_phase_concluded_line_mirror") } +fn w_the_raw_governor_shape_is_gone_from_the_seed() -> Bool { + let gov = census_source(path: "src/v1/stage0/src/memory_governor.rs") + !string_contains(s: gov, pattern: "[governor] hard back-off:") && + !string_contains(s: gov, pattern: "[governor] creep back-off:") && + !string_contains(s: gov, pattern: "[governor] receipt:") && + !string_contains(s: gov, pattern: "[governor] adaptive width") && + string_contains(s: gov, pattern: "render_governor_hold_line_mirror") && + string_contains(s: gov, pattern: "GOVERNOR_CENSUS_MARKER") +} + test fn observation_emit_census_witnesses() -> Bool { w_every_rostered_marker_still_exists_in_the_seed() && w_roster_staleness_check_discriminates() && w_every_frontier_row_carries_a_real_dissolve_on() && w_the_named_negative_example_has_migrated() && w_gantt_has_migrated() && + w_governor_has_migrated() && w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && w_the_raw_byte_dump_shape_is_gone_from_the_seed() && - w_the_raw_gantt_shape_is_gone_from_the_seed() + w_the_raw_gantt_shape_is_gone_from_the_seed() && + w_the_raw_governor_shape_is_gone_from_the_seed() } diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index ca1f03815fe..c36d83b1ae7 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3222,6 +3222,120 @@ mod tests { ); } + fn run_seed_psi_hold_line( + source_roots: &[String], + avg10_bp: u64, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "seed_psi_hold_line", + &[ + (Some("avg10_bp".to_string()), Value::Int(avg10_bp as i64)), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + fn run_seed_high_water_hold_line( + source_roots: &[String], + current_bytes: u64, + high_water_bytes: u64, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "seed_high_water_hold_line", + &[ + ( + Some("current_bytes".to_string()), + Value::Int(current_bytes as i64), + ), + ( + Some("high_water_bytes".to_string()), + Value::Int(high_water_bytes as i64), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + // Governor flip byte-oracle: HoldReason → ci_hold_cause_text mirrors must stay + // byte-equal to seed_psi_hold_line / seed_high_water_hold_line. + #[test] + fn governor_hold_mirrors_match_seed_oracle() { + use v1_compiler::memory_governor::{ + render_governor_hold_line_mirror, HoldReason, + }; + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + // 37.5% → 3750 basis points + let psi_oracle = run_seed_psi_hold_line(&roots, 3750, true) + .expect("seed_psi_hold_line must resolve and render"); + let psi_mirror = render_governor_hold_line_mirror( + &HoldReason::PsiPressure { avg10: 37.5 }, + true, + ); + assert_eq!( + psi_oracle, psi_mirror, + "psi hold mirror must be byte-equal to seed oracle" + ); + assert!( + psi_oracle.starts_with('⏳') && psi_oracle.contains("blocked on memory reclaim"), + "psi hold shape: {psi_oracle:?}" + ); + + let hw_oracle = + run_seed_high_water_hold_line(&roots, 8_589_934_592, 10_737_418_240, true) + .expect("seed_high_water_hold_line must resolve and render"); + let hw_mirror = render_governor_hold_line_mirror( + &HoldReason::CurrentHighWater { + current: 8_589_934_592, + high_water: 10_737_418_240, + }, + true, + ); + assert_eq!( + hw_oracle, hw_mirror, + "high-water hold mirror must be byte-equal to seed oracle" + ); + assert!( + hw_oracle.contains("blocked on the memory high-water line"), + "high-water hold shape: {hw_oracle:?}" + ); + } + #[allow(clippy::too_many_arguments)] fn run_seed_heartbeat_line( source_roots: &[String], diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 372f4f80b45..989ab8d19b6 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -45,6 +45,38 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; +/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the +/// raw `[governor]` key=value shapes dissolve into the observation projection. +#[allow(dead_code)] +pub const GOVERNOR_CENSUS_MARKER: &str = "[governor]"; + +fn governor_emoji() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + +fn mirror_ci_tenths_text(tenths: u64) -> String { + format!("{}.{}", tenths / 10, tenths % 10) +} + +fn mirror_ci_human_bytes(bytes: u64) -> String { + let tenths = (bytes.saturating_mul(10)) / 1_073_741_824; + format!("{} GiB", mirror_ci_tenths_text(tenths)) +} + +fn mirror_ci_human_percent(bp: u64) -> String { + format!("{}%", mirror_ci_tenths_text(bp / 10)) +} + +fn render_governor_info_line(text: &str, emoji: bool) -> String { + let glyph = if emoji { "🔄" } else { "◐" }; + format!("{glyph} {text}") +} + +fn render_governor_done_line(text: &str, emoji: bool) -> String { + let glyph = if emoji { "✅" } else { "✓" }; + format!("{glyph} {text}") +} + /// Multiplicative-decrease divisor is 2 (halve), additive increase is +1 — classic AIMD. /// The remaining thresholds are POLICY (like TCP's), not measurements of any workload: /// they scale with the budget or are dimensionless, so no per-corpus constant returns. @@ -115,6 +147,8 @@ pub enum HoldReason { impl HoldReason { fn describe(&self) -> String { + // Internal diagnostic text retained for tests/debug; log projection goes through + // `render_governor_hold_line_mirror` (ci_hold_cause_text authority). match self { HoldReason::WindowFull { active, target } => { format!("window full (active={active} target={target})") @@ -144,6 +178,15 @@ impl HoldReason { ), } } + fn emit_hold_line(&self, old_target: usize, new_target: usize) -> String { + let _ = GOVERNOR_CENSUS_MARKER; + let base = render_governor_hold_line_mirror(self, governor_emoji()); + if old_target != new_target { + format!("{base} — target_width {old_target}→{new_target}") + } else { + base + } + } fn is_memory_creep(&self) -> bool { !matches!( self, @@ -155,6 +198,39 @@ impl HoldReason { } } +/// Mirror of `gunbc.observation_ci_render.ci_hold_cause_text` over the seed's HoldReason +/// (lockstep with SchedulerHold). Proven byte-equal to the seed oracle for the two +/// narrated arms (PsiPressure, CurrentHighWater); other variants share the model's +/// generic "blocked on scheduler admission" text. +pub fn mirror_ci_hold_cause_text(hold: &HoldReason) -> String { + match hold { + HoldReason::CurrentHighWater { + current, + high_water, + } => format!( + "blocked on the memory high-water line ({} of {})", + mirror_ci_human_bytes(*current), + mirror_ci_human_bytes(*high_water) + ), + HoldReason::PsiPressure { avg10 } => { + // avg10 is percent with one decimal; basis points = percent × 100. + let bp = (*avg10 * 100.0).round() as u64; + format!( + "blocked on memory reclaim (pressure {})", + mirror_ci_human_percent(bp) + ) + } + _ => "blocked on scheduler admission".to_string(), + } +} + +/// Mirror of `gunbc.observation_seed_render.seed_governor_hold_line` — +/// `ci_hold_cause_text ∘ StatusBlocked ∘ ci_render_line`. +pub fn render_governor_hold_line_mirror(hold: &HoldReason, emoji: bool) -> String { + let glyph = if emoji { "⏳" } else { "◷" }; + format!("{glyph} {}", mirror_ci_hold_cause_text(hold)) +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum AdmitDecision { /// A worker slot was granted. `forced_serial` marks the width-1 progress floor firing @@ -258,17 +334,16 @@ struct HardEvent { } impl HardEvent { - fn describe(&self) -> String { + fn emit_line(&self) -> String { + let _ = GOVERNOR_CENSUS_MARKER; + let glyph = if governor_emoji() { "⏳" } else { "◷" }; let cause = if self.budget_exceeded { - "memory.current exceeded the declared budget".to_string() + "blocked on the declared memory budget" } else { - format!( - "memory.events high +{} oom_kill +{}", - self.high_delta, self.oom_kill_delta - ) + "blocked on memory reclaim" }; format!( - "[governor] hard back-off: {cause} — target_width {}→{} (workers drain between units)", + "{glyph} {cause} — target_width {}→{}", self.old_target, self.new_target ) } @@ -665,18 +740,25 @@ impl MemoryGovernor { budget_source: source_label, max_width: max_width.max(1), }; + let _ = GOVERNOR_CENSUS_MARKER; eprintln!( - "[governor] adaptive width (AIMD): budget={} source={} max_width={} sensors={}", - limits - .budget_bytes - .map(|b| b.to_string()) - .unwrap_or_else(|| "unknown".into()), - limits.budget_source, - limits.max_width, - sensor_dir - .as_ref() - .map(|d| d.display().to_string()) - .unwrap_or_else(|| "none (creep checks inert)".into()), + "{}", + render_governor_info_line( + &format!( + "governor adaptive width — budget={} source={} max_width={} sensors={}", + limits + .budget_bytes + .map(|b| b.to_string()) + .unwrap_or_else(|| "unknown".into()), + limits.budget_source, + limits.max_width, + sensor_dir + .as_ref() + .map(|d| d.display().to_string()) + .unwrap_or_else(|| "none (creep checks inert)".into()), + ), + governor_emoji() + ) ); MemoryGovernor { limits, @@ -695,16 +777,11 @@ impl MemoryGovernor { let new_target = core.target_width; drop(core); if let Some(h) = hard { - eprintln!("{}", h.describe()); + eprintln!("{}", h.emit_line()); } if let AdmitDecision::Hold(reason) = &decision { if reason.is_memory_creep() && !was_holding { - eprintln!( - "[governor] creep back-off: {} — target_width {}→{} (admissions held; workers drain between units)", - reason.describe(), - old_target, - new_target - ); + eprintln!("{}", reason.emit_hold_line(old_target, new_target)); } } if let AdmitDecision::Admit { @@ -712,7 +789,11 @@ impl MemoryGovernor { } = &decision { eprintln!( - "[governor] progress floor: signals hot but zero workers active — admitting one (counted forced_serial)" + "{}", + render_governor_info_line( + "governor progress floor — signals hot but zero workers active; admitting one (counted forced_serial)", + governor_emoji() + ) ); } decision @@ -749,12 +830,11 @@ impl MemoryGovernor { let (hard, creep_backoff) = note_completion(&mut core, &sig, &self.limits, kind); drop(core); if let Some(h) = hard { - eprintln!("{}", h.describe()); + eprintln!("{}", h.emit_line()); } if let Some((old, new)) = creep_backoff { - eprintln!( - "[governor] creep back-off (observed at completion): target_width {old}→{new} (admissions held; workers drain between units)" - ); + let glyph = if governor_emoji() { "⏳" } else { "◷" }; + eprintln!("{glyph} blocked on scheduler admission — target_width {old}→{new}"); } } @@ -768,7 +848,14 @@ impl MemoryGovernor { if !had_share { if let Some(s) = share { eprintln!( - "[governor] measured worker share: {s} bytes (first slot's cost over the pool baseline) — headroom gate armed" + "{}", + render_governor_info_line( + &format!( + "governor measured worker share — {} (first slot cost over pool baseline); headroom gate armed", + mirror_ci_human_bytes(s) + ), + governor_emoji() + ) ); } } @@ -803,32 +890,36 @@ impl MemoryGovernor { /// The end-of-run receipt: the counted degradations (§5 — observable, prioritizable). pub fn receipt_line(&self) -> String { + let _ = GOVERNOR_CENSUS_MARKER; let core = self.core.lock().unwrap(); - format!( - "[governor] receipt: budget={} source={} max_width_reached={} admissions={} \ + render_governor_done_line( + &format!( + "governor receipt — budget={} source={} max_width_reached={} admissions={} \ width_growths={} creep_backoffs={} pacing_holds={} headroom_holds={} \ ceiling_holds={} hard_backoffs={} budget_exceeded={} forced_serial={} \ peak_current={}", - self.limits - .budget_bytes - .map(|b| b.to_string()) - .unwrap_or_else(|| "unknown".into()), - self.limits.budget_source, - core.max_width_reached, - core.admissions, - core.width_growths, - core.creep_backoffs, - core.pacing_holds, - core.headroom_holds, - core.ceiling_holds, - core.hard_backoffs, - core.budget_exceeded_backoffs, - core.forced_serial_admissions, - if core.peak_current_bytes == 0 { - "unreadable".to_string() - } else { - core.peak_current_bytes.to_string() - }, + self.limits + .budget_bytes + .map(|b| b.to_string()) + .unwrap_or_else(|| "unknown".into()), + self.limits.budget_source, + core.max_width_reached, + core.admissions, + core.width_growths, + core.creep_backoffs, + core.pacing_holds, + core.headroom_holds, + core.ceiling_holds, + core.hard_backoffs, + core.budget_exceeded_backoffs, + core.forced_serial_admissions, + if core.peak_current_bytes == 0 { + "unreadable".to_string() + } else { + core.peak_current_bytes.to_string() + }, + ), + governor_emoji(), ) } } From 5c478427baaf31c957c18532f53f2080cf5418b0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:46:00 +0000 Subject: [PATCH 26/39] cargo fmt: claim_executor after governor hold oracle RED Co-authored-by: Brian Searls --- src/v1/stage0/src/bin/claim_executor.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index c36d83b1ae7..041882fab39 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3292,9 +3292,7 @@ mod tests { // byte-equal to seed_psi_hold_line / seed_high_water_hold_line. #[test] fn governor_hold_mirrors_match_seed_oracle() { - use v1_compiler::memory_governor::{ - render_governor_hold_line_mirror, HoldReason, - }; + use v1_compiler::memory_governor::{render_governor_hold_line_mirror, HoldReason}; let root = workspace_root(); let roots = vec![ root.join("src/v2").to_string_lossy().into_owned(), @@ -3303,10 +3301,8 @@ mod tests { // 37.5% → 3750 basis points let psi_oracle = run_seed_psi_hold_line(&roots, 3750, true) .expect("seed_psi_hold_line must resolve and render"); - let psi_mirror = render_governor_hold_line_mirror( - &HoldReason::PsiPressure { avg10: 37.5 }, - true, - ); + let psi_mirror = + render_governor_hold_line_mirror(&HoldReason::PsiPressure { avg10: 37.5 }, true); assert_eq!( psi_oracle, psi_mirror, "psi hold mirror must be byte-equal to seed oracle" @@ -3316,9 +3312,8 @@ mod tests { "psi hold shape: {psi_oracle:?}" ); - let hw_oracle = - run_seed_high_water_hold_line(&roots, 8_589_934_592, 10_737_418_240, true) - .expect("seed_high_water_hold_line must resolve and render"); + let hw_oracle = run_seed_high_water_hold_line(&roots, 8_589_934_592, 10_737_418_240, true) + .expect("seed_high_water_hold_line must resolve and render"); let hw_mirror = render_governor_hold_line_mirror( &HoldReason::CurrentHighWater { current: 8_589_934_592, From 650f33df386e88c4473a39651abc6fec7edd8861 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:48:13 +0000 Subject: [PATCH 27/39] Fix runtime_rust.dag: escape Rust format braces for daglang parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit daglang treats {ident} inside string literals as interpolation; the gantt mirror's format!("{glyph}…") (and use std::sync::{Mutex,…}) panicked the regen self-compile. Escape as \{…\} so the emitted Rust keeps real braces. Emitter twin resynced; byte-equal to v1_rt.rs. Co-authored-by: Brian Searls --- src/v1/runtime_rust.dag | 127 ++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 64 deletions(-) diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 99118e70386..b533c290551 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -445,28 +445,28 @@ fn rt_hash_ops() -> String { "const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;\n", "const FNV1A64_PRIME: u64 = 0x100000001b3;\n", "\n", - "fn fnv1a64(bytes: &[u8]) -> u64 {\n", + "fn fnv1a64(bytes: &[u8]) -> u64 \{\n", " let mut hash = FNV1A64_OFFSET;\n", - " for b in bytes {\n", + " for b in bytes \{\n", " hash ^= *b as u64;\n", " hash = hash.wrapping_mul(FNV1A64_PRIME);\n", - " }\n", + " \}\n", " hash\n", - "}\n", + "\}\n", "\n", "pub type Hash = String;\n", "\n", "const HASH_DIGEST_LEN: usize = 16;\n", "\n", - "pub fn is_hash_digest(s: &str) -> bool {\n", + "pub fn is_hash_digest(s: &str) -> bool \{\n", " s.len() == HASH_DIGEST_LEN && s.bytes().all(|b| b.is_ascii_hexdigit())\n", - "}\n", + "\}\n", "\n", - "fn expect_hash_digest(s: &str, arg: &str) {\n", - " if !is_hash_digest(s) {\n", - " panic!(\"{} must be a 16-char hex Hash digest\", arg);\n", - " }\n", - "}\n", + "fn expect_hash_digest(s: &str, arg: &str) \{\n", + " if !is_hash_digest(s) \{\n", + " panic!(\"\{\} must be a 16-char hex Hash digest\", arg);\n", + " \}\n", + "\}\n", "\n", "/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the\n", "/// raw gantt key=value shape (process-absolute millis and rss-mib fields) dissolves into the observation projection.\n", @@ -476,19 +476,19 @@ fn rt_hash_ops() -> String { "/// Mirror of `gunbc.observation_ci_render.ci_minute_switch_seconds`.\n", "const OBS_MINUTE_SWITCH_SECONDS: u64 = 90;\n", "\n", - "fn obs_human_duration(ms: u64) -> String {\n", - " if ms < 1_000 {\n", - " format!(\"{ms}ms\")\n", - " } else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 {\n", - " format!(\"{} seconds\", ms / 1_000)\n", - " } else {\n", - " format!(\"{} minutes\", ms / 60_000)\n", - " }\n", - "}\n", + "fn obs_human_duration(ms: u64) -> String \{\n", + " if ms < 1_000 \{\n", + " format!(\"\{ms\}ms\")\n", + " \} else if ms < OBS_MINUTE_SWITCH_SECONDS * 1_000 \{\n", + " format!(\"\{\} seconds\", ms / 1_000)\n", + " \} else \{\n", + " format!(\"\{\} minutes\", ms / 60_000)\n", + " \}\n", + "\}\n", "\n", - "fn obs_phase_emoji() -> bool {\n", + "fn obs_phase_emoji() -> bool \{\n", " std::env::var(\"GITHUB_ACTIONS\").as_deref() == Ok(\"true\")\n", - "}\n", + "\}\n", "\n", "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_begin_line` —\n", "/// `ci_event_line(Begin) ∘ ci_render_line`. Justified divergence from the\n", @@ -496,18 +496,18 @@ fn rt_hash_ops() -> String { "/// inside compile itself (hand-synced path and interpreter intrinsic), and\n", "/// calling back into the interpreter to render would recurse. Proven\n", "/// byte-equal to the `.dag` oracle by the seed RED.\n", - "pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String {\n", - " let glyph = if emoji { \"🔄\" } else { \"◐\" };\n", - " format!(\"{glyph} started {phase}\")\n", - "}\n", + "pub fn render_phase_begin_line_mirror(phase: &str, emoji: bool) -> String \{\n", + " let glyph = if emoji \{ \"🔄\" \} else \{ \"◐\" \};\n", + " format!(\"\{glyph\} started \{phase\}\")\n", + "\}\n", "\n", "/// Pure Rust mirror of `gunbc.observation_seed_render.phase_concluded_line` —\n", - "/// `ci_event_line(Concluded{Done}) ∘ ci_render_line`. Same justified-divergence\n", + "/// `ci_event_line(Concluded\{Done\}) ∘ ci_render_line`. Same justified-divergence\n", "/// note as `render_phase_begin_line_mirror`.\n", - "pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String {\n", - " let glyph = if emoji { \"✅\" } else { \"✓\" };\n", - " format!(\"{glyph} {phase} done in {}\", obs_human_duration(elapsed_ms))\n", - "}\n", + "pub fn render_phase_concluded_line_mirror(phase: &str, elapsed_ms: u64, emoji: bool) -> String \{\n", + " let glyph = if emoji \{ \"✅\" \} else \{ \"✓\" \};\n", + " format!(\"\{glyph\} \{phase\} done in \{\}\", obs_human_duration(elapsed_ms))\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", @@ -525,8 +525,8 @@ fn rt_hash_ops() -> String { "/// as `phase_profile.rs` / `GUNBC_FLOOR_GANTT`, per `docs/plans/ci-floor-fractal-gantt.md`\n", "/// § dissolution). Receipt = that witness green with these marks deleted and stage walls\n", "/// still attributable from the model path.\n", - "pub fn trace_mark(label: String) {\n", - " use std::sync::{Mutex, OnceLock};\n", + "pub fn trace_mark(label: String) \{\n", + " 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", @@ -537,69 +537,68 @@ fn rt_hash_ops() -> String { " let emoji = obs_phase_emoji();\n", " let now = Instant::now();\n", " let _ = GANTT_CENSUS_MARKER;\n", - " if let Some(phase) = label.strip_suffix(\".begin\") {\n", - " if let Ok(mut map) = opens.lock() {\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", - " }\n", - " if let Ok(mut l) = last.lock() {\n", + " \}\n", + " if let Ok(mut l) = last.lock() \{\n", " *l = 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", + " \}\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", + " match started \{\n", " Some(t) => now.saturating_duration_since(t).as_millis() as u64,\n", - " None => {\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", + " \}\n", + " \}\n", + " \};\n", + " if let Ok(mut l) = last.lock() \{\n", " *l = now;\n", - " }\n", + " \}\n", " eprintln!(\n", - " \"{}\",\n", + " \"\{\}\",\n", " render_phase_concluded_line_mirror(phase, elapsed_ms, emoji)\n", " );\n", - " } else {\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", + " if let Ok(mut l) = last.lock() \{\n", " *l = now;\n", - " }\n", + " \}\n", " eprintln!(\n", - " \"{}\",\n", + " \"\{\}\",\n", " render_phase_concluded_line_mirror(&label, elapsed_ms, emoji)\n", " );\n", - " }\n", - "}\n", + " \}\n", + "\}\n", "\n", "/// Content hash over raw bytes — the byte-level single authority. `atom_identity_hash`\n", "/// is the `String` projection of this. Use this directly for arbitrary binary content\n", "/// (e.g. an executable or serialized payload): routing bytes through `String`/\n", "/// `from_utf8_lossy` first collapses every invalid UTF-8 sequence to U+FFFD, so distinct\n", "/// byte sequences would hash equal — a §5 silent-collision fail-open for content-addressing.\n", - "pub fn bytes_identity_hash(bytes: &[u8]) -> Hash {\n", - " format!(\"{:016x}\", fnv1a64(bytes))\n", - "}\n", + "pub fn bytes_identity_hash(bytes: &[u8]) -> Hash \{\n", + " format!(\"\{:016x\}\", fnv1a64(bytes))\n", + "\}\n", "\n", - "pub fn atom_identity_hash(s: String) -> Hash {\n", + "pub fn atom_identity_hash(s: String) -> Hash \{\n", " bytes_identity_hash(s.as_bytes())\n", - "}\n", + "\}\n", "\n", - "pub fn hash_combine(a: Hash, b: Hash) -> Hash {\n", + "pub fn hash_combine(a: Hash, b: Hash) -> Hash \{\n", " expect_hash_digest(&a, \"a\");\n", " expect_hash_digest(&b, \"b\");\n", " let mut bytes = a.into_bytes();\n", " bytes.push(0);\n", " bytes.extend_from_slice(b.as_bytes());\n", - " format!(\"{:016x}\", fnv1a64(&bytes))\n", - "}\n", + " format!(\"\{:016x\}\", fnv1a64(&bytes))\n", + "\}\n", "\n") } - fn rt_resolution_silent_pick_telemetry() -> String { concat( "/// Read-only silent-pick telemetry for resolution divergence census slice 2.\n", From 3d2d342d1b9022069cd165d47d70e27722a690b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 01:57:51 +0000 Subject: [PATCH 28/39] =?UTF-8?q?Wiring=20flip:=20[typecheck-attribution]?= =?UTF-8?q?=20=E2=86=92=20ModuleSegment+PhaseSegment=20observation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-module typecheck Begin/Concluded via typecheck_*_line mirrors (render_typecheck_*_line_mirror); 2s pathology threshold preserved. Census MigratedToObservation; frontier 2→1. Mirror↔oracle RED for the captured crawl fixture module. Verified via claim_batch. Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 9 +-- dag/gunbc/observation_seed_render.dag | 38 +++++++++ .../observation_emit_census_witness_test.dag | 24 +++++- src/v1/stage0/src/bin/claim_executor.rs | 81 +++++++++++++++++++ src/v1/stage0/src/cli_run.rs | 55 ++++++++++++- 5 files changed, 197 insertions(+), 10 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index c5efb0d6322..e9e1bff0bcc 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (4b), [gantt] (Begin/Concluded PhaseSegment), [governor] (ci_hold_cause_text / seed_governor_hold_line / render_governor_hold_line_mirror). Each keeps its marker string so the roster cannot go stale." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (4b), [gantt], [governor], [typecheck-attribution] (ModuleSegment+PhaseSegment typecheck via typecheck_*_line / render_typecheck_*_line_mirror). Each keeps its marker string so the roster cannot go stale." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -32,9 +32,8 @@ data floor_memory_site: CensusedEmitSite = CensusedEmitSite { data typecheck_attribution_site: CensusedEmitSite = CensusedEmitSite { marker: "[typecheck-attribution]" as NonEmptyStr, source_file: "src/v1/stage0/src/cli_run.rs" as NonEmptyStr, - disposition: CountedFrontierSite { - reason: "per-module typecheck cost, emitted only at walk end — the receipt that caused the 55-minute mis-triage because it never wrote until the walk it was timing completed" as NonEmptyStr, - dissolve_on: "a Concluded event per module carrying its measured wall (the observation event stream is written as work concludes, not batched to walk end), rendered by ci_event_line — lands with the P1 wiring after the rework PR" as NonEmptyStr + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_event_line (seed typecheck_begin_line / typecheck_concluded_line; mirror render_typecheck_*_line_mirror; ModuleSegment+PhaseSegment)" as NonEmptyStr } } @@ -80,7 +79,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: two tag families still frontier (typecheck-attribution, measurement), three migrated (floor-memory, gantt, governor), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." +data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: one tag family still frontier (measurement), four migrated (floor-memory, gantt, governor, typecheck-attribution), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index e82ca935d35..38d484d8877 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -9,6 +9,7 @@ import std.observation { ObservationSubject, BatchSegment, PhaseSegment, + ModuleSegment, MeasuredValue, MeasuredUnavailable, ObservationEvent, @@ -136,6 +137,43 @@ fn seed_high_water_hold_line(current_bytes: Nat, high_water_bytes: Nat, emoji: B ) } +data seed_typecheck_attribution_note: String = "Per-module typecheck attribution (wiring flip): a Concluded event on ModuleSegment+PhaseSegment\{typecheck\} carrying the measured wall, rendered by ci_event_line — written as the module concludes rather than batched to walk end. The 2s threshold that kept the floor log quiet is preserved at the emit site (pathology lane only). Hot-path mirror in cli_run (interpreter render per module under resolve would dominate the wall); oracle RED keeps the mirror byte-equal to this seed." + +fn seed_typecheck_subject(module_path: NonEmptyStr) -> ObservationSubject { + ObservationSubject { + segments: [ + ModuleSegment { module_path: module_path }, + PhaseSegment { name: "typecheck" as NonEmptyStr } + ] + } +} + +fn typecheck_begin_line(module_path: NonEmptyStr, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_typecheck_subject(module_path: module_path), + transition: Begin, + wall: MeasuredUnavailable { cause: "begin has no elapsed yet" as NonEmptyStr }, + rss: MeasuredUnavailable { cause: "not sampled at a typecheck boundary" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + +fn typecheck_concluded_line(module_path: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_typecheck_subject(module_path: module_path), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: elapsed_ms) }, + rss: MeasuredUnavailable { cause: "not sampled at a typecheck boundary" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + data observation_seed_render_disposition: Disposition = Terminal { reason: "The seed to .dag render boundary is Terminal, not a scaffold: a boundary the seed calls across to keep the format single-authority is the mechanism, not an interim stand-in. What dissolves is each raw eprintln the seed replaces with a call here — counted by gunbc.observation_emit_census — and, further out, the seed itself as v2 realizes the witnesses natively; neither changes what this module models, which is that a seed occurrence is an observation event projected by the one renderer." } diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 98b74007671..ba6ba1ff26b 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -16,11 +16,12 @@ import gunbc.observation_emit_census { floor_memory_site, gantt_site, governor_site, + typecheck_attribution_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt], [governor]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt], [governor], [typecheck-attribution]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." fn census_source(path: String) -> String { let r = filesystem_read(path: path) @@ -80,8 +81,16 @@ fn w_governor_has_migrated() -> Bool { string_contains(s: emit_site_dissolve_on(site: governor_site), pattern: "render_governor_hold_line_mirror") } +fn w_typecheck_attribution_has_migrated() -> Bool { + census_marker_present(site: typecheck_attribution_site) && + !emit_site_is_frontier(site: typecheck_attribution_site) && + string_contains(s: typecheck_attribution_site.marker, pattern: "typecheck-attribution") && + string_contains(s: emit_site_dissolve_on(site: typecheck_attribution_site), pattern: "ci_event_line") && + string_contains(s: emit_site_dissolve_on(site: typecheck_attribution_site), pattern: "typecheck_concluded_line") +} + fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { - observation_emit_frontier_count() == 2 && + observation_emit_frontier_count() == 1 && count(observation_emit_roster) == 5 } @@ -113,6 +122,13 @@ fn w_the_raw_governor_shape_is_gone_from_the_seed() -> Bool { string_contains(s: gov, pattern: "GOVERNOR_CENSUS_MARKER") } +fn w_the_raw_typecheck_attribution_shape_is_gone_from_the_seed() -> Bool { + let cli = census_source(path: "src/v1/stage0/src/cli_run.rs") + !string_contains(s: cli, pattern: "[typecheck-attribution] module=") && + string_contains(s: cli, pattern: "render_typecheck_concluded_line_mirror") && + string_contains(s: cli, pattern: "TYPECHECK_ATTRIBUTION_CENSUS_MARKER") +} + test fn observation_emit_census_witnesses() -> Bool { w_every_rostered_marker_still_exists_in_the_seed() && w_roster_staleness_check_discriminates() && @@ -120,8 +136,10 @@ test fn observation_emit_census_witnesses() -> Bool { w_the_named_negative_example_has_migrated() && w_gantt_has_migrated() && w_governor_has_migrated() && + w_typecheck_attribution_has_migrated() && w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && w_the_raw_byte_dump_shape_is_gone_from_the_seed() && w_the_raw_gantt_shape_is_gone_from_the_seed() && - w_the_raw_governor_shape_is_gone_from_the_seed() + w_the_raw_governor_shape_is_gone_from_the_seed() && + w_the_raw_typecheck_attribution_shape_is_gone_from_the_seed() } diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 041882fab39..55f7eb09e27 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -3331,6 +3331,87 @@ mod tests { ); } + fn run_seed_typecheck_concluded_line( + source_roots: &[String], + module_path: &str, + elapsed_ms: u64, + overhead_ms: u64, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "typecheck_concluded_line", + &[ + ( + Some("module_path".to_string()), + Value::Str(module_path.to_string()), + ), + ( + Some("elapsed_ms".to_string()), + Value::Int(elapsed_ms as i64), + ), + ( + Some("overhead_ms".to_string()), + Value::Int(overhead_ms as i64), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + #[test] + fn typecheck_attribution_mirrors_match_seed_oracle() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + let oracle = run_seed_typecheck_concluded_line( + &roots, + "v2.compiler.normalized_tree", + 606_984, + 300_000, + true, + ) + .expect("typecheck_concluded_line must resolve and render"); + let mirror = v1_compiler::cli_run::render_typecheck_concluded_line_mirror( + "v2.compiler.normalized_tree", + 606_984, + true, + ); + assert_eq!( + oracle, mirror, + "typecheck concluded mirror must be byte-equal to seed oracle" + ); + assert!( + oracle.starts_with('✅') + && oracle.contains("typecheck v2.compiler.normalized_tree done in 10 minutes"), + "typecheck concluded shape: {oracle:?}" + ); + let begin_mirror = v1_compiler::cli_run::render_typecheck_begin_line_mirror( + "v2.compiler.normalized_tree", + true, + ); + assert_eq!( + begin_mirror, + "🔄 started typecheck v2.compiler.normalized_tree" + ); + } + #[allow(clippy::too_many_arguments)] fn run_seed_heartbeat_line( source_roots: &[String], diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index c7f66aca85b..a4c4fbc6f83 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -6708,6 +6708,47 @@ fn index_record_schedule_module( } } +/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the +/// raw `[typecheck-attribution]` key=value shape dissolves into the observation projection. +#[allow(dead_code)] +pub const TYPECHECK_ATTRIBUTION_CENSUS_MARKER: &str = "[typecheck-attribution]"; + +const TYPECHECK_MINUTE_SWITCH_SECONDS: u64 = 90; + +fn typecheck_emoji() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + +fn typecheck_human_duration(ms: u64) -> String { + if ms < 1_000 { + format!("{ms}ms") + } else if ms < TYPECHECK_MINUTE_SWITCH_SECONDS * 1_000 { + format!("{} seconds", ms / 1_000) + } else { + format!("{} minutes", ms / 60_000) + } +} + +/// Mirror of `gunbc.observation_seed_render.typecheck_begin_line` — +/// leaf text is `typecheck ` (ModuleSegment + PhaseSegment). +pub fn render_typecheck_begin_line_mirror(module_path: &str, emoji: bool) -> String { + let glyph = if emoji { "🔄" } else { "◐" }; + format!("{glyph} started typecheck {module_path}") +} + +/// Mirror of `gunbc.observation_seed_render.typecheck_concluded_line`. +pub fn render_typecheck_concluded_line_mirror( + module_path: &str, + elapsed_ms: u64, + emoji: bool, +) -> String { + let glyph = if emoji { "✅" } else { "✓" }; + format!( + "{glyph} typecheck {module_path} done in {}", + typecheck_human_duration(elapsed_ms) + ) +} + /// Snapshot of the floor's active-batch progress, sampled by the detached /// floor-memory heartbeat thread. Armed only when `entry_total` is known and /// non-zero — never a fabricated 0-of-0 (observation law 2 / §5). @@ -8947,7 +8988,11 @@ fn reconcile_with_typed_cache( // Once-per-node receipt (§6.2): count only genuine computes (cache misses). bump_typecheck_compute_count(); if phase_profile::phase_profile_enabled() { - eprintln!("[typecheck-attribution] module={mod_name} start"); + let _ = TYPECHECK_ATTRIBUTION_CENSUS_MARKER; + eprintln!( + "{}", + render_typecheck_begin_line_mirror(&mod_name, typecheck_emoji()) + ); } let module_tc_started = std::time::Instant::now(); // Same-tree bare underlay for the module being typechecked @@ -8987,8 +9032,14 @@ fn reconcile_with_typed_cache( }); let module_tc_ms = module_tc_elapsed.as_millis(); if module_tc_ms >= 2_000 { + let _ = TYPECHECK_ATTRIBUTION_CENSUS_MARKER; eprintln!( - "[typecheck-attribution] module={mod_name} ms={module_tc_ms}" + "{}", + render_typecheck_concluded_line_mirror( + &mod_name, + module_tc_ms as u64, + typecheck_emoji(), + ) ); } let computed = index_insert_typed(index, typed_key.clone(), computed)?; From fc75cc3e340e0eeab9cf6823ca45aadca9712b14 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 02:33:50 +0000 Subject: [PATCH 29/39] =?UTF-8?q?Wiring=20flip:=20[measurement]=20+=20shel?= =?UTF-8?q?l-echo=20=E2=86=92=20ObservationEvents=20(one=20pass)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measurement peak-RSS/cgroup dumps project through ci_measurement_rss_line (seed_peak_rss_line ↔ render_peak_rss_line_mirror; identity-first, human GiB). Shell host-effects become ObservationEvents: Ambient Begin/Done still gated by ShellTrace; Anomaly Failed gated by effect_stream disposition alone (never silenced by ShellTrace Suppressed), with law-4 idempotent group_end, self- describing `$ argv (exit=N)`, and empty-stderr surfacing. ShellTrace/ disposition tables left unread for the interim Condensed-at-Normal split. Census: measurement + shell MigratedToObservation; tagged frontier count 0; 76 raw eprintln residue recounted. Standing check: claim_batch census witnesses PASS before push. Co-authored-by: Brian Searls --- dag/gunbc/observation_ci_render.dag | 11 + dag/gunbc/observation_emit_census.dag | 20 +- dag/gunbc/observation_seed_render.dag | 78 ++++ dag/gunbc/output_policy.dag | 2 + .../observation_emit_census_witness_test.dag | 52 ++- src/v1/stage0/src/bin/claim_batch.rs | 29 +- src/v1/stage0/src/bin/claim_executor.rs | 200 +++++++++- src/v1/stage0/src/cli_run.rs | 40 ++ src/v1/stage0/src/v1_interpreter.rs | 375 ++++++++++++------ 9 files changed, 657 insertions(+), 150 deletions(-) diff --git a/dag/gunbc/observation_ci_render.dag b/dag/gunbc/observation_ci_render.dag index 1bac12a78f3..45a29646c0b 100644 --- a/dag/gunbc/observation_ci_render.dag +++ b/dag/gunbc/observation_ci_render.dag @@ -449,6 +449,17 @@ fn ci_refusal_restate_line(subject: ObservationSubject, diagnostic: NonEmptyStr) data ci_subject_text_guard_note: String = "A restated diagnostic is text this repo relays from a SUBJECT process, so it goes through extdeps.github.log_annotations.neutralize_workflow_commands: a child whose captured stderr happens to begin with the workflow-command prefix would otherwise mint annotations in the parent run that no gunbc code authored. Consuming the existing guard rather than re-deriving it is the point — the neutralization rule is that module's fact, not this renderer's." + +data ci_measurement_rss_note: String = "Measurement-feed projection (wiring flip): peak-RSS and cgroup samples are not progress Begin/Concluded pairs — they are Measured facts about the run. This line is Ambient, identity-first (the label), vitals in human GiB via ci_measured_bytes_text, never a raw byte dump. Placement consumers and receipts still read the typed facts; this is the log surface only." + +fn ci_measurement_rss_line(label: NonEmptyStr, rss: Measured) -> RenderedLine { + RenderedLine { + glyph: StatusPulse, + text: concat(label, concat(" — ", ci_measured_bytes_text(m: rss))), + attention: Ambient + } +} + data observation_ci_render_disposition: Disposition = Terminal { reason: "The CI-log renderer is Terminal, not a scaffold: append-only line projection is what the GitHub Actions medium IS, not an interim stand-in for a richer one. The TTY renderer is a sibling projection of the same carriers rather than this module's successor, and the dashboard is a third. What dissolves later is the hand-rolled emit sites this renderer replaces, each counted by the P3 census." } diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index e9e1bff0bcc..146dc8cd604 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrites the floor's emit sites, so the EXHAUSTIVE per-print wall over every raw eprintln in claim_executor is a declared frontier gated on that rebase — censusing sites about to be rewritten would be work churned twice, the migration class the operator ruled out. What lands now is the census MODEL, the roster of the structured-tag families that exist regardless of the rework, and the hygiene witness. Migrated so far: [floor-memory] (4b), [gantt], [governor], [typecheck-attribution] (ModuleSegment+PhaseSegment typecheck via typecheck_*_line / render_typecheck_*_line_mirror). Each keeps its marker string so the roster cannot go stale." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrote the floor's emit sites, so the tagged structured-tag families migrate on this PR. Migrated: [floor-memory] (4b), [gantt], [governor], [typecheck-attribution], [measurement] (ci_measurement_rss_line / render_peak_rss_line_mirror), [shell] (shell_effect_*_line / render_shell_effect_*_line_mirror; Ambient via ShellTrace, Anomaly via disposition alone). Tagged frontier count is now 0. The unmarked raw eprintln sites in claim_executor remain the residue — currently 76 `eprintln!` call sites by live count — their per-print classification is the re-census this PR's dissolve condition named." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -56,9 +56,16 @@ data governor_site: CensusedEmitSite = CensusedEmitSite { data measurement_site: CensusedEmitSite = CensusedEmitSite { marker: "[measurement]" as NonEmptyStr, source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, - disposition: CountedFrontierSite { - reason: "peak-RSS and per-shard memory samples consumed by the placement divisor and compile-jobs derive — a MEASUREMENT feed with real downstream consumers, not a progress line, so its migration is subtler than the display sites" as NonEmptyStr, - dissolve_on: "re-census after the CI two-tier rework PR: the rework changes which measurements the placement axis consumes, so classifying this as projection-or-frontier before the rework would classify a site about to be rewritten" as NonEmptyStr + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_measurement_rss_line (seed seed_peak_rss_line; mirror render_peak_rss_line_mirror; identity-first label, human GiB)" as NonEmptyStr + } +} + +data shell_site: CensusedEmitSite = CensusedEmitSite { + marker: "[shell]" as NonEmptyStr, + source_file: "src/v1/stage0/src/v1_interpreter.rs" as NonEmptyStr, + disposition: MigratedToObservation { + via: "gunbc.observation_ci_render.ci_event_line (seed shell_effect_begin_line / shell_effect_done_line / shell_effect_failed_line; mirror render_shell_effect_*_line_mirror; Ambient via ShellTrace, Anomaly Failed self-describing via disposition alone)" as NonEmptyStr } } @@ -67,7 +74,8 @@ data observation_emit_roster: List = [ typecheck_attribution_site, gantt_site, governor_site, - measurement_site + measurement_site, + shell_site ] data observation_emit_roster_completeness_disposition: Disposition = Scaffold { @@ -79,7 +87,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "The roster covers the STRUCTURED-TAG emit families that exist in the seed regardless of the rework. It does NOT yet cover the ~75 unmarked raw eprintln sites in claim_executor: those are exactly the sites the CI two-tier rework rewrites, so their per-print classification is the declared frontier that dissolves when this PR rebases over that rework and re-censuses. Until then the honest count is stated, never zero: one tag family still frontier (measurement), four migrated (floor-memory, gantt, governor, typecheck-attribution), the raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale in the meantime. The end state — a live wall that reds any new bare print outside the projection — is reached when the post-rework sites are enumerated, per the design section 5 P3." +data observation_emit_roster_completeness_note: String = "Tagged structured-tag families are now all MigratedToObservation (frontier count 0). The roster does NOT yet enumerate the 76 unmarked raw eprintln sites in claim_executor as individual CountedFrontierSite rows — that per-print re-census is the remaining completeness scaffold, and its dissolve condition (\"when the post-rework sites are enumerated\") is satisfied by this PR's landing of the tagged migrations, so the enumeration itself is the next micro-pass on this bar. Until those rows land the honest count is: zero tagged frontier, six migrated families, 76 raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 38d484d8877..2dedbf0dc4e 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -16,6 +16,7 @@ import std.observation { Begin, Concluded, Done, + Failed, AttentionBasis, Anomaly, SchedulerHold, @@ -27,6 +28,7 @@ import gunbc.observation_ci_render { ci_render_line, ci_heartbeat_line, ci_hold_cause_text, + ci_measurement_rss_line, HeartbeatSample, RenderedLine, } @@ -174,6 +176,82 @@ fn typecheck_concluded_line(module_path: NonEmptyStr, elapsed_ms: Nat, overhead_ ) } +data seed_measurement_unreadable_cause: NonEmptyStr = "no /proc/self/status" as NonEmptyStr + +data seed_measurement_note: String = "Measurement-feed projection (wiring flip): peak-RSS samples become ci_measurement_rss_line — identity-first label, human GiB, MeasuredUnavailable names its cause. Hot-path mirrors in claim_executor / claim_batch (same class as heartbeat). The typed receipt consumers are unchanged; only the log dialect migrates." + +fn seed_peak_rss_line(label: NonEmptyStr, rss_bytes: Nat, rss_available: Bool, emoji: Bool) -> String { + ci_render_line( + line: ci_measurement_rss_line( + label: label, + rss: if rss_available { + MeasuredValue { value: byte_size(rss_bytes) } + } else { + MeasuredUnavailable { cause: seed_measurement_unreadable_cause } + } + ), + tier: seed_tier(emoji: emoji) + ) +} + +data seed_shell_effect_note: String = "Shell host-effect projection (operator steer 2026-07-25): effects become ObservationEvents with per-event attention — routine Begin/Concluded\{Done\} Ambient (collapses inside the host-effects group); failing Concluded\{Failed\} Anomaly (OutsideGroup by law 4). Carry-forward from 60a4496 as event properties: Failed.error is self-describing `$ (exit=N)` whitespace-collapsed; empty stderr still surfaces via the Failed line alone. ShellTrace channel decision and effect_stream_disposition tables stay unread-from for Anomaly gating differently: Ambient spawn/done still READ channel_decision(ShellTrace); Anomaly surfaces from SurfaceContent disposition alone and is never silenced by ShellTrace Suppressed. Hot-path mirrors in v1_interpreter; oracle RED keeps them honest." + +fn seed_shell_subject(argv_summary: NonEmptyStr) -> ObservationSubject { + ObservationSubject { segments: [ PhaseSegment { name: argv_summary } ] } +} + +fn shell_effect_begin_line(argv_summary: NonEmptyStr, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_shell_subject(argv_summary: argv_summary), + transition: Begin, + wall: MeasuredUnavailable { cause: "begin has no elapsed yet" as NonEmptyStr }, + rss: MeasuredUnavailable { cause: "not sampled at shell spawn" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + +fn shell_effect_done_line(argv_summary: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { + let event = ObservationEvent { + subject: seed_shell_subject(argv_summary: argv_summary), + transition: Concluded { outcome: Done }, + wall: MeasuredValue { value: millisecond(count: elapsed_ms) }, + rss: MeasuredUnavailable { cause: "not sampled at shell completion" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + +fn shell_effect_failed_line( + argv_summary: NonEmptyStr, + argv_collapsed: NonEmptyStr, + exit_code: Nat, + elapsed_ms: Nat, + overhead_ms: Nat, + emoji: Bool +) -> String { + let error = concat( + "$ ", + concat(argv_collapsed, concat(" (exit=", concat(to_string(exit_code), ")"))) + ) as NonEmptyStr + let event = ObservationEvent { + subject: seed_shell_subject(argv_summary: argv_summary), + transition: Concluded { + outcome: Failed { error: error, output: "" } + }, + wall: MeasuredValue { value: millisecond(count: elapsed_ms) }, + rss: MeasuredUnavailable { cause: "not sampled at shell completion" as NonEmptyStr } + } + ci_render_line( + line: ci_event_line(event: event, basis: seed_phase_basis(overhead_ms: overhead_ms)), + tier: seed_tier(emoji: emoji) + ) +} + data observation_seed_render_disposition: Disposition = Terminal { reason: "The seed to .dag render boundary is Terminal, not a scaffold: a boundary the seed calls across to keep the format single-authority is the mechanism, not an interim stand-in. What dissolves is each raw eprintln the seed replaces with a call here — counted by gunbc.observation_emit_census — and, further out, the seed itself as v2 realizes the witnesses natively; neither changes what this module models, which is that a seed occurrence is an observation event projected by the one renderer." } diff --git a/dag/gunbc/output_policy.dag b/dag/gunbc/output_policy.dag index 074d7de12bc..b3a379881dc 100644 --- a/dag/gunbc/output_policy.dag +++ b/dag/gunbc/output_policy.dag @@ -201,3 +201,5 @@ fn shell_trace_stream_policy(verbosity: Verbosity) -> EffectStreamPolicy { fn resolve_shell_trace_stream_policy(verbose: Bool, quiet: Bool) -> EffectStreamPolicy { shell_trace_stream_policy(verbosity: resolve_verbosity(env: VerbosityEnv { verbose: verbose, quiet: quiet })) } + +data shell_trace_observation_convergence_note: String = "CONVERGENCE (operator steer 2026-07-25, post-60a4496 revert): ShellTrace at Normal Condensed and effect_stream_disposition remain the channel-layer split until shell host-effects emit as ObservationEvents with per-event attention (routine Ambient / failure Anomaly). When the event path carries the traffic — Begin/Concluded projections, Anomaly OutsideGroup by law 4, self-describing $ argv and empty-stderr exit on Failed — this channel-level split dissolves: delete this note, retire Condensed-at-Normal as the Ambient gate, and let derived attention plus ci_line_placement supersede the dual-class conflation. Do NOT build an interim disposition↔channel decoupling; the observation wiring is the dissolve." diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index ba6ba1ff26b..944865c3d39 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -17,11 +17,13 @@ import gunbc.observation_emit_census { gantt_site, governor_site, typecheck_attribution_site, + measurement_site, + shell_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites ([floor-memory], [gantt], [governor], [typecheck-attribution]) keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." +data witness_note: String = "P3 census hygiene, executable against the live seed (docs/plans/progress-observation-design.md section 5). A census that is not checked against the code it censuses is coverage-by-illusion — an inert lens is itself a lie. So every rostered marker is held against the seed file it names: a marker that has vanished reds (the roster went stale, the site was renamed or deleted without re-census), and a frontier row must carry a real, non-empty dissolve-on so the debt stays prioritizable rather than open-ended. Migrated sites keep their marker strings so the roster cannot go stale, and each has a RED that the raw byte shape is gone from the seed." fn census_source(path: String) -> String { let r = filesystem_read(path: path) @@ -89,9 +91,25 @@ fn w_typecheck_attribution_has_migrated() -> Bool { string_contains(s: emit_site_dissolve_on(site: typecheck_attribution_site), pattern: "typecheck_concluded_line") } -fn w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() -> Bool { - observation_emit_frontier_count() == 1 && - count(observation_emit_roster) == 5 +fn w_measurement_has_migrated() -> Bool { + census_marker_present(site: measurement_site) && + !emit_site_is_frontier(site: measurement_site) && + string_contains(s: measurement_site.marker, pattern: "measurement") && + string_contains(s: emit_site_dissolve_on(site: measurement_site), pattern: "ci_measurement_rss_line") && + string_contains(s: emit_site_dissolve_on(site: measurement_site), pattern: "render_peak_rss_line_mirror") +} + +fn w_shell_has_migrated() -> Bool { + census_marker_present(site: shell_site) && + !emit_site_is_frontier(site: shell_site) && + string_contains(s: shell_site.marker, pattern: "shell") && + string_contains(s: emit_site_dissolve_on(site: shell_site), pattern: "shell_effect_failed_line") && + string_contains(s: emit_site_dissolve_on(site: shell_site), pattern: "render_shell_effect_") +} + +fn w_the_census_states_a_zero_tagged_frontier_count() -> Bool { + observation_emit_frontier_count() == 0 && + count(observation_emit_roster) == 6 } fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { @@ -129,6 +147,24 @@ fn w_the_raw_typecheck_attribution_shape_is_gone_from_the_seed() -> Bool { string_contains(s: cli, pattern: "TYPECHECK_ATTRIBUTION_CENSUS_MARKER") } +fn w_the_raw_measurement_shape_is_gone_from_the_seed() -> Bool { + let executor = census_source(path: "src/v1/stage0/src/bin/claim_executor.rs") + let cli = census_source(path: "src/v1/stage0/src/cli_run.rs") + !string_contains(s: executor, pattern: "bytes (VmHWM)") && + !string_contains(s: executor, pattern: "[measurement] floor peak RSS:") && + !string_contains(s: executor, pattern: "[measurement] cgroup peak:") && + string_contains(s: executor, pattern: "render_peak_rss_line_mirror") && + string_contains(s: cli, pattern: "MEASUREMENT_CENSUS_MARKER") +} + +fn w_the_raw_shell_shape_is_gone_from_the_seed() -> Bool { + let interp = census_source(path: "src/v1/stage0/src/v1_interpreter.rs") + !string_contains(s: interp, pattern: "[shell] done exit=") && + !string_contains(s: interp, pattern: "[shell] stderr (exit=") && + string_contains(s: interp, pattern: "render_shell_effect_failed_line_mirror") && + string_contains(s: interp, pattern: "SHELL_CENSUS_MARKER") +} + test fn observation_emit_census_witnesses() -> Bool { w_every_rostered_marker_still_exists_in_the_seed() && w_roster_staleness_check_discriminates() && @@ -137,9 +173,13 @@ test fn observation_emit_census_witnesses() -> Bool { w_gantt_has_migrated() && w_governor_has_migrated() && w_typecheck_attribution_has_migrated() && - w_the_census_states_a_nonzero_frontier_count_never_a_hidden_zero() && + w_measurement_has_migrated() && + w_shell_has_migrated() && + w_the_census_states_a_zero_tagged_frontier_count() && w_the_raw_byte_dump_shape_is_gone_from_the_seed() && w_the_raw_gantt_shape_is_gone_from_the_seed() && w_the_raw_governor_shape_is_gone_from_the_seed() && - w_the_raw_typecheck_attribution_shape_is_gone_from_the_seed() + w_the_raw_typecheck_attribution_shape_is_gone_from_the_seed() && + w_the_raw_measurement_shape_is_gone_from_the_seed() && + w_the_raw_shell_shape_is_gone_from_the_seed() } diff --git a/src/v1/stage0/src/bin/claim_batch.rs b/src/v1/stage0/src/bin/claim_batch.rs index c3aeea24015..5692b000c0e 100644 --- a/src/v1/stage0/src/bin/claim_batch.rs +++ b/src/v1/stage0/src/bin/claim_batch.rs @@ -77,10 +77,11 @@ fn children_max_rss_bytes() -> Option { } fn emit_rss_measurement(label: &str) { - match peak_rss_bytes() { - Some(bytes) => eprintln!("[measurement] {label}: {bytes} bytes (VmHWM)"), - None => eprintln!("[measurement] {label}: unavailable (no /proc/self/status)"), - } + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror(label, peak_rss_bytes(), emoji) + ); } fn print_interp_stats(ctx: &InterpContext, flatten_baseline: (u64, u64)) { @@ -578,7 +579,15 @@ fn run() -> Result { Ok(keys) => { emit_rss_measurement("post-mock-precompute-rss"); if let Some(bytes) = children_max_rss_bytes() { - eprintln!("[measurement] post-mock-precompute-children-max-rss: {bytes} bytes (getrusage RUSAGE_CHILDREN)"); + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror( + "post-mock-precompute-children-max-rss", + Some(bytes), + emoji, + ) + ); } if keys.is_empty() { eprintln!( @@ -692,7 +701,15 @@ fn run() -> Result { emit_rss_measurement("per-shard-peak-rss"); if let Some(bytes) = children_max_rss_bytes() { - eprintln!("[measurement] children-max-rss: {bytes} bytes (getrusage RUSAGE_CHILDREN)"); + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror( + "children-max-rss", + Some(bytes), + emoji, + ) + ); } if any_failed { diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 55f7eb09e27..940e77d4ad7 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1626,8 +1626,17 @@ fn verify_build_artifacts(paths: &[String]) -> Result { /// standalone `--measure-cgroup-peak` mode so the `ci` and `rust_tests` jobs report an /// identically-shaped line. `context` distinguishes the call site. fn emit_cgroup_measurement(context: &str) { + let emoji = std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"); match cgroup_job_measurement() { Some(m) => { + let label = format!("cgroup peak @ {} ({context})", m.leaf_rel); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror(&label, Some(m.leaf_peak), emoji) + ); + // Diagnostic companions (cap/pids/sccache) stay as Ambient detail beside the + // Measured peak — not the old raw-byte `[measurement]` dump. Placement still + // reads the typed `cgroup_job_measurement` fact, not this prose. let cap = match m.cap_bytes { Some(b) => format!("{b} bytes"), None => "uncapped(RAM-bound)".to_string(), @@ -1642,16 +1651,23 @@ fn emit_cgroup_measurement(context: &str) { (None, _) => "not-found (treat as fixed host overhead)".to_string(), }; eprintln!( - "[measurement] cgroup peak: {peak} bytes (memory.peak @ {rel}) memory.max={cap} host_ram={host_ram} pids.current={pc} pids.max={pm} sccache-server-cgroup={sccache} context={context}", - peak = m.leaf_peak, - rel = m.leaf_rel, + " memory.max={cap} host_ram={host_ram} pids_current={pc} pids_max={pm} sccache-server-cgroup={sccache}", pc = m.pids_current, pm = m.pids_max ); } - None => eprintln!( - "[measurement] cgroup peak: unavailable (no leaf cgroup or memory.peak unreadable; kernel < 5.19?) context={context}" - ), + None => { + let label = format!("cgroup peak ({context})"); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror_with_cause( + &label, + None, + "no leaf cgroup or memory.peak unreadable; kernel < 5.19?", + emoji, + ) + ); + } } } @@ -2875,10 +2891,22 @@ fn run() -> Result { ); match peak_rss_bytes() { Some(bytes) => { - eprintln!("[measurement] floor peak RSS: {bytes} bytes (VmHWM) (adaptive width)"); + eprintln!( + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror( + "floor peak RSS (adaptive width)", + Some(bytes), + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"), + ) + ); } None => eprintln!( - "[measurement] floor peak RSS: unavailable (no /proc/self/status) (adaptive width)" + "{}", + v1_compiler::cli_run::render_peak_rss_line_mirror( + "floor peak RSS (adaptive width)", + None, + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true"), + ) ), } // The governor receipt is the §5-counted degradation story for the run: every graceful @@ -3412,6 +3440,162 @@ mod tests { ); } + fn run_seed_peak_rss_line( + source_roots: &[String], + label: &str, + rss_bytes: u64, + rss_available: bool, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "seed_peak_rss_line", + &[ + (Some("label".to_string()), Value::Str(label.to_string())), + (Some("rss_bytes".to_string()), Value::Int(rss_bytes as i64)), + ( + Some("rss_available".to_string()), + Value::Bool(rss_available), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + #[test] + fn peak_rss_mirror_matches_seed_oracle() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + // 1 GiB exact → "1.0 GiB" + let oracle = run_seed_peak_rss_line(&roots, "floor peak RSS", 1_073_741_824, true, true) + .expect("seed_peak_rss_line must resolve and render"); + let mirror = v1_compiler::cli_run::render_peak_rss_line_mirror( + "floor peak RSS", + Some(1_073_741_824), + true, + ); + assert_eq!(oracle, mirror, "peak RSS mirror must be byte-equal to seed"); + assert_eq!(oracle, "🕐 floor peak RSS — 1.0 GiB"); + + // 1536 MiB → "1.5 GiB" + let oracle15 = run_seed_peak_rss_line(&roots, "cgroup peak", 1_610_612_736, true, false) + .expect("seed"); + let mirror15 = v1_compiler::cli_run::render_peak_rss_line_mirror( + "cgroup peak", + Some(1_610_612_736), + false, + ); + assert_eq!(oracle15, mirror15); + assert_eq!(oracle15, "◷ cgroup peak — 1.5 GiB"); + + let unread = run_seed_peak_rss_line(&roots, "floor peak RSS", 0, false, true) + .expect("unreadable seed"); + let unread_mirror = + v1_compiler::cli_run::render_peak_rss_line_mirror("floor peak RSS", None, true); + assert_eq!(unread, unread_mirror); + assert!(unread.contains("unreadable (no /proc/self/status)")); + } + + fn run_seed_shell_effect_failed_line( + source_roots: &[String], + argv_summary: &str, + argv_collapsed: &str, + exit_code: u64, + elapsed_ms: u64, + overhead_ms: u64, + emoji: bool, + ) -> Option { + let entry = source_roots + .iter() + .map(|r| Path::new(r).join("gunbc/observation_seed_render.dag")) + .find(|p| p.exists())? + .to_string_lossy() + .into_owned(); + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry).ok()?; + let ctx = make_eval_context(&graph, indices, ExecutionMode::Hermetic); + let out = run_in_context_with_args( + &ctx, + "shell_effect_failed_line", + &[ + ( + Some("argv_summary".to_string()), + Value::Str(argv_summary.to_string()), + ), + ( + Some("argv_collapsed".to_string()), + Value::Str(argv_collapsed.to_string()), + ), + (Some("exit_code".to_string()), Value::Int(exit_code as i64)), + ( + Some("elapsed_ms".to_string()), + Value::Int(elapsed_ms as i64), + ), + ( + Some("overhead_ms".to_string()), + Value::Int(overhead_ms as i64), + ), + (Some("emoji".to_string()), Value::Bool(emoji)), + ], + false, + ) + .ok()?; + match out { + Value::Str(s) => Some(s), + _ => None, + } + } + + #[test] + fn shell_effect_failed_mirror_matches_seed_oracle() { + let root = workspace_root(); + let roots = vec![ + root.join("src/v2").to_string_lossy().into_owned(), + root.join("dag").to_string_lossy().into_owned(), + ]; + let oracle = run_seed_shell_effect_failed_line( + &roots, + "$ echo hi", + "echo hi", + 1, + 2000, + 300_000, + true, + ) + .expect("shell_effect_failed_line must resolve and render"); + let mirror = v1_compiler::v1_interpreter::render_shell_effect_failed_line_mirror( + "$ echo hi", + "echo hi", + 1, + 2000, + true, + ); + assert_eq!( + oracle, mirror, + "shell Failed mirror must be byte-equal to seed oracle" + ); + assert_eq!( + oracle, + "❌ $ echo hi failed: $ echo hi (exit=1) in 2 seconds" + ); + } + #[allow(clippy::too_many_arguments)] fn run_seed_heartbeat_line( source_roots: &[String], diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index a4c4fbc6f83..a2cabd2195c 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -6713,6 +6713,46 @@ fn index_record_schedule_module( #[allow(dead_code)] pub const TYPECHECK_ATTRIBUTION_CENSUS_MARKER: &str = "[typecheck-attribution]"; +/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the +/// raw `[measurement] … bytes (VmHWM)` dump dissolves into `ci_measurement_rss_line`. +#[allow(dead_code)] +pub const MEASUREMENT_CENSUS_MARKER: &str = "[measurement]"; + +const MEASUREMENT_UNREADABLE_CAUSE: &str = "no /proc/self/status"; + +fn measurement_emoji() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + +fn measurement_human_bytes(bytes: u64) -> String { + // Mirror of ci_gibibyte_tenths: (bytes * 10) / gibibyte_scale_factor_bytes (2^30). + let tenths = (bytes.saturating_mul(10)) / 1_073_741_824; + format!("{}.{} GiB", tenths / 10, tenths % 10) +} + +/// Mirror of `gunbc.observation_seed_render.seed_peak_rss_line` — +/// `ci_measurement_rss_line ∘ ci_render_line`. Identity-first label, human GiB. +/// When `rss_bytes` is `None`, `unreadable_cause` names the MeasuredUnavailable cause +/// (seed default: `no /proc/self/status`). +pub fn render_peak_rss_line_mirror(label: &str, rss_bytes: Option, emoji: bool) -> String { + render_peak_rss_line_mirror_with_cause(label, rss_bytes, MEASUREMENT_UNREADABLE_CAUSE, emoji) +} + +pub fn render_peak_rss_line_mirror_with_cause( + label: &str, + rss_bytes: Option, + unreadable_cause: &str, + emoji: bool, +) -> String { + let _ = MEASUREMENT_CENSUS_MARKER; + let glyph = if emoji { "🕐" } else { "◷" }; + let rss = match rss_bytes { + Some(b) => measurement_human_bytes(b), + None => format!("unreadable ({unreadable_cause})"), + }; + format!("{glyph} {label} — {rss}") +} + const TYPECHECK_MINUTE_SWITCH_SECONDS: u64 = 90; fn typecheck_emoji() -> bool { diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 7d7ef0854f4..8df79eb5788 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5,6 +5,7 @@ use std::collections::BTreeSet; use std::fmt; use std::hash::{Hash, Hasher}; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Instant; use im::HashMap as HamtMap; @@ -5747,20 +5748,30 @@ pub fn host_trace_grouping_active() -> bool { || output_decision(OutputChannel::Instrumentation) != OutputDecision::Suppressed) } +/// Tracks an open host-effect group so `group_end` is idempotent — law 4 closes the +/// group before an Anomaly shell failure, and the batch-end `group_end` must not emit +/// a second `::endgroup::`. +static GROUP_OPEN: AtomicBool = AtomicBool::new(false); + /// Open a titled group on stderr — the same stream the host-effect trace lines use, /// so the runner folds those lines under the marker. No-op when no syntax is /// installed. Pair with `group_end`; the caller must keep the bracket tight (open → /// run+join the effectful work → close) and defer non-trace output (PASS/FAIL) until -/// after `group_end` so it stays OUTSIDE the collapsed section. +/// after `group_end` so it stays outside the collapsed section. pub fn group_begin(title: &str) { if let Some(s) = GROUP_SYNTAX.get() { eprintln!("{}{}{}", s.open_prefix, title, s.open_suffix); + GROUP_OPEN.store(true, Ordering::SeqCst); } } /// Close the current group. Emits the close line only when the target defines one -/// (GitHub Actions); a plain terminal closes implicitly and prints nothing. +/// (GitHub Actions) and a group is actually open. Idempotent: a second call is a +/// no-op (law 4 may have already closed for an Anomaly). pub fn group_end() { + if !GROUP_OPEN.swap(false, Ordering::SeqCst) { + return; + } if let Some(s) = GROUP_SYNTAX.get() { if let Some(close) = &s.close_line { eprintln!("{close}"); @@ -5779,90 +5790,180 @@ fn trace_emit(channel: OutputChannel, line: &str) { } } -// The funnel for the ShellTrace channel. Consumes the installed -// `gunbc.output_policy` ShellTrace decision (Suppressed / Condensed / Full) -// rather than re-deriving it from verbosity — keeps CI logs readable instead of -// dumping every `sh -c` script. -fn render_shell_trace(argv: &[String]) { - match output_decision(OutputChannel::ShellTrace) { - OutputDecision::Suppressed => {} - OutputDecision::Full => eprintln!("[shell] {}", argv.join(" ")), - OutputDecision::Condensed => { - // Collapse newlines/runs of whitespace into a single readable line, - // then truncate so a multiline `sh -c` script is one tidy summary. - let collapsed: String = argv - .join(" ") - .split_whitespace() - .collect::>() - .join(" "); - // Fallback column bound (no Viewport at the trace site); the single - // authority is `gunbc.output_policy.shell_trace_summary_max_columns`. - const MAX: usize = 100; - let summary = if collapsed.chars().count() > MAX { - let head: String = collapsed.chars().take(MAX).collect(); - format!("{head}…") - } else { - collapsed - }; - eprintln!("{}", paint(&format!(" $ {summary}"), sgr::DIM)); +/// Census hygiene marker for the `[shell]` emit family — kept after the wiring flip so +/// the observation_emit_census roster cannot go stale. +pub const SHELL_CENSUS_MARKER: &str = "[shell]"; + +/// Collapse argv into one readable line — runs of whitespace become a single space — +/// so a multiline `sh -c` script reads as one command. Shared by Ambient Begin/Done +/// (optionally capped) and Anomaly Failed (uncapped: an anomaly expands fully). +fn shell_argv_collapsed(argv: &[String]) -> String { + argv.join(" ") + .split_whitespace() + .collect::>() + .join(" ") +} + +/// `$ ` identity for shell ObservationEvent subjects. `cap` truncates the +/// command body (Condensed Ambient); `None` leaves it uncapped (Full / Anomaly). +fn shell_argv_summary(argv: &[String], cap: Option) -> String { + let collapsed = shell_argv_collapsed(argv); + let body = if let Some(max) = cap { + if collapsed.chars().count() > max { + let head: String = collapsed.chars().take(max).collect(); + format!("{head}…") + } else { + collapsed } + } else { + collapsed + }; + format!("$ {body}") +} + +fn shell_obs_emoji() -> bool { + std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") +} + +fn shell_obs_human_duration(ms: u64) -> String { + if ms < 1_000 { + format!("{ms}ms") + } else if ms < 60_000 { + format!("{} seconds", ms / 1_000) + } else { + format!("{} minutes", ms / 60_000) } } -fn shell_completion_trace_line( - exit_code: i32, - stdout_bytes: usize, - stderr_bytes: usize, - wall: std::time::Duration, +/// Mirror of `gunbc.observation_seed_render.shell_effect_begin_line`. +pub fn render_shell_effect_begin_line_mirror(argv_summary: &str, emoji: bool) -> String { + let _ = SHELL_CENSUS_MARKER; + let glyph = if emoji { "🔄" } else { "◐" }; + format!("{glyph} started {argv_summary}") +} + +/// Mirror of `gunbc.observation_seed_render.shell_effect_done_line`. +pub fn render_shell_effect_done_line_mirror( + argv_summary: &str, + elapsed_ms: u64, + emoji: bool, ) -> String { + let _ = SHELL_CENSUS_MARKER; + let glyph = if emoji { "✅" } else { "✓" }; format!( - "[shell] done exit={exit_code} stdout={stdout_bytes} stderr={stderr_bytes} bytes wall={:.3}s", - wall.as_secs_f64() + "{glyph} {argv_summary} done in {}", + shell_obs_human_duration(elapsed_ms) ) } -/// Post-wait completion trace for every shell transport: exit, stdout/stderr bytes, -/// spawn-to-wait wall seconds. Pairs with `render_shell_trace` (pre-spawn). +/// Mirror of `gunbc.observation_seed_render.shell_effect_failed_line`. +/// `argv_collapsed` is WITHOUT the `$ ` prefix (the seed concatenates it into Failed.error). +pub fn render_shell_effect_failed_line_mirror( + argv_summary: &str, + argv_collapsed: &str, + exit_code: u64, + elapsed_ms: u64, + emoji: bool, +) -> String { + let _ = SHELL_CENSUS_MARKER; + let glyph = if emoji { "❌" } else { "✗" }; + format!( + "{glyph} {argv_summary} failed: $ {argv_collapsed} (exit={exit_code}) in {}", + shell_obs_human_duration(elapsed_ms) + ) +} + +// Ambient shell Begin — gated by the installed ShellTrace channel decision +// (Suppressed / Condensed / Full). Tables stay unread for Anomaly; only Ambient +// spawn/done still read channel_decision(ShellTrace). +fn render_shell_trace(argv: &[String]) { + let decision = output_decision(OutputChannel::ShellTrace); + if decision == OutputDecision::Suppressed { + return; + } + // Fallback column bound (no Viewport at the trace site); the single authority is + // `gunbc.output_policy.shell_trace_summary_max_columns`. + const MAX: usize = 100; + let cap = if decision == OutputDecision::Condensed { + Some(MAX) + } else { + None + }; + let summary = shell_argv_summary(argv, cap); + let line = render_shell_effect_begin_line_mirror(&summary, shell_obs_emoji()); + trace_emit(OutputChannel::ShellTrace, &line); +} + +/// Post-wait completion: Ambient Done via ShellTrace, or Anomaly Failed via +/// `effect_stream_disposition` alone (never silenced by ShellTrace Suppressed). +/// Law 4: `group_end` before an Anomaly so it lands OutsideGroup. /// -/// Whether the captured stderr CONTENT is surfaced (tail-bounded) or left as a byte -/// count is the `.dag` authority's `effect_stream_disposition`, keyed on what the -/// caller DECLARED this effect would do: divergence surfaces, agreement counts. At -/// the migration default `ExpectSuccess` that is the previous behaviour exactly — -/// content on non-zero exit (a failing op whose error text is discarded is an -/// undiagnosable failure, DESIGN §5; a whole self-host build failure was once -/// invisible in CI because only `stderr=N bytes` was logged), counts on success so -/// benign compiler warnings do not drown the trace. What the expectation axis adds -/// is the other direction: an effect declared `ExpectFailure` that SUCCEEDS is -/// divergence, and now surfaces instead of passing silently. +/// Carry-forward from 60a4496 as event properties: Failed.error is self-describing +/// `$ (exit=N)`; empty stderr still surfaces via the Failed line alone. +/// Captured stderr CONTENT (when present) follows as a neutralized, tail-bounded +/// block — still gated by SurfaceContent, not by ShellTrace. fn render_shell_completion_trace( expected: ExpectedOutcome, exit_code: i32, - stdout_bytes: usize, + _stdout_bytes: usize, stderr: &[u8], wall: std::time::Duration, + argv: &[String], ) { - trace_emit( - OutputChannel::ShellTrace, - &shell_completion_trace_line(exit_code, stdout_bytes, stderr.len(), wall), - ); let disposition = effect_stream_disposition(expected, exit_code); - if let Some(block) = shell_completion_stderr_trace_block(disposition, exit_code, stderr) { - trace_emit(OutputChannel::ShellTrace, &block); + let emoji = shell_obs_emoji(); + let elapsed_ms = wall.as_millis() as u64; + let collapsed = shell_argv_collapsed(argv); + + if disposition == StreamDisposition::SurfaceContent { + // Anomaly — disposition is the sole gate. Close the host-effects group first + // (idempotent) so law 4 places the failure OutsideGroup. + group_end(); + let summary = format!("$ {collapsed}"); + let code = if exit_code < 0 { + exit_code.unsigned_abs() as u64 + } else { + exit_code as u64 + }; + let line = + render_shell_effect_failed_line_mirror(&summary, &collapsed, code, elapsed_ms, emoji); + eprintln!("{line}"); + if let Some(block) = shell_completion_stderr_content(stderr) { + eprintln!("{block}"); + } + return; + } + + // Ambient Done — ShellTrace-gated (agreement / non-surfacing dispositions). + let decision = output_decision(OutputChannel::ShellTrace); + if decision == OutputDecision::Suppressed { + return; } + const MAX: usize = 100; + let cap = if decision == OutputDecision::Condensed { + Some(MAX) + } else { + None + }; + let summary = shell_argv_summary(argv, cap); + let line = render_shell_effect_done_line_mirror(&summary, elapsed_ms, emoji); + trace_emit(OutputChannel::ShellTrace, &line); } -/// Pure tail-bounding of captured stderr for the completion trace. Returns `None` when the -/// disposition is not `SurfaceContent`, or when there is no stderr to surface; `Some(block)` -/// is the `[shell] stderr` diagnostic, its content tail-bounded with a leading elision marker -/// when it exceeds the cap and every subject line guarded so relayed text cannot mint workflow -/// commands in the parent run. Kept pure (no `trace_emit`) so the surfacing decision is -/// unit-testable with a RED control. -fn shell_completion_stderr_trace_block( - disposition: StreamDisposition, - exit_code: i32, - stderr: &[u8], -) -> Option { - if disposition != StreamDisposition::SurfaceContent || stderr.is_empty() { +/// Whether a SurfaceContent failure should emit a Failed observation line. +/// Pure: disposition alone — never the ShellTrace channel. Empty stderr still +/// returns true (the Failed line is the sole signal). RED control for the +/// installed-policy path: pair with `effect_stream_disposition`. +fn shell_failure_surfaces(disposition: StreamDisposition) -> bool { + disposition == StreamDisposition::SurfaceContent +} + +/// Pure tail-bounding of captured stderr that follows a Failed observation line. +/// Returns `None` when there is no stderr to surface; `Some(block)` is the content +/// only (the Failed line already carries `$ argv (exit=N)`). Subject lines are +/// guarded so relayed text cannot mint workflow commands in the parent run. +fn shell_completion_stderr_content(stderr: &[u8]) -> Option { + if stderr.is_empty() { return None; } const MAX_STDERR_TRACE: usize = 16384; @@ -5880,11 +5981,9 @@ fn shell_completion_stderr_trace_block( String::new() }; Some(format!( - "[shell] stderr (exit={exit_code}):\n{prefix}{}", + "{prefix}{}", // Trailing newlines are stripped before guarding so a subject whose stderr // ends in `\n` (almost all of them) does not render a stray guard-only line. - // This is presentation of the block, not a change to the guard rule: the - // .dag authority still prefixes every line of whatever text it is given. neutralize_workflow_commands(String::from_utf8_lossy(tail).trim_end_matches('\n')) )) } @@ -6010,6 +6109,7 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), + &argv, ); output } else { @@ -6026,6 +6126,7 @@ fn dispatch_shell( output.stdout.len(), &output.stderr, wall_start.elapsed(), + &argv, ); output }; @@ -10122,13 +10223,16 @@ mod dispatch_rest_decision_tests { mod shell_completion_trace_tests { use super::hermetic_checkout_read_disposition_under; use super::neutralize_workflow_commands; - use super::shell_completion_stderr_trace_block; - use super::shell_completion_trace_line; + use super::render_shell_effect_begin_line_mirror; + use super::render_shell_effect_done_line_mirror; + use super::render_shell_effect_failed_line_mirror; + use super::shell_argv_collapsed; + use super::shell_completion_stderr_content; + use super::shell_failure_surfaces; use super::{ effect_stream_disposition, ExpectedOutcome, StreamDisposition, EFFECT_STREAM_POLICY_FALLBACK, SUBJECT_LINE_GUARD_FALLBACK, }; - use std::time::Duration; /// Convenience for the tests below: the disposition a failing effect gets when /// its caller declared success — the migration default, and what every call @@ -10137,61 +10241,95 @@ mod shell_completion_trace_tests { StreamDisposition::SurfaceContent } + fn av(parts: &[&str]) -> Vec { + parts.iter().map(|s| s.to_string()).collect() + } + + #[test] + fn shell_effect_begin_mirror_formats_started_subject() { + let line = render_shell_effect_begin_line_mirror("$ echo hi", true); + assert_eq!(line, "🔄 started $ echo hi"); + let unicode = render_shell_effect_begin_line_mirror("$ true", false); + assert_eq!(unicode, "◐ started $ true"); + } + + #[test] + fn shell_effect_done_mirror_formats_duration() { + let line = render_shell_effect_done_line_mirror("$ true", 5150, true); + assert_eq!(line, "✅ $ true done in 5 seconds"); + } + #[test] - fn shell_completion_trace_line_formats_exit_stdout_stderr_wall() { - let line = shell_completion_trace_line(0, 1234, 56, Duration::from_millis(5150)); + fn shell_effect_failed_mirror_is_self_describing() { + // Failed.error carries `$ (exit=N)` so the line stands alone when + // stderr is empty (the common CI miss). + let line = render_shell_effect_failed_line_mirror("$ echo hi", "echo hi", 1, 2000, true); + assert_eq!(line, "❌ $ echo hi failed: $ echo hi (exit=1) in 2 seconds"); + } + + #[test] + fn shell_argv_collapsed_squeezes_whitespace() { assert_eq!( - line, - "[shell] done exit=0 stdout=1234 stderr=56 bytes wall=5.150s" + shell_argv_collapsed(&av(&["sh", "-c", "echo hi\nthere"])), + "sh -c echo hi there" ); } #[test] - fn stderr_block_surfaces_content_on_nonzero_exit() { - let block = - shell_completion_stderr_trace_block(surfacing(), 101, b"error: manifest not found\n") - .expect("non-zero exit with stderr must surface a diagnostic block"); - assert!(block.starts_with("[shell] stderr (exit=101):\n")); + fn stderr_content_surfaces_body_on_nonzero_exit() { + let block = shell_completion_stderr_content(b"error: manifest not found\n") + .expect("non-empty stderr must surface a content block"); assert!(block.contains("error: manifest not found")); + assert!(!block.contains("[shell]")); } #[test] - fn stderr_block_none_when_disposition_is_not_surface_content() { - // RED control: the block is gated on the .dag disposition, not on a local - // `exit != 0` re-derivation — a non-surfacing disposition yields nothing even - // with a non-zero exit and non-empty stderr, and an empty stderr yields - // nothing even when the disposition says surface. - assert_eq!( - shell_completion_stderr_trace_block( - StreamDisposition::SummarizeCounts, - 1, - b"error: real failure\n" - ), - None - ); - assert_eq!( - shell_completion_stderr_trace_block( - StreamDisposition::StreamSuppressed, - 1, - b"error: real failure\n" - ), - None - ); - assert_eq!( - shell_completion_stderr_trace_block(surfacing(), 1, b""), - None + fn stderr_content_none_when_empty() { + assert_eq!(shell_completion_stderr_content(b""), None); + } + + #[test] + fn failure_surfaces_from_disposition_alone_not_channel() { + // RED control: the Failed observation is gated on the .dag disposition, not + // on a local `exit != 0` re-derivation — and empty stderr does NOT suppress it + // (the Failed line is the sole signal once Ambient counts are observation Done). + assert!(shell_failure_surfaces(surfacing())); + assert!(!shell_failure_surfaces(StreamDisposition::SummarizeCounts)); + assert!(!shell_failure_surfaces(StreamDisposition::StreamSuppressed)); + } + + #[test] + fn at_normal_a_failing_effect_surfaces_its_command_a_passing_one_is_silent() { + // Composes the .dag disposition (ExpectSuccess × exit → the Normal four + // corners via the uninstalled fallback that mirrors Normal) with the + // failure-surfaces predicate. GREEN: passing → SummarizeCounts → silent. + // Discriminating RED: failing → SurfaceContent → Failed line must fire + // even with empty stderr (self-describing `$ argv`). + assert!(!shell_failure_surfaces(effect_stream_disposition( + ExpectedOutcome::ExpectSuccess, + 0 + ))); + assert!(shell_failure_surfaces(effect_stream_disposition( + ExpectedOutcome::ExpectSuccess, + 1 + ))); + let collapsed = shell_argv_collapsed(&av(&["git", "rev-parse", "--show-toplevel"])); + let line = render_shell_effect_failed_line_mirror( + &format!("$ {collapsed}"), + &collapsed, + 1, + 0, + false, ); + assert!(line.contains("failed: $ git rev-parse --show-toplevel (exit=1)")); + assert_eq!(shell_completion_stderr_content(b""), None); } #[test] - fn stderr_block_tail_bounds_and_marks_elision() { + fn stderr_content_tail_bounds_and_marks_elision() { let big = vec![b'x'; 16384 + 500]; - let block = shell_completion_stderr_trace_block(surfacing(), 1, &big) - .expect("oversized stderr surfaces"); + let block = shell_completion_stderr_content(&big).expect("oversized stderr surfaces"); assert!(block.contains("<500 earlier stderr bytes elided>")); - // Only the 16384-byte tail is carried, not the full 16884-byte body: the trailing - // contiguous run of stderr bytes is exactly the cap. The guard prefix is a - // line-initial insertion, so it does not disturb the trailing run. assert_eq!(block.chars().rev().take_while(|c| *c == 'x').count(), 16384); } @@ -10200,13 +10338,11 @@ mod shell_completion_trace_tests { // The priced incident: a child's stderr legitimately carrying `::error::` // annotated the PARENT run as failing. Every relayed line is guarded, so no // subject text can occupy the line-initial `::` position GitHub parses. - let block = shell_completion_stderr_trace_block( - surfacing(), - 1, + let block = shell_completion_stderr_content( b"::error::build verification: artifact absent\n::warning::next\n", ) .expect("failing effect surfaces its stderr"); - for line in block.lines().skip(1) { + for line in block.lines() { assert!( !line.trim_start().starts_with("::"), "relayed subject line is command-bearing: {line:?}" @@ -10259,15 +10395,6 @@ mod shell_completion_trace_tests { ); } - #[test] - fn shell_completion_trace_line_surfaces_nonzero_exit() { - let line = shell_completion_trace_line(1, 0, 4096, Duration::from_secs(2)); - assert_eq!( - line, - "[shell] done exit=1 stdout=0 stderr=4096 bytes wall=2.000s" - ); - } - #[test] fn hermetic_checkout_read_admits_relative_path_under_root() { let dir = From 60a3d3c003c52b50d810d6dcb4aadfadde773b4f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 03:31:21 +0000 Subject: [PATCH 30/39] Fix GeneratedArtifact name collision in observation_model witnesses Whole-tree compile-clean failed: bare SelectionNoOp.GeneratedArtifact collided with v2.std.artifact.GeneratedArtifact and gunbc.generated_artifact.GeneratedArtifact. Mint via selection_noop_generated_artifact in the defining module so the variant resolves unambiguously. Co-authored-by: Brian Searls --- dag/std/observation.dag | 6 ++++++ dag/test/claim/observation_model_witness_test.dag | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/dag/std/observation.dag b/dag/std/observation.dag index 797083831c4..3c3165679a6 100644 --- a/dag/std/observation.dag +++ b/dag/std/observation.dag @@ -390,6 +390,12 @@ type SelectionNoOp data observation_selection_no_op_note: String = "A touched file that selected no work is a CLOSED SUM, never a bare nothing. Nothing meaning five different things is the state-space conflation the house splits into named variants, and here the remedies genuinely differ: docs select no executable work and that is correct and quiet; a touched declaration no witness references is a COVERAGE GAP surfaced at review time, so it renders as a visible nudge rather than a comfort; only comments or whitespace intersected is fine and says so; a generated artifact is checked by the drift gate against its authority, not by witnesses, so nothing must never appear beside it; and a departed path is NOT a no-op at all — a deleted module cannot be scoped, so selection widens to baseline and the line says why. Because the sum is closed and the presentation assignment below is total, an unlabelled nothing is UNWRITABLE rather than censused after the fact — construction where a census would have conceded the bad line was writable." +data selection_noop_generated_artifact_mint_note: String = "Mint helper for the SelectionNoOp.GeneratedArtifact arm. The bare constructor name `GeneratedArtifact` collides under whole-tree compile-clean with v2.std.artifact.GeneratedArtifact and gunbc.generated_artifact.GeneratedArtifact — three unrelated carriers that share a noun. Callers outside this module construct via this helper so the variant resolves in the defining module where the name is unambiguous." + +fn selection_noop_generated_artifact(drift_gate_authority: NonEmptyStr) -> SelectionNoOp { + GeneratedArtifact { drift_gate_authority: drift_gate_authority } +} + fn observation_no_op_is_actually_a_widen(n: SelectionNoOp) -> Bool { match n { DocsPolicy => false diff --git a/dag/test/claim/observation_model_witness_test.dag b/dag/test/claim/observation_model_witness_test.dag index 7d61d907250..db270fe484b 100644 --- a/dag/test/claim/observation_model_witness_test.dag +++ b/dag/test/claim/observation_model_witness_test.dag @@ -124,8 +124,8 @@ import std.observation { DocsPolicy, Uncovered, NoDeclsTouched, - GeneratedArtifact, DepartedPath, + selection_noop_generated_artifact, observation_no_op_is_actually_a_widen, observation_no_op_attention, observation_no_op_symbol, @@ -481,7 +481,7 @@ fn w_no_op_kinds_each_explain_themselves() -> Bool { DocsPolicy, Uncovered { declaration: "std.observation.observation_attention" as NonEmptyStr }, NoDeclsTouched, - GeneratedArtifact { drift_gate_authority: "gunbc.ci_workflow" as NonEmptyStr }, + selection_noop_generated_artifact(drift_gate_authority: "gunbc.ci_workflow" as NonEmptyStr), DepartedPath { cause: "src/v1/stage0/src/gone.rs" as NonEmptyStr } ] count(kinds) == 5 && @@ -505,7 +505,7 @@ fn w_departed_path_is_not_a_no_op() -> Bool { observation_no_op_is_actually_a_widen(n: DepartedPath { cause: "deleted module" as NonEmptyStr }) && !observation_no_op_is_actually_a_widen(n: DocsPolicy) && !observation_no_op_is_actually_a_widen(n: NoDeclsTouched) && - !observation_no_op_is_actually_a_widen(n: GeneratedArtifact { drift_gate_authority: "x" as NonEmptyStr }) && + !observation_no_op_is_actually_a_widen(n: selection_noop_generated_artifact(drift_gate_authority: "x" as NonEmptyStr)) && !observation_no_op_is_actually_a_widen(n: Uncovered { declaration: "x" as NonEmptyStr }) && observation_no_op_attention(n: DepartedPath { cause: "deleted module" as NonEmptyStr }) == Notable } From b613b3fc06060c060c40c084e843f69dabb08e33 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 05:37:44 +0000 Subject: [PATCH 31/39] Observation aesthetics: named-intent shell, glyph discipline, census bidir, failure-receipt miss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator live-log review (run 30142403230) — four pieces in one pass: - Shell subjects are typed service.op intents; argv only in Failed.error. Ambient ShellTrace is Suppressed at Normal (silent scaffolding); Anomaly still surfaces via divergence alone (Quiet no longer forces StreamSuppressed). - Governor receipt uses StatusPulse (not Done glyph); peak RSS / governor / cgroup wrap in one "floor receipts" group. - Census roster grows four CountedFrontierSite rows ([floor-drain], [gate-warm-cost], [receipt], [file]) with a bidirectional hygiene witness. - Undeclared *_failure_receipt companions (NoMainFunction) treat as empty detail instead of stuffing failure_receipt_refused onto ordinary Bool(false) reds. Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 52 ++++++- dag/gunbc/observation_seed_render.dag | 18 +-- dag/gunbc/output_policy.dag | 8 +- .../observation_emit_census_witness_test.dag | 26 +++- dag/test/claim/output_policy_witness_test.dag | 20 ++- src/v1/stage0/src/bin/claim_executor.rs | 19 +-- src/v1/stage0/src/cli_run.rs | 8 +- src/v1/stage0/src/memory_governor.rs | 7 +- src/v1/stage0/src/v1_interpreter.rs | 137 +++++++----------- 9 files changed, 164 insertions(+), 131 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index 146dc8cd604..26ddf0b1847 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -5,9 +5,9 @@ import std.nat { Nat } import std.disposition { Disposition, Terminal, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } -data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness reads the live seed and reds when a rostered marker vanishes (a stale roster) or when a family the census claims is migrated still emits its raw form, so the census cannot rot into a lie." +data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness is bidirectional: it reds when a rostered marker vanishes AND when a known live tag family is missing from the roster, so growth after the snapshot cannot silently understate." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and design section 6b: the CI two-tier rework (PR-1 on #7162's line) rewrote the floor's emit sites, so the tagged structured-tag families migrate on this PR. Migrated: [floor-memory] (4b), [gantt], [governor], [typecheck-attribution], [measurement] (ci_measurement_rss_line / render_peak_rss_line_mirror), [shell] (shell_effect_*_line / render_shell_effect_*_line_mirror; Ambient via ShellTrace, Anomaly via disposition alone). Tagged frontier count is now 0. The unmarked raw eprintln sites in claim_executor remain the residue — currently 76 `eprintln!` call sites by live count — their per-print classification is the re-census this PR's dissolve condition named." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and live-log review (run 30142403230): tagged migrations landed (floor-memory, gantt, governor, typecheck-attribution, measurement, shell with named-intent subjects; Ambient ShellTrace Suppressed at Normal). Post-census growth tags ([floor-drain], [gate-warm-cost], [receipt], [file]) are CountedFrontierSite rows so the roster cannot silently understate. Unmarked raw eprintln residue in claim_executor remains counted separately." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -65,7 +65,43 @@ data shell_site: CensusedEmitSite = CensusedEmitSite { marker: "[shell]" as NonEmptyStr, source_file: "src/v1/stage0/src/v1_interpreter.rs" as NonEmptyStr, disposition: MigratedToObservation { - via: "gunbc.observation_ci_render.ci_event_line (seed shell_effect_begin_line / shell_effect_done_line / shell_effect_failed_line; mirror render_shell_effect_*_line_mirror; Ambient via ShellTrace, Anomaly Failed self-describing via disposition alone)" as NonEmptyStr + via: "gunbc.observation_ci_render.ci_event_line (seed shell_effect_begin_line / shell_effect_done_line / shell_effect_failed_line; mirror render_shell_effect_*_line_mirror; named-intent subject; Ambient ShellTrace Suppressed-at-Normal; Anomaly Failed via disposition alone)" as NonEmptyStr + } +} + +data floor_drain_site: CensusedEmitSite = CensusedEmitSite { + marker: "[floor-drain]" as NonEmptyStr, + source_file: "src/v1/stage0/src/cli_run.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "schedule-retention / typed-cache drain diagnostics born after the initial census snapshot — retention kit instrumentation, not yet an observation projection" as NonEmptyStr, + dissolve_on: "re-census into MigratedToObservation once drain events speak the observation vocabulary (or retire the tag into a named group summary)" as NonEmptyStr + } +} + +data gate_warm_cost_site: CensusedEmitSite = CensusedEmitSite { + marker: "[gate-warm-cost]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "per-gate warm-cost TSV probe lines — the instrument producing the fleet receipt basis, still a raw tag family" as NonEmptyStr, + dissolve_on: "fold the TSV emit into an observation MeasurementSegment / receipt projection and retire the bracket tag" as NonEmptyStr + } +} + +data receipt_site: CensusedEmitSite = CensusedEmitSite { + marker: "[receipt]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "floor resolve / batch-wall / materialization receipt lines — data, not outcomes; still bracket-tagged" as NonEmptyStr, + dissolve_on: "collapse into the floor-receipts observation group as Ambient MeasurementSegment / StatusPulse lines" as NonEmptyStr + } +} + +data file_trace_site: CensusedEmitSite = CensusedEmitSite { + marker: "[file]" as NonEmptyStr, + source_file: "src/v1/stage0/src/v1_interpreter.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "Filesystem transport host-effect traces — sibling of the shell-echo class, still raw-tagged" as NonEmptyStr, + dissolve_on: "same named-intent ObservationEvent path as shell (Filesystem.Read/Write/…) once shell grain is proven" as NonEmptyStr } } @@ -75,7 +111,11 @@ data observation_emit_roster: List = [ gantt_site, governor_site, measurement_site, - shell_site + shell_site, + floor_drain_site, + gate_warm_cost_site, + receipt_site, + file_trace_site ] data observation_emit_roster_completeness_disposition: Disposition = Scaffold { @@ -87,7 +127,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "Tagged structured-tag families are now all MigratedToObservation (frontier count 0). The roster does NOT yet enumerate the 76 unmarked raw eprintln sites in claim_executor as individual CountedFrontierSite rows — that per-print re-census is the remaining completeness scaffold, and its dissolve condition (\"when the post-rework sites are enumerated\") is satisfied by this PR's landing of the tagged migrations, so the enumeration itself is the next micro-pass on this bar. Until those rows land the honest count is: zero tagged frontier, six migrated families, 76 raw-print residue counted but unclassified, and the witness holds the roster against the seed so it cannot go stale." +data observation_emit_roster_completeness_note: String = "Roster is bidirectional: every rostered marker must still exist in the seed, AND every known live tag family born after the initial snapshot must appear on the roster (operator finding 3, run 30142403230). Frontier count = 4 (floor-drain, gate-warm-cost, receipt, file). Six families MigratedToObservation. The unmarked eprintln sites remain the per-print completeness scaffold." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { @@ -109,5 +149,5 @@ fn observation_emit_frontier_count() -> Nat { } data observation_emit_census_disposition: Disposition = Terminal { - reason: "The census MODEL is Terminal — a closed classification of emit sites is the wall's authority, not an interim stand-in. What is a scaffold is the ROSTER's completeness (declared above), because the site list grows when the rework's post-rewrite sites are enumerated. The model that classifies them does not change; only the rows do." + reason: "The census MODEL is Terminal — a closed classification of emit sites is the wall's authority, not an interim stand-in. What is a scaffold is the ROSTER's completeness (declared above), because the site list grows when post-rewrite sites are enumerated. The model that classifies them does not change; only the rows do." } diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 2dedbf0dc4e..3e6861c65e5 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -194,15 +194,15 @@ fn seed_peak_rss_line(label: NonEmptyStr, rss_bytes: Nat, rss_available: Bool, e ) } -data seed_shell_effect_note: String = "Shell host-effect projection (operator steer 2026-07-25): effects become ObservationEvents with per-event attention — routine Begin/Concluded\{Done\} Ambient (collapses inside the host-effects group); failing Concluded\{Failed\} Anomaly (OutsideGroup by law 4). Carry-forward from 60a4496 as event properties: Failed.error is self-describing `$ (exit=N)` whitespace-collapsed; empty stderr still surfaces via the Failed line alone. ShellTrace channel decision and effect_stream_disposition tables stay unread-from for Anomaly gating differently: Ambient spawn/done still READ channel_decision(ShellTrace); Anomaly surfaces from SurfaceContent disposition alone and is never silenced by ShellTrace Suppressed. Hot-path mirrors in v1_interpreter; oracle RED keeps them honest." +data seed_shell_effect_note: String = "Shell host-effect projection (operator live-log 2026-07-25 acceptance): subjects are NAMED INTENTS — the typed service.op key (`git.Core.HeadCommit`, `shell.Env.Get`), never raw `$ argv`. Argv is demoted to Failed.error (`$ (exit=N)`, whitespace-collapsed) and the Verbose/title channel. Routine Begin/Concluded\{Done\} are Ambient (ShellTrace-gated; Suppressed at Normal so scaffolding collapses); failing Concluded\{Failed\} is Anomaly (OutsideGroup by law 4), gated by divergence disposition alone — channel Suppressed must never silence a failure. Hot-path mirrors in v1_interpreter; oracle RED keeps them honest." -fn seed_shell_subject(argv_summary: NonEmptyStr) -> ObservationSubject { - ObservationSubject { segments: [ PhaseSegment { name: argv_summary } ] } +fn seed_shell_subject(intent: NonEmptyStr) -> ObservationSubject { + ObservationSubject { segments: [ PhaseSegment { name: intent } ] } } -fn shell_effect_begin_line(argv_summary: NonEmptyStr, overhead_ms: Nat, emoji: Bool) -> String { +fn shell_effect_begin_line(intent: NonEmptyStr, overhead_ms: Nat, emoji: Bool) -> String { let event = ObservationEvent { - subject: seed_shell_subject(argv_summary: argv_summary), + subject: seed_shell_subject(intent: intent), transition: Begin, wall: MeasuredUnavailable { cause: "begin has no elapsed yet" as NonEmptyStr }, rss: MeasuredUnavailable { cause: "not sampled at shell spawn" as NonEmptyStr } @@ -213,9 +213,9 @@ fn shell_effect_begin_line(argv_summary: NonEmptyStr, overhead_ms: Nat, emoji: B ) } -fn shell_effect_done_line(argv_summary: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { +fn shell_effect_done_line(intent: NonEmptyStr, elapsed_ms: Nat, overhead_ms: Nat, emoji: Bool) -> String { let event = ObservationEvent { - subject: seed_shell_subject(argv_summary: argv_summary), + subject: seed_shell_subject(intent: intent), transition: Concluded { outcome: Done }, wall: MeasuredValue { value: millisecond(count: elapsed_ms) }, rss: MeasuredUnavailable { cause: "not sampled at shell completion" as NonEmptyStr } @@ -227,7 +227,7 @@ fn shell_effect_done_line(argv_summary: NonEmptyStr, elapsed_ms: Nat, overhead_m } fn shell_effect_failed_line( - argv_summary: NonEmptyStr, + intent: NonEmptyStr, argv_collapsed: NonEmptyStr, exit_code: Nat, elapsed_ms: Nat, @@ -239,7 +239,7 @@ fn shell_effect_failed_line( concat(argv_collapsed, concat(" (exit=", concat(to_string(exit_code), ")"))) ) as NonEmptyStr let event = ObservationEvent { - subject: seed_shell_subject(argv_summary: argv_summary), + subject: seed_shell_subject(intent: intent), transition: Concluded { outcome: Failed { error: error, output: "" } }, diff --git a/dag/gunbc/output_policy.dag b/dag/gunbc/output_policy.dag index b3a379881dc..f956e016be3 100644 --- a/dag/gunbc/output_policy.dag +++ b/dag/gunbc/output_policy.dag @@ -53,7 +53,7 @@ fn channel_decision(channel: OutputChannel, verbosity: Verbosity) -> OutputDecis } ShellTrace => match verbosity { Quiet => Suppressed - Normal => Condensed + Normal => Suppressed Verbose => Full } Instrumentation => match verbosity { @@ -141,7 +141,7 @@ fn divergence_disposition(diverges: Bool) -> StreamDisposition { } } -data effect_stream_dispatch_note: String = "Verbosity and divergence compose rather than compete: Quiet suppresses the stream whatever the outcome, Verbose surfaces it whatever the outcome (the operator asked for everything), and the divergence rule decides the Normal middle — which is where CI runs. So the channel's existing authority is consumed, not re-derived, and the new axis only refines the arm that was previously fixed." +data effect_stream_dispatch_note: String = "Observation dissolve (operator live-log 2026-07-25): Ambient ShellTrace silence at Normal must not force StreamSuppressed — Anomaly Failed still surfaces via divergence alone. Quiet no longer swallows divergence either (DESIGN §5: a failing effect is never silent). Verbose still surfaces every stream (Full). The channel grades Ambient scaffolding; the stream policy grades subject content; they compose rather than conflate." fn effect_stream_disposition( channel: OutputChannel, @@ -150,7 +150,7 @@ fn effect_stream_disposition( observed: ObservedOutcome ) -> StreamDisposition { match channel_decision(channel: channel, verbosity: verbosity) { - Suppressed => StreamSuppressed + Suppressed => divergence_disposition(diverges: outcome_diverges(expected: expected, observed: observed)) Condensed => divergence_disposition(diverges: outcome_diverges(expected: expected, observed: observed)) Full => SurfaceContent } @@ -202,4 +202,4 @@ fn resolve_shell_trace_stream_policy(verbose: Bool, quiet: Bool) -> EffectStream shell_trace_stream_policy(verbosity: resolve_verbosity(env: VerbosityEnv { verbose: verbose, quiet: quiet })) } -data shell_trace_observation_convergence_note: String = "CONVERGENCE (operator steer 2026-07-25, post-60a4496 revert): ShellTrace at Normal Condensed and effect_stream_disposition remain the channel-layer split until shell host-effects emit as ObservationEvents with per-event attention (routine Ambient / failure Anomaly). When the event path carries the traffic — Begin/Concluded projections, Anomaly OutsideGroup by law 4, self-describing $ argv and empty-stderr exit on Failed — this channel-level split dissolves: delete this note, retire Condensed-at-Normal as the Ambient gate, and let derived attention plus ci_line_placement supersede the dual-class conflation. Do NOT build an interim disposition↔channel decoupling; the observation wiring is the dissolve." +data shell_trace_observation_convergence_note: String = "DISSOLVED (operator live-log 2026-07-25, run 30142403230): shell host-effects now emit as ObservationEvents with named-intent subjects (service.op); Ambient Begin/Done are ShellTrace-gated (Suppressed at Normal — silent scaffolding); Anomaly Failed surfaces from divergence disposition alone (channel Suppressed no longer forces StreamSuppressed). Derived attention + ci_line_placement supersede the dual-class conflation. Keep this note as the dissolve receipt; delete when a later census no longer needs the history." diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 944865c3d39..1ec4c39ca2e 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -19,6 +19,10 @@ import gunbc.observation_emit_census { typecheck_attribution_site, measurement_site, shell_site, + floor_drain_site, + gate_warm_cost_site, + receipt_site, + file_trace_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree @@ -107,9 +111,22 @@ fn w_shell_has_migrated() -> Bool { string_contains(s: emit_site_dissolve_on(site: shell_site), pattern: "render_shell_effect_") } -fn w_the_census_states_a_zero_tagged_frontier_count() -> Bool { - observation_emit_frontier_count() == 0 && - count(observation_emit_roster) == 6 +fn w_the_census_states_post_snapshot_frontier_count() -> Bool { + observation_emit_frontier_count() == 4 && + count(observation_emit_roster) == 10 +} + +data w_post_snapshot_live_tags_are_rostered_bidirectional_note: String = "Operator finding 3 (run 30142403230): hygiene is bidirectional — growth tags born after the initial census snapshot must appear on the roster, not only the reverse (rostered markers still exist)." + +fn w_post_snapshot_live_tags_are_rostered_bidirectional() -> Bool { + census_marker_present(site: floor_drain_site) && + emit_site_is_frontier(site: floor_drain_site) && + census_marker_present(site: gate_warm_cost_site) && + emit_site_is_frontier(site: gate_warm_cost_site) && + census_marker_present(site: receipt_site) && + emit_site_is_frontier(site: receipt_site) && + census_marker_present(site: file_trace_site) && + emit_site_is_frontier(site: file_trace_site) } fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { @@ -175,7 +192,8 @@ test fn observation_emit_census_witnesses() -> Bool { w_typecheck_attribution_has_migrated() && w_measurement_has_migrated() && w_shell_has_migrated() && - w_the_census_states_a_zero_tagged_frontier_count() && + w_the_census_states_post_snapshot_frontier_count() && + w_post_snapshot_live_tags_are_rostered_bidirectional() && w_the_raw_byte_dump_shape_is_gone_from_the_seed() && w_the_raw_gantt_shape_is_gone_from_the_seed() && w_the_raw_governor_shape_is_gone_from_the_seed() && diff --git a/dag/test/claim/output_policy_witness_test.dag b/dag/test/claim/output_policy_witness_test.dag index f0cd03f0d72..f7488a69f5e 100644 --- a/dag/test/claim/output_policy_witness_test.dag +++ b/dag/test/claim/output_policy_witness_test.dag @@ -35,8 +35,8 @@ fn w_instrumentation_is_debug_only() -> Bool { dec_is(d: channel_decision(channel: Instrumentation, verbosity: Verbose), suppressed: false, condensed: false, full: true) } -fn w_shell_trace_condenses_at_normal() -> Bool { - dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: false, condensed: true, full: false) && +fn w_shell_trace_is_debug_only_at_normal() -> Bool { + dec_is(d: channel_decision(channel: ShellTrace, verbosity: Normal), suppressed: true, condensed: false, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Quiet), suppressed: true, condensed: false, full: false) && dec_is(d: channel_decision(channel: ShellTrace, verbosity: Verbose), suppressed: false, condensed: false, full: true) } @@ -71,7 +71,7 @@ fn w_resolve_channel_policy_normal_bundles_decisions() -> Bool { dec_is(d: p.diagnostic, suppressed: false, condensed: false, full: true) && dec_is(d: p.claim_result, suppressed: false, condensed: false, full: true) && dec_is(d: p.progress, suppressed: false, condensed: true, full: false) && - dec_is(d: p.shell_trace, suppressed: false, condensed: true, full: false) && + dec_is(d: p.shell_trace, suppressed: true, condensed: false, full: false) && dec_is(d: p.instrumentation, suppressed: true, condensed: false, full: false) } @@ -138,11 +138,15 @@ fn w_neutralization_is_the_only_route_to_surfaced_text() -> Bool { subject_text_line_guard == "| " } -fn w_quiet_suppresses_and_verbose_surfaces_regardless_of_outcome() -> Bool { +data w_quiet_ambient_silent_but_divergence_still_surfaces_note: String = "Observation dissolve: Quiet suppresses Ambient ShellTrace scaffolding, but stream disposition follows divergence — Anomaly is never silenced by channel Suppressed (DESIGN section 5). Verbose still surfaces every stream (Full)." + +fn w_quiet_ambient_silent_but_divergence_still_surfaces() -> Bool { disp_is(d: shell_stream(verbosity: Quiet, expected: ExpectFailure, observed: ObservedSuccess), - surface: false, counts: false, suppressed: true) && + surface: true, counts: false, suppressed: false) && disp_is(d: shell_stream(verbosity: Quiet, expected: ExpectSuccess, observed: observed_outcome_from_exit(exit: 1)), - surface: false, counts: false, suppressed: true) && + surface: true, counts: false, suppressed: false) && + disp_is(d: shell_stream(verbosity: Quiet, expected: ExpectSuccess, observed: ObservedSuccess), + surface: false, counts: true, suppressed: false) && disp_is(d: shell_stream(verbosity: Verbose, expected: ExpectSuccess, observed: ObservedSuccess), surface: true, counts: false, suppressed: false) && disp_is(d: shell_stream(verbosity: Verbose, expected: ExpectFailure, observed: observed_outcome_from_exit(exit: 1)), @@ -178,7 +182,7 @@ test fn output_policy_effect_stream_witnesses() -> Bool { w_red_control_that_unexpectedly_succeeds_surfaces() && w_surfaced_subject_text_cannot_mint_annotations() && w_neutralization_is_the_only_route_to_surfaced_text() && - w_quiet_suppresses_and_verbose_surfaces_regardless_of_outcome() && + w_quiet_ambient_silent_but_divergence_still_surfaces() && w_effect_stream_disposition_is_exit_invariant() && w_expectation_axis_is_load_bearing_red_control() && w_shell_trace_stream_policy_projects_the_four_corners() @@ -187,7 +191,7 @@ test fn output_policy_effect_stream_witnesses() -> Bool { test fn output_policy_witnesses() -> Bool { w_diagnostic_never_hidden() && w_instrumentation_is_debug_only() && - w_shell_trace_condenses_at_normal() && + w_shell_trace_is_debug_only_at_normal() && w_claim_result_shown_at_normal_hidden_at_quiet() && w_progress_condenses_at_normal() && w_verbose_wins_over_quiet() && diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 940e77d4ad7..90830cedaa6 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -2889,6 +2889,9 @@ fn run() -> Result { batch_clamp_params.as_deref(), budget_tighten_ms, ); + // Floor receipts block — data, not outcomes. One named group; pulse glyphs only + // (operator live-log 2026-07-25: outcome glyphs for outcomes only). + v1_compiler::v1_interpreter::group_begin("floor receipts"); match peak_rss_bytes() { Some(bytes) => { eprintln!( @@ -2912,11 +2915,12 @@ fn run() -> Result { // The governor receipt is the §5-counted degradation story for the run: every graceful // hold, hard back-off, and forced-serial admission, beside the width actually reached. eprintln!("{}", governor.receipt_line()); - // [measurement] WHOLE-TREE cgroup peak — the SOUND placement divisor input (SELF-RSS above omits + // WHOLE-TREE cgroup peak — the SOUND placement divisor input (SELF-RSS above omits // child rustc/sccache PIDs; cgroup-v2 `memory.peak` at the leaf job cgroup is hierarchical and // captures them). Single authority `emit_cgroup_measurement` so the `ci` and `rust_tests` jobs // report an identically-shaped line. Runtime-harmless read-only. emit_cgroup_measurement("floor adaptive-width"); + v1_compiler::v1_interpreter::group_end(); floor_terminal_fast_exit(walk_exit_code(outcome.any_failed)) } @@ -3515,7 +3519,7 @@ mod tests { fn run_seed_shell_effect_failed_line( source_roots: &[String], - argv_summary: &str, + intent: &str, argv_collapsed: &str, exit_code: u64, elapsed_ms: u64, @@ -3534,10 +3538,7 @@ mod tests { &ctx, "shell_effect_failed_line", &[ - ( - Some("argv_summary".to_string()), - Value::Str(argv_summary.to_string()), - ), + (Some("intent".to_string()), Value::Str(intent.to_string())), ( Some("argv_collapsed".to_string()), Value::Str(argv_collapsed.to_string()), @@ -3571,7 +3572,7 @@ mod tests { ]; let oracle = run_seed_shell_effect_failed_line( &roots, - "$ echo hi", + "shell.Exec.Run", "echo hi", 1, 2000, @@ -3580,7 +3581,7 @@ mod tests { ) .expect("shell_effect_failed_line must resolve and render"); let mirror = v1_compiler::v1_interpreter::render_shell_effect_failed_line_mirror( - "$ echo hi", + "shell.Exec.Run", "echo hi", 1, 2000, @@ -3592,7 +3593,7 @@ mod tests { ); assert_eq!( oracle, - "❌ $ echo hi failed: $ echo hi (exit=1) in 2 seconds" + "❌ shell.Exec.Run failed: $ echo hi (exit=1) in 2 seconds" ); } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index a2cabd2195c..03e1bc05e4c 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9967,8 +9967,11 @@ fn failure_receipt_companion(function: &str) -> Option { } /// Run a witness companion that returns `String` divergence detail (Lane B agreement loudness). -/// Empty string = no divergence detail (clean companion). Non-empty refusal sentinel on -/// interpreter error or wrong type — never silent None (review 41847, §5). +/// Empty string = no divergence detail. An undeclared companion (`NoMainFunction` — the +/// lookup miss misnamed for historical reasons) is treated as absent, not a refusal: +/// almost no `*_holds` declares a companion, and stuffing `failure_receipt_refused: no main +/// function found` onto every ordinary Bool(false) red defeats the point of the receipt. +/// Wrong-type / real interp errors still refuse loudly (§5 / #7199 Finding 3). pub fn run_claim_failure_receipt(ctx: &v1_interpreter::InterpContext, function: &str) -> String { match v1_interpreter::run_in_context(ctx, function, false) { Ok(v1_interpreter::Value::Str(s)) => s, @@ -9976,6 +9979,7 @@ pub fn run_claim_failure_receipt(ctx: &v1_interpreter::InterpContext, function: "failure_receipt_refused: {function} returned {}, expected String", ctx.format_value(&other) ), + Err(v1_interpreter::InterpError::NoMainFunction) => String::new(), Err(e) => format!("failure_receipt_refused: {function}: {e}"), } } diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 989ab8d19b6..74385417115 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -68,13 +68,14 @@ fn mirror_ci_human_percent(bp: u64) -> String { } fn render_governor_info_line(text: &str, emoji: bool) -> String { - let glyph = if emoji { "🔄" } else { "◐" }; + let glyph = if emoji { "🕐" } else { "◷" }; format!("{glyph} {text}") } fn render_governor_done_line(text: &str, emoji: bool) -> String { - let glyph = if emoji { "✅" } else { "✓" }; - format!("{glyph} {text}") + // Glyph discipline (operator live-log 2026-07-25): a receipt is *data*, not an + // outcome — StatusPulse, never the Done/success glyph. + render_governor_info_line(text, emoji) } /// Multiplicative-decrease divisor is 2 (halve), additive increase is +1 — classic AIMD. diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 8df79eb5788..ec612246164 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5057,7 +5057,7 @@ fn wet_service_call( msg: format!("no transport for service {}", key), })?; let param_env = build_service_param_env(op_node, args, env, ctx)?; - dispatch_service_wet(service_node, op_node, transport, ¶m_env, ctx) + dispatch_service_wet(service_node, op_node, transport, ¶m_env, ctx, &key) } fn unix_secs_from_clock_value( @@ -5238,7 +5238,14 @@ fn eval_service_call( .map(|requested| hermetic_checkout_read_disposition(&requested).is_ok()) .unwrap_or(false); if confirmed_checkout_input { - return dispatch_service_wet(service_node, op_node, transport, ¶m_env, ctx); + return dispatch_service_wet( + service_node, + op_node, + transport, + ¶m_env, + ctx, + &key, + ); } } let published = ctx.published_mock_keys()?; @@ -5283,7 +5290,7 @@ fn eval_service_call( return eval_mock_response(op_node, ctx); } - let result = dispatch_service_wet(service_node, op_node, transport, ¶m_env, ctx)?; + let result = dispatch_service_wet(service_node, op_node, transport, ¶m_env, ctx, &key)?; if ctx.execution_mode.is_record() { let store = ctx @@ -5312,9 +5319,10 @@ fn dispatch_service_wet( transport: &Rc, param_env: &Rc, ctx: &InterpContext, + intent: &str, ) -> InterpResult { if is_shell_transport(transport.clone()) { - let result = dispatch_shell(transport, param_env, ctx)?; + let result = dispatch_shell(transport, param_env, ctx, intent)?; return map_shell_outputs(&result, op_node, ctx); } @@ -5795,8 +5803,8 @@ fn trace_emit(channel: OutputChannel, line: &str) { pub const SHELL_CENSUS_MARKER: &str = "[shell]"; /// Collapse argv into one readable line — runs of whitespace become a single space — -/// so a multiline `sh -c` script reads as one command. Shared by Ambient Begin/Done -/// (optionally capped) and Anomaly Failed (uncapped: an anomaly expands fully). +/// so a multiline `sh -c` script reads as one command. Used in Failed.error (uncapped: +/// an anomaly expands fully). Ambient subjects are named intents, not argv. fn shell_argv_collapsed(argv: &[String]) -> String { argv.join(" ") .split_whitespace() @@ -5804,23 +5812,6 @@ fn shell_argv_collapsed(argv: &[String]) -> String { .join(" ") } -/// `$ ` identity for shell ObservationEvent subjects. `cap` truncates the -/// command body (Condensed Ambient); `None` leaves it uncapped (Full / Anomaly). -fn shell_argv_summary(argv: &[String], cap: Option) -> String { - let collapsed = shell_argv_collapsed(argv); - let body = if let Some(max) = cap { - if collapsed.chars().count() > max { - let head: String = collapsed.chars().take(max).collect(); - format!("{head}…") - } else { - collapsed - } - } else { - collapsed - }; - format!("$ {body}") -} - fn shell_obs_emoji() -> bool { std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") } @@ -5836,30 +5827,26 @@ fn shell_obs_human_duration(ms: u64) -> String { } /// Mirror of `gunbc.observation_seed_render.shell_effect_begin_line`. -pub fn render_shell_effect_begin_line_mirror(argv_summary: &str, emoji: bool) -> String { +pub fn render_shell_effect_begin_line_mirror(intent: &str, emoji: bool) -> String { let _ = SHELL_CENSUS_MARKER; let glyph = if emoji { "🔄" } else { "◐" }; - format!("{glyph} started {argv_summary}") + format!("{glyph} started {intent}") } /// Mirror of `gunbc.observation_seed_render.shell_effect_done_line`. -pub fn render_shell_effect_done_line_mirror( - argv_summary: &str, - elapsed_ms: u64, - emoji: bool, -) -> String { +pub fn render_shell_effect_done_line_mirror(intent: &str, elapsed_ms: u64, emoji: bool) -> String { let _ = SHELL_CENSUS_MARKER; let glyph = if emoji { "✅" } else { "✓" }; format!( - "{glyph} {argv_summary} done in {}", + "{glyph} {intent} done in {}", shell_obs_human_duration(elapsed_ms) ) } /// Mirror of `gunbc.observation_seed_render.shell_effect_failed_line`. -/// `argv_collapsed` is WITHOUT the `$ ` prefix (the seed concatenates it into Failed.error). +/// Subject is the named intent; `argv_collapsed` (WITHOUT `$ `) feeds Failed.error only. pub fn render_shell_effect_failed_line_mirror( - argv_summary: &str, + intent: &str, argv_collapsed: &str, exit_code: u64, elapsed_ms: u64, @@ -5868,29 +5855,17 @@ pub fn render_shell_effect_failed_line_mirror( let _ = SHELL_CENSUS_MARKER; let glyph = if emoji { "❌" } else { "✗" }; format!( - "{glyph} {argv_summary} failed: $ {argv_collapsed} (exit={exit_code}) in {}", + "{glyph} {intent} failed: $ {argv_collapsed} (exit={exit_code}) in {}", shell_obs_human_duration(elapsed_ms) ) } -// Ambient shell Begin — gated by the installed ShellTrace channel decision -// (Suppressed / Condensed / Full). Tables stay unread for Anomaly; only Ambient -// spawn/done still read channel_decision(ShellTrace). -fn render_shell_trace(argv: &[String]) { - let decision = output_decision(OutputChannel::ShellTrace); - if decision == OutputDecision::Suppressed { +// Ambient shell Begin — named intent subject, ShellTrace-gated (Suppressed at Normal). +fn render_shell_trace(intent: &str) { + if output_decision(OutputChannel::ShellTrace) == OutputDecision::Suppressed { return; } - // Fallback column bound (no Viewport at the trace site); the single authority is - // `gunbc.output_policy.shell_trace_summary_max_columns`. - const MAX: usize = 100; - let cap = if decision == OutputDecision::Condensed { - Some(MAX) - } else { - None - }; - let summary = shell_argv_summary(argv, cap); - let line = render_shell_effect_begin_line_mirror(&summary, shell_obs_emoji()); + let line = render_shell_effect_begin_line_mirror(intent, shell_obs_emoji()); trace_emit(OutputChannel::ShellTrace, &line); } @@ -5898,10 +5873,8 @@ fn render_shell_trace(argv: &[String]) { /// `effect_stream_disposition` alone (never silenced by ShellTrace Suppressed). /// Law 4: `group_end` before an Anomaly so it lands OutsideGroup. /// -/// Carry-forward from 60a4496 as event properties: Failed.error is self-describing +/// Subject is the typed service.op intent. Failed.error carries self-describing /// `$ (exit=N)`; empty stderr still surfaces via the Failed line alone. -/// Captured stderr CONTENT (when present) follows as a neutralized, tail-bounded -/// block — still gated by SurfaceContent, not by ShellTrace. fn render_shell_completion_trace( expected: ExpectedOutcome, exit_code: i32, @@ -5909,6 +5882,7 @@ fn render_shell_completion_trace( stderr: &[u8], wall: std::time::Duration, argv: &[String], + intent: &str, ) { let disposition = effect_stream_disposition(expected, exit_code); let emoji = shell_obs_emoji(); @@ -5916,17 +5890,14 @@ fn render_shell_completion_trace( let collapsed = shell_argv_collapsed(argv); if disposition == StreamDisposition::SurfaceContent { - // Anomaly — disposition is the sole gate. Close the host-effects group first - // (idempotent) so law 4 places the failure OutsideGroup. group_end(); - let summary = format!("$ {collapsed}"); let code = if exit_code < 0 { exit_code.unsigned_abs() as u64 } else { exit_code as u64 }; let line = - render_shell_effect_failed_line_mirror(&summary, &collapsed, code, elapsed_ms, emoji); + render_shell_effect_failed_line_mirror(intent, &collapsed, code, elapsed_ms, emoji); eprintln!("{line}"); if let Some(block) = shell_completion_stderr_content(stderr) { eprintln!("{block}"); @@ -5934,19 +5905,10 @@ fn render_shell_completion_trace( return; } - // Ambient Done — ShellTrace-gated (agreement / non-surfacing dispositions). - let decision = output_decision(OutputChannel::ShellTrace); - if decision == OutputDecision::Suppressed { + if output_decision(OutputChannel::ShellTrace) == OutputDecision::Suppressed { return; } - const MAX: usize = 100; - let cap = if decision == OutputDecision::Condensed { - Some(MAX) - } else { - None - }; - let summary = shell_argv_summary(argv, cap); - let line = render_shell_effect_done_line_mirror(&summary, elapsed_ms, emoji); + let line = render_shell_effect_done_line_mirror(intent, elapsed_ms, emoji); trace_emit(OutputChannel::ShellTrace, &line); } @@ -6029,6 +5991,7 @@ fn dispatch_shell( transport: &Rc, param_env: &Rc, ctx: &InterpContext, + intent: &str, ) -> InterpResult { // Migration default: every effect issues as `ExpectSuccess`, which makes the // dispatch below behaviour-identical to the untyped `exit != 0` proxy it @@ -6053,7 +6016,7 @@ fn dispatch_shell( }); } - render_shell_trace(&argv); + render_shell_trace(intent); // Arg-size wall: a single argv token over the host MAX_ARG_STRLEN would make // the spawn below die with an opaque `os error 7` (E2BIG). Refuse here with a @@ -6110,6 +6073,7 @@ fn dispatch_shell( &output.stderr, wall_start.elapsed(), &argv, + intent, ); output } else { @@ -6127,6 +6091,7 @@ fn dispatch_shell( &output.stderr, wall_start.elapsed(), &argv, + intent, ); output }; @@ -10247,24 +10212,28 @@ mod shell_completion_trace_tests { #[test] fn shell_effect_begin_mirror_formats_started_subject() { - let line = render_shell_effect_begin_line_mirror("$ echo hi", true); - assert_eq!(line, "🔄 started $ echo hi"); - let unicode = render_shell_effect_begin_line_mirror("$ true", false); - assert_eq!(unicode, "◐ started $ true"); + let line = render_shell_effect_begin_line_mirror("shell.Exec.Run", true); + assert_eq!(line, "🔄 started shell.Exec.Run"); + let unicode = render_shell_effect_begin_line_mirror("git.Core.HeadCommit", false); + assert_eq!(unicode, "◐ started git.Core.HeadCommit"); } #[test] fn shell_effect_done_mirror_formats_duration() { - let line = render_shell_effect_done_line_mirror("$ true", 5150, true); - assert_eq!(line, "✅ $ true done in 5 seconds"); + let line = render_shell_effect_done_line_mirror("shell.Exec.Run", 5150, true); + assert_eq!(line, "✅ shell.Exec.Run done in 5 seconds"); } #[test] fn shell_effect_failed_mirror_is_self_describing() { // Failed.error carries `$ (exit=N)` so the line stands alone when // stderr is empty (the common CI miss). - let line = render_shell_effect_failed_line_mirror("$ echo hi", "echo hi", 1, 2000, true); - assert_eq!(line, "❌ $ echo hi failed: $ echo hi (exit=1) in 2 seconds"); + let line = + render_shell_effect_failed_line_mirror("shell.Exec.Run", "echo hi", 1, 2000, true); + assert_eq!( + line, + "❌ shell.Exec.Run failed: $ echo hi (exit=1) in 2 seconds" + ); } #[test] @@ -10314,14 +10283,10 @@ mod shell_completion_trace_tests { 1 ))); let collapsed = shell_argv_collapsed(&av(&["git", "rev-parse", "--show-toplevel"])); - let line = render_shell_effect_failed_line_mirror( - &format!("$ {collapsed}"), - &collapsed, - 1, - 0, - false, - ); + let line = + render_shell_effect_failed_line_mirror("git.Core.Toplevel", &collapsed, 1, 0, false); assert!(line.contains("failed: $ git rev-parse --show-toplevel (exit=1)")); + assert!(line.starts_with("✗ git.Core.Toplevel failed:")); assert_eq!(shell_completion_stderr_content(b""), None); } @@ -10620,7 +10585,7 @@ mod argv_arg_limit_test { let ctx = argv_limit_test_context(); let transport = shell_check_style_transport(&"x".repeat(HOST_ARG_MAX_STRLEN_BYTES + 1)); let env = Env::empty(); - match dispatch_shell(&transport, &env, &ctx) { + match dispatch_shell(&transport, &env, &ctx, "shell.Exec.Check") { Err(InterpError::ArgvExceedsHostArgMax { actual_bytes, limit_bytes, @@ -10644,7 +10609,7 @@ mod argv_arg_limit_test { let ctx = argv_limit_test_context(); let transport = shell_check_style_transport("true"); let env = Env::empty(); - match dispatch_shell(&transport, &env, &ctx) { + match dispatch_shell(&transport, &env, &ctx, "shell.Exec.Check") { Err(InterpError::ArgvExceedsHostArgMax { .. }) => { panic!("small argv must not trip the arg-size wall") } From d52e6b512f3032c5812aff66afe5371b3d904655 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 06:03:17 +0000 Subject: [PATCH 32/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- src/v1/stage0/src/cli_run.rs | 44 +++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 4661822914f..dcadcf6bb5d 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -7452,12 +7452,24 @@ pub struct ResolveStageNanos { pub typecheck_compute: u128, /// `collect_parent_envs` calls inside reconcile (every module, cache hit or miss). pub parent_envs: u128, - /// Reconcile total minus the two rows above: variant surfaces, registry merge, - /// transitive-service expansion, the three rewire passes, emit-graph info — the - /// whole-closure assembly residue that reruns per entry even at 100% cache hits. + /// Reconcile total minus the rows above and the assembly sub-rows below: the + /// unattributed whole-closure assembly residue that reruns per entry even at + /// 100% cache hits. pub reconcile_assembly: u128, /// `extract_ownership_proofs` + its diagnostics walk. pub ownership: u128, + /// `module_schedule_batches` — antichain schedule build over the closure. + pub assembly_schedule: u128, + /// `try_reconcile_all_cache_hits` pass 1: per-module content key + store probe. + pub assembly_probe: u128, + /// `item_registry` merge fold across the closure's typed modules. + pub assembly_registry: u128, + /// `expand_transitive_services` (bounded 5-pass fixpoint over every bodied item). + pub assembly_services: u128, + /// The three `rewire_*` passes (type-env parents, import-str identity, func-env parents). + pub assembly_rewire: u128, + /// `corpus_has_v1_seed_source_indices` + `build_emit_graph_info`. + pub assembly_emit_info: u128, } impl ResolveStageNanos { @@ -7470,6 +7482,12 @@ impl ResolveStageNanos { self.parent_envs += other.parent_envs; self.reconcile_assembly += other.reconcile_assembly; self.ownership += other.ownership; + self.assembly_schedule += other.assembly_schedule; + self.assembly_probe += other.assembly_probe; + self.assembly_registry += other.assembly_registry; + self.assembly_services += other.assembly_services; + self.assembly_rewire += other.assembly_rewire; + self.assembly_emit_info += other.assembly_emit_info; } /// Sum of the attributed stages; the caller's lump minus this is the @@ -7483,6 +7501,12 @@ impl ResolveStageNanos { + self.parent_envs + self.reconcile_assembly + self.ownership + + self.assembly_schedule + + self.assembly_probe + + self.assembly_registry + + self.assembly_services + + self.assembly_rewire + + self.assembly_emit_info } } @@ -7497,6 +7521,12 @@ thread_local! { parent_envs: 0, reconcile_assembly: 0, ownership: 0, + assembly_schedule: 0, + assembly_probe: 0, + assembly_registry: 0, + assembly_services: 0, + assembly_rewire: 0, + assembly_emit_info: 0, }) }; } @@ -8019,11 +8049,15 @@ fn finish_resolved_graph_assembly( source_indices: Rc>>, ) -> Result, String> { let (same_tree_fork_count, cross_tree_fork_count) = binding_fork_counts; + let registry_started = std::time::Instant::now(); let item_registry = modules.iter().fold(v1_rt::rc_empty_map(), |acc, typed| { v1_rt::rc_map_merge(acc, typed.item_registry.clone()) }); + resolve_stage_slot_add(|s| s.assembly_registry += registry_started.elapsed().as_nanos()); + let services_started = std::time::Instant::now(); let expanded_registry = v1_compiler_infer::expand_transitive_services(modules.clone(), item_registry, 5); + resolve_stage_slot_add(|s| s.assembly_services += services_started.elapsed().as_nanos()); let diagnostics: Rc>> = Rc::new({ let mut acc = im::Vector::new(); for chunk in &diag_chunks { @@ -8037,6 +8071,7 @@ fn finish_resolved_graph_assembly( "[binding-fork-ledger] same_tree={same_tree_fork_count} cross_tree={cross_tree_fork_count} total={total_fork_count}" ); } + let rewire_started = std::time::Instant::now(); let modules = v1_compiler_infer::rewire_type_env_parent_links(modules.clone(), source_indices.clone()); let modules = v1_compiler_infer::rewire_type_env_import_str_binding_identity( @@ -8045,8 +8080,11 @@ fn finish_resolved_graph_assembly( ); let modules = v1_compiler_infer::rewire_func_env_parent_links(modules.clone(), source_indices.clone()); + resolve_stage_slot_add(|s| s.assembly_rewire += rewire_started.elapsed().as_nanos()); + let emit_info_started = std::time::Instant::now(); let has_v1_seed = v1_compiler_infer::corpus_has_v1_seed_source_indices(modules.clone()); let emit_graph_info = v1_compiler_infer::build_emit_graph_info(modules.clone(), has_v1_seed); + resolve_stage_slot_add(|s| s.assembly_emit_info += emit_info_started.elapsed().as_nanos()); Ok(Rc::new(ResolvedGraph { modules, item_registry: expanded_registry, From 4cb92a34a31c44a3f137ce286bee6171fd33983e Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 06:13:42 +0000 Subject: [PATCH 33/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- src/v1/stage0/src/bin/claim_batch.rs | 28 ++++++++++++++ src/v1/stage0/src/bin/claim_executor.rs | 10 +++++ src/v1/stage0/src/cli_run.rs | 51 ++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/bin/claim_batch.rs b/src/v1/stage0/src/bin/claim_batch.rs index c3aeea24015..441fb49477e 100644 --- a/src/v1/stage0/src/bin/claim_batch.rs +++ b/src/v1/stage0/src/bin/claim_batch.rs @@ -689,6 +689,34 @@ fn run() -> Result { "[resolve-summary] {} resolve(s) in {}ms; {} witness(es) in {}ms", timings.resolves, timings.resolve_ms, timings.witnesses, timings.witness_ms, ); + { + let st = v1_compiler::cli_run::resolve_stage_totals(); + let ms = |n: u128| n as f64 / 1.0e6; + eprintln!( + "[resolve-split] load={:.1}ms parse={:.1}ms resolve={:.1}ms normalize={:.1}ms typecheck={:.1}ms parent_envs={:.1}ms reconcile_assembly={:.1}ms ownership={:.1}ms", + ms(st.load), + ms(st.parse), + ms(st.resolve), + ms(st.normalize), + ms(st.typecheck_compute), + ms(st.parent_envs), + ms(st.reconcile_assembly), + ms(st.ownership), + ); + eprintln!( + "[assembly-split] schedule={:.1}ms probe={:.1}ms registry={:.1}ms services={:.1}ms rewire={:.1}ms (type_env={:.1}ms import_str={:.1}ms func_env={:.1}ms) emit_info={:.1}ms residue={:.1}ms", + ms(st.assembly_schedule), + ms(st.assembly_probe), + ms(st.assembly_registry), + ms(st.assembly_services), + ms(st.assembly_rewire), + ms(st.assembly_rewire_type_env), + ms(st.assembly_rewire_import_str), + ms(st.assembly_rewire_func_env), + ms(st.assembly_emit_info), + ms(st.reconcile_assembly), + ); + } emit_rss_measurement("per-shard-peak-rss"); if let Some(bytes) = children_max_rss_bytes() { diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index ac2c36dddd7..d135535d6ef 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1150,6 +1150,16 @@ fn run_discovery_batch_node( .total_resolve_nanos .saturating_sub(st.attributed_total())), ); + eprintln!( + "[assembly-split] schedule={:.1}ms probe={:.1}ms registry={:.1}ms services={:.1}ms rewire={:.1}ms emit_info={:.1}ms residue={:.1}ms", + ms(st.assembly_schedule), + ms(st.assembly_probe), + ms(st.assembly_registry), + ms(st.assembly_services), + ms(st.assembly_rewire), + ms(st.assembly_emit_info), + ms(st.reconcile_assembly), + ); match compute_histogram_data(&summary) { Ok(data) => match render_timing_histogram(&source_roots, &data) { Ok(histogram) => eprintln!("{histogram}"), diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index dcadcf6bb5d..1bd0635aef4 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -7468,6 +7468,12 @@ pub struct ResolveStageNanos { pub assembly_services: u128, /// The three `rewire_*` passes (type-env parents, import-str identity, func-env parents). pub assembly_rewire: u128, + /// `rewire_type_env_parent_links` alone. + pub assembly_rewire_type_env: u128, + /// `rewire_type_env_import_str_binding_identity` alone. + pub assembly_rewire_import_str: u128, + /// `rewire_func_env_parent_links` alone. + pub assembly_rewire_func_env: u128, /// `corpus_has_v1_seed_source_indices` + `build_emit_graph_info`. pub assembly_emit_info: u128, } @@ -7487,6 +7493,9 @@ impl ResolveStageNanos { self.assembly_registry += other.assembly_registry; self.assembly_services += other.assembly_services; self.assembly_rewire += other.assembly_rewire; + self.assembly_rewire_type_env += other.assembly_rewire_type_env; + self.assembly_rewire_import_str += other.assembly_rewire_import_str; + self.assembly_rewire_func_env += other.assembly_rewire_func_env; self.assembly_emit_info += other.assembly_emit_info; } @@ -7526,11 +7535,31 @@ thread_local! { assembly_registry: 0, assembly_services: 0, assembly_rewire: 0, + assembly_rewire_type_env: 0, + assembly_rewire_import_str: 0, + assembly_rewire_func_env: 0, assembly_emit_info: 0, }) }; } +thread_local! { + static RESOLVE_STAGE_TOTAL: std::cell::RefCell = + std::cell::RefCell::new(ResolveStageNanos::default()); +} + +/// Cumulative per-worker stage attribution across every entry resolve this thread +/// has run (the per-entry slot folded in at each reset, plus the live slot). Read by +/// `claim_batch`'s `[assembly-split]` receipt, which — unlike `claim_executor`'s +/// discovery summary — has no per-entry receipt list to sum. +pub fn resolve_stage_totals() -> ResolveStageNanos { + let mut total = RESOLVE_STAGE_TOTAL.with(|t| *t.borrow()); + total.accumulate(&resolve_stage_slot_snapshot()); + total +} + fn resolve_stage_slot_reset() { + let carried = resolve_stage_slot_snapshot(); + RESOLVE_STAGE_TOTAL.with(|t| t.borrow_mut().accumulate(&carried)); RESOLVE_STAGE_SLOT.with(|s| s.set(ResolveStageNanos::default())); } @@ -7760,7 +7789,16 @@ fn resolved_graph_from_sources_with_index( // accumulated into the slot during this call (typecheck computes + parent envs). let reconcile_total = reconcile_started.elapsed().as_nanos(); resolve_stage_slot_add(|s| { - s.reconcile_assembly += reconcile_total.saturating_sub(s.typecheck_compute + s.parent_envs); + s.reconcile_assembly += reconcile_total.saturating_sub( + s.typecheck_compute + + s.parent_envs + + s.assembly_schedule + + s.assembly_probe + + s.assembly_registry + + s.assembly_services + + s.assembly_rewire + + s.assembly_emit_info, + ); }); let has_type_errors = typed @@ -8074,12 +8112,17 @@ fn finish_resolved_graph_assembly( let rewire_started = std::time::Instant::now(); let modules = v1_compiler_infer::rewire_type_env_parent_links(modules.clone(), source_indices.clone()); + resolve_stage_slot_add(|s| s.assembly_rewire_type_env += rewire_started.elapsed().as_nanos()); + let rewire2_started = std::time::Instant::now(); let modules = v1_compiler_infer::rewire_type_env_import_str_binding_identity( modules.clone(), source_indices.clone(), ); + resolve_stage_slot_add(|s| s.assembly_rewire_import_str += rewire2_started.elapsed().as_nanos()); + let rewire3_started = std::time::Instant::now(); let modules = v1_compiler_infer::rewire_func_env_parent_links(modules.clone(), source_indices.clone()); + resolve_stage_slot_add(|s| s.assembly_rewire_func_env += rewire3_started.elapsed().as_nanos()); resolve_stage_slot_add(|s| s.assembly_rewire += rewire_started.elapsed().as_nanos()); let emit_info_started = std::time::Instant::now(); let has_v1_seed = v1_compiler_infer::corpus_has_v1_seed_source_indices(modules.clone()); @@ -8130,6 +8173,7 @@ fn try_reconcile_all_cache_hits( closure_path_to_authored_name_map(closure_modules, closure_names); let mut results: Vec>> = vec![None; closure_modules.len()]; + let probe_started = std::time::Instant::now(); let mut pending: Vec = schedule.iter().flatten().copied().collect(); let mut defer_pass = 0usize; while !pending.is_empty() { @@ -8157,6 +8201,7 @@ fn try_reconcile_all_cache_hits( Err(e) => return Err(e), }; let Some(tc_result) = index_get_typed(index, &typed_key)? else { + resolve_stage_slot_add(|s| s.assembly_probe += probe_started.elapsed().as_nanos()); return Ok(None); }; note_interface_hash(&mut interface_hash_by_name, mod_name, &tc_result); @@ -8184,6 +8229,8 @@ fn try_reconcile_all_cache_hits( } } + resolve_stage_slot_add(|s| s.assembly_probe += probe_started.elapsed().as_nanos()); + // Pass 2 — resolver's ORIGINAL order, assembling the `ResolvedGraph` byte-identically to // the legacy serial fold (module order is an output-shape invariant, not just a schedule // convenience — see `reconcile_with_typed_cache`'s S2a move 2 comment). @@ -8566,8 +8613,10 @@ fn reconcile_with_typed_cache( .iter() .map(|m| authored_name_at(source_indices.clone(), m.module.clone())) .collect(); + let schedule_started = std::time::Instant::now(); let (schedule, cycle_residue_slots) = module_schedule_batches(&closure_modules, &closure_names, index); + resolve_stage_slot_add(|s| s.assembly_schedule += schedule_started.elapsed().as_nanos()); if let Some(assembled) = try_reconcile_all_cache_hits( &closure_modules, &closure_names, From 3082752633afe303e57923d1474a3b172fe0434a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 06:28:43 +0000 Subject: [PATCH 34/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- src/v1/04_infer.dag | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 80590cc95a3..1973bf182d2 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -8015,25 +8015,35 @@ fn build_type_name_export_index(modules: List) -> Map export_index_merge_module(acc: acc, m: m)) } -fn module_exports_type_name(m: TypedModule, name: String) -> Bool { - (filter(m.type_env.bindings |> map_values, b => b.name == name) |> count) > 0 +data module_exported_type_names_cost_note: String = "Cost shape (§6 bare-minimum cost, floor batch-3 receipt 2026-07-25): the type names a module exports is ONE derived fact — a set — and both consumers in rewire_type_env_import_str_binding_identity read it. Before this row it existed twice: the caller's per-module `local_names` fold, and `module_exports_type_name`, a LINEAR SCAN (map_values allocates a Vec of every binding, then filters by name) re-run once per (consumer module x inherited key x direct import). Measured on 79 discovery entries: that pass alone was 191.1s of a 331.9s resolve wall (99% of all rewire time, 2.4s/entry) while the other two rewire passes together cost 1.8s. Hoisting the set per module and testing membership makes direct_import_exporter_count O(direct imports) instead of O(direct imports x |parent bindings|), with no change to the predicate it computes." + +fn module_exported_type_names(m: TypedModule) -> Set { + fold(m.type_env.bindings |> map_values, init: empty_set(), f: (acc, b) => set_insert(acc, b.name)) +} + +fn build_module_exported_type_name_index(modules: List, source_indices: Map) -> Map> { + fold(modules, init: empty_map(), f: (acc, m) => + map_insert(acc, authored_name_at(source_indices: source_indices, node: m.module), module_exported_type_names(m: m)) + ) } -fn direct_import_exporter_count(m: TypedModule, name: String, index: Map, source_indices: Map) -> Int { - module_imports(n: m.module) |> filter(imp => +fn direct_import_export_name_sets(m: TypedModule, export_name_index: Map>, source_indices: Map) -> List> { + flat_map(module_imports(n: m.module), imp => let path = import_module_path_at(imp: imp, source_indices: source_indices) - match map_get(index, path) { - Present { value: parent } => module_exports_type_name(m: parent, name: name) - Absent => false + match map_get(export_name_index, path) { + Present { value: names } => [names] + Absent => [] } - ) |> count + ) +} + +fn direct_import_exporter_count(import_export_names: List>, name: String) -> Int { + import_export_names |> filter(names => set_contains(names, name)) |> count } fn rewire_inherited_str_binding( type_name_index: Map, - module_index: Map, - consumer: TypedModule, - source_indices: Map, + import_export_names: List>, local_names: Set, str_bindings: Map, ancestry_str_bindings: Map, @@ -8043,7 +8053,7 @@ fn rewire_inherited_str_binding( Present { value: facts } => facts.exporter_count Absent => 0 } - if set_contains(local_names, name) || direct_import_exporter_count(m: consumer, name: name, index: module_index, source_indices: source_indices) > 1 || exporter_count > 1 { + if set_contains(local_names, name) || direct_import_exporter_count(import_export_names: import_export_names, name: name) > 1 || exporter_count > 1 { StrBindingsRewireAccum { str_bindings: str_bindings, ancestry_str_bindings: ancestry_str_bindings } } else { match map_get(type_name_index, name) { @@ -8065,20 +8075,17 @@ fn rewire_inherited_str_binding( fn rewire_type_env_import_str_binding_identity(modules: List, source_indices: Map) -> List { let type_name_index = build_type_name_export_index(modules: modules) - let index = fold(modules, init: empty_map(), f: (acc, m) => - map_insert(acc, authored_name_at(source_indices: source_indices, node: m.module), m) - ) + let export_name_index = build_module_exported_type_name_index(modules: modules, source_indices: source_indices) map(modules, m => - let local_names = fold(m.type_env.bindings |> map_values, init: empty_set(), f: (acc, b) => set_insert(acc, b.name)) + let local_names = module_exported_type_names(m: m) + let import_export_names = direct_import_export_name_sets(m: m, export_name_index: export_name_index, source_indices: source_indices) let ancestry_keys = m.type_env.ancestry_str_bindings |> map_keys let str_keys = m.type_env.str_bindings |> map_keys let inherited_keys = concat(ancestry_keys, str_keys) let rewired = fold(inherited_keys, init: StrBindingsRewireAccum { str_bindings: m.type_env.str_bindings, ancestry_str_bindings: m.type_env.ancestry_str_bindings }, f: (acc, name) => rewire_inherited_str_binding( type_name_index: type_name_index, - module_index: index, - consumer: m, - source_indices: source_indices, + import_export_names: import_export_names, local_names: local_names, str_bindings: acc.str_bindings, ancestry_str_bindings: acc.ancestry_str_bindings, From 86318c11ecc1ab32f395a363bfcf97ed36ea250c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 06:39:10 +0000 Subject: [PATCH 35/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- src/v1/stage0/src/cli_run.rs | 4 +- src/v1/stage0/src/v1_compiler_infer.rs | 122 ++++++++++++++----------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 1bd0635aef4..b2e95b95163 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -8118,7 +8118,9 @@ fn finish_resolved_graph_assembly( modules.clone(), source_indices.clone(), ); - resolve_stage_slot_add(|s| s.assembly_rewire_import_str += rewire2_started.elapsed().as_nanos()); + resolve_stage_slot_add(|s| { + s.assembly_rewire_import_str += rewire2_started.elapsed().as_nanos() + }); let rewire3_started = std::time::Instant::now(); let modules = v1_compiler_infer::rewire_func_env_parent_links(modules.clone(), source_indices.clone()); diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index 8a0ebd8fcf8..db02c10c8b1 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -17785,40 +17785,76 @@ pub fn build_type_name_export_index( ) } -pub fn module_exports_type_name(m: Rc, name: String) -> bool { - ((Rc::new({ +pub fn module_exported_type_names_cost_note() -> String { + thread_local! { + static CACHED: String = { + "Cost shape (§6 bare-minimum cost, floor batch-3 receipt 2026-07-25): the type names a module exports is ONE derived fact — a set — and both consumers in rewire_type_env_import_str_binding_identity read it. Before this row it existed twice: the caller's per-module `local_names` fold, and `module_exports_type_name`, a LINEAR SCAN (map_values allocates a Vec of every binding, then filters by name) re-run once per (consumer module x inherited key x direct import). Measured on 79 discovery entries: that pass alone was 191.1s of a 331.9s resolve wall (99% of all rewire time, 2.4s/entry) while the other two rewire passes together cost 1.8s. Hoisting the set per module and testing membership makes direct_import_exporter_count O(direct imports) instead of O(direct imports x |parent bindings|), with no change to the predicate it computes.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn module_exported_type_names(m: Rc) -> Rc> { + Rc::new(v1_rt::map_values(&m.type_env.clone().bindings.clone())) + .iter() + .cloned() + .fold( + v1_rt::rc_empty_set::(), + |acc: Rc>, b: Rc| { + v1_rt::rc_set_insert(acc, b.name.clone()) + }, + ) +} + +pub fn build_module_exported_type_name_index( + modules: Rc>>, + source_indices: Rc>>, +) -> Rc>>> { + modules.clone().iter().cloned().fold( + v1_rt::rc_empty_map::>>(), + |acc: Rc>>>, m: Rc| { + v1_rt::rc_map_insert( + acc, + authored_name_at(source_indices.clone(), m.module.clone()), + module_exported_type_names(m.clone()), + ) + }, + ) +} + +pub fn direct_import_export_name_sets( + m: Rc, + export_name_index: Rc>>>, + source_indices: Rc>>, +) -> Rc>>> { + Rc::new({ let mut __result = Vec::new(); - for b in Rc::new(v1_rt::map_values(&m.type_env.clone().bindings.clone())) - .iter() - .cloned() - { - if (b.name.clone() == name.clone()) { - __result.push(b); - } + for imp in module_imports(m.module.clone()).iter().cloned() { + __result.extend( + (*{ + let path = import_module_path_at(imp.clone(), source_indices.clone()); + match v1_rt::map_get(&export_name_index, path.clone()) { + Some(names) => Rc::new(vec![names.clone()]), + None => Rc::new(vec![]), + } + }) + .iter() + .cloned(), + ); } __result }) - .len() as i64) - > 0) } pub fn direct_import_exporter_count( - m: Rc, + import_export_names: Rc>>>, name: String, - index: Rc>>, - source_indices: Rc>>, ) -> i64 { (Rc::new({ let mut __result = Vec::new(); - for imp in module_imports(m.module.clone()).iter().cloned() { - if { - let path = import_module_path_at(imp.clone(), source_indices.clone()); - match v1_rt::map_get(&index, path.clone()) { - Some(parent) => module_exports_type_name(parent.clone(), name.clone()), - None => false, - } - } { - __result.push(imp); + for names in import_export_names.clone().iter().cloned() { + if v1_rt::set_contains(&names, name.clone()) { + __result.push(names); } } __result @@ -17828,9 +17864,7 @@ pub fn direct_import_exporter_count( pub fn rewire_inherited_str_binding( type_name_index: Rc>>, - module_index: Rc>>, - consumer: Rc, - source_indices: Rc>>, + import_export_names: Rc>>>, local_names: Rc>, str_bindings: Rc>>, ancestry_str_bindings: Rc>>, @@ -17842,12 +17876,7 @@ pub fn rewire_inherited_str_binding( None => 0, }; if ((v1_rt::set_contains(&local_names, name.clone()) - || (direct_import_exporter_count( - consumer.clone(), - name.clone(), - module_index.clone(), - source_indices.clone(), - ) > 1)) + || (direct_import_exporter_count(import_export_names.clone(), name.clone()) > 1)) || (exporter_count.clone() > 1)) { Rc::new(StrBindingsRewireAccum { @@ -17896,27 +17925,18 @@ pub fn rewire_type_env_import_str_binding_identity( ) -> Rc>> { { let type_name_index = build_type_name_export_index(modules.clone()); - let index = modules.clone().iter().cloned().fold( - v1_rt::rc_empty_map::>(), - |acc: Rc>>, m: Rc| { - v1_rt::rc_map_insert( - acc, - authored_name_at(source_indices.clone(), m.module.clone()), - m.clone(), - ) - }, - ); + let export_name_index = + build_module_exported_type_name_index(modules.clone(), source_indices.clone()); Rc::new({ let mut __result = Vec::new(); for m in modules.clone().iter().cloned() { __result.push({ - let local_names = - Rc::new(v1_rt::map_values(&m.type_env.clone().bindings.clone())) - .iter() - .cloned() - .fold(v1_rt::rc_empty_set::<_>(), |acc: _, b: Rc| { - v1_rt::rc_set_insert(acc, b.name.clone()) - }); + let local_names = module_exported_type_names(m.clone()); + let import_export_names = direct_import_export_name_sets( + m.clone(), + export_name_index.clone(), + source_indices.clone(), + ); let ancestry_keys = Rc::new(v1_rt::map_keys( &m.type_env.clone().ancestry_str_bindings.clone(), )); @@ -17931,9 +17951,7 @@ pub fn rewire_type_env_import_str_binding_identity( |acc: Rc, name: String| { rewire_inherited_str_binding( type_name_index.clone(), - index.clone(), - m.clone(), - source_indices.clone(), + import_export_names.clone(), local_names.clone(), acc.str_bindings.clone(), acc.ancestry_str_bindings.clone(), From 7f208f006e33017154f80e43e337b7c7b88c4cae Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 06:51:22 +0000 Subject: [PATCH 36/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- ...ime-namespace-walk-regression-diagnosis.md | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/plans/floor-time-namespace-walk-regression-diagnosis.md b/docs/plans/floor-time-namespace-walk-regression-diagnosis.md index a56a597ab6d..bbbb872603d 100644 --- a/docs/plans/floor-time-namespace-walk-regression-diagnosis.md +++ b/docs/plans/floor-time-namespace-walk-regression-diagnosis.md @@ -228,13 +228,100 @@ gates (`ci_floor_plan_witness_test.dag` checked); `runnable_excludes_corpus_co_r on `profile.memory`, orthogonal to this flag. Falsifiable claim pending: a real CI floor run must show batch-4 wall time drop from the ~18.7min baseline — not yet obtained at PR-open time. -## 7. Provenance +## 7. The measured root is the ASSEMBLY, not the loader (post-flip lane, 2026-07-25) + +The #7204 budget stopgap was dispatched as "#6848's once-per-entry bare-reference fixpoint". +The `[resolve-split]` receipt the executor already prints refutes that reading and names the +real row. + +### 7.1 The loader class is already dissolved + +| arm | run | witnesses | resolve wall | `load` | `reconcile_assembly` | +|---|---|---:|---:|---:|---:| +| PRE-flip (#7137) | `30081341899` | 2310 | 1,358,819ms | **0.6ms** | 1,307,224ms (96.2%) | +| POST-flip (#7188) | `30142221249` | 2336 | 1,654,155ms | **1.0ms** | 1,583,620ms (95.7%) | + +`load` — `load_sources_for_entry_with_pool`, i.e. the both-closure fixpoint §2 names — is +**1ms across a whole run**. #7056 (`BothClosureEdgeIndex`, per-process edge precompute) and +#6999 (entry-closure memo) already took it to zero. The entire post-flip delta +(+295s of resolve wall, ~+112ms/witness) lands in `reconcile_assembly`: the whole-closure +`ResolvedGraph` view rebuilt per entry *even at 100% typed-cache hits*. + +### 7.2 Sub-row attribution (this PR's instrument) + +`reconcile_assembly` was one undifferentiated number covering ~96% of the wall — the same +condition `ResolveStageNanos`'s own doc-comment was written to end one level up. This PR +splits it into named rows (`schedule`, `probe`, `registry`, `services`, `rewire` — with the +three `rewire_*` passes separated — and `emit_info`; `reconcile_assembly` keeps the residue) +and prints them as `[assembly-split]` beside `[resolve-split]` in both `claim_executor` +(discovery summary) and `claim_batch` (new `cli_run::resolve_stage_totals`, a per-worker +cumulative fold — `claim_batch` has no per-entry receipt list to sum). + +Local receipt, 79 discovery entries (`dag/test/claim`, `claim_batch --entry/--function`): + +``` +[resolve-split] load=36215.8 parse=3447.9 resolve=1165.0 normalize=430.4 + typecheck=71583.8 parent_envs=5.6 reconcile_assembly=38062.6 ownership=422.7 +[assembly-split] schedule=29.4 probe=56.2 registry=80.3 services=3305.9 + rewire=192867.4 (type_env=1395.9 import_str=191090.4 func_env=381.1) + emit_info=3632.2 residue=38062.6 +``` + +**One row is 58% of the entire resolve wall**: `rewire_type_env_import_str_binding_identity`, +191.1s — 99% of all rewire time, 2.4s/entry, against 1.8s for the other two passes combined. + +### 7.3 The cost-shape defect + +`direct_import_exporter_count(m, name, …)` asked `module_exports_type_name(parent, name)`, +which was a **linear scan**: `map_values` allocates a `Vec` of every binding in the parent's +env, then filters by name. It ran once per **(consumer module × inherited key × direct +import)**, so the pass was O(modules × keys × imports × |parent bindings|). The namespace-only +flip widened the inherited-key sets, which is why the same pass grew ~20% at #7178 — the flip +did not add a mechanism, it enlarged the input to one that was already quadratic. + +The set of type names a module exports is **one derived fact**, and it already existed twice +in the same function: as the caller's per-module `local_names` fold, and as this rescan. +Fix (`src/v1/04_infer.dag`, regen-emitted to `v1_compiler_infer.rs`): lift +`module_exported_type_names` as the single authority, build a `module → Set` index +once per pass, hoist each consumer's direct-import name sets out of the per-key loop, and make +`direct_import_exporter_count` a membership test — O(direct imports). The predicate is +unchanged by construction; `module_exports_type_name` is deleted (no remaining consumer). + +### 7.4 Receipt (by execution) + +Same 79 entries, same binary path, after the fix: + +``` +[resolve-summary] 79 resolve(s) in 160418ms (was 331923ms — −51.7%) +[assembly-split] rewire=18582.4 (type_env=1403.6 import_str=16804.3 func_env=374.5) + (import_str was 191090.4 — −91.2%) +``` + +All 79 witness verdicts byte-identical before vs after (`PASS`/`FAIL` set diffed, empty). +Corpus-scale before/after: the PR's own floor run against main's `[resolve-split]` line. + +### 7.5 Honest residue + +This does **not** close #6848's named class — it shows the class is no longer where the wall +is. Post-fix, the remaining per-entry assembly is attributed by the same sub-rows: `residue` +(the uninstrumented reconcile remainder — symbol-index build, variant surfaces, pass-2 +assembly), then `rewire`, `emit_info`, `services`. Those are the next lane's targets, and they +are now *named and counted* rather than pooled in one number. + +**Dissolution trigger:** when a post-merge main floor run shows batch-3 back under the +pre-flip 1680s basis, the #7204 stopgap row (`gunbc.ci_spec`, 2100s) re-tightens by ordinary +receipt note — that re-tighten is this change's dissolution event. + +## 8. Provenance - Log-diff receipts: bright-seal-219 (this session), by execution on CI logs. - Post-landing validation §5: bright-seal-219, runs `29763408563` / `29819122813` / `29855080611` (2026-07-21). - Memory-side bisection: eager-pike-178 / #6953 (`c10f4b091`). - Parent coordination: sunny-wolf-225 mandate (msg_fdeee8c5); lane close validation (msg_1879f052). - §6 residual fix: proud-bear-438 (dashboard `adhoc-21c65e1a-2ff`), PR #7030. +- §7 assembly attribution + import-identity rewire fix: lively-ferret-823 (dashboard + `adhoc-d4240652-b27`), PR #7205 — CI `[resolve-split]` diff (runs `30081341899` / + `30142221249`) and the local 79-entry before/after, both by execution. Related: [floor-memory-pool-parse-regression-diagnosis.md](floor-memory-pool-parse-regression-diagnosis.md) · [namespace-resolution-design.md](namespace-resolution-design.md) §PR-5b · From 2bf2297627243f1d66a847047bf235934aa8deec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 06:54:57 +0000 Subject: [PATCH 37/39] Silence Ambient shell on gunbc run; roster #7205 resolve/assembly split tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge-admission stamp is `gunbc run`, which never installed output_policy — so ShellTrace fell back to Full and every Ambient Begin/Done still printed despite named-intent subjects. Install policy + group syntax at handle_run startup (same as claim_executor). Census grows [resolve-split]/[assembly-split] frontier rows so #7205's new tags stay bidirectional. Co-authored-by: Brian Searls --- dag/gunbc/observation_emit_census.dag | 26 ++++++++++++++++--- .../observation_emit_census_witness_test.dag | 14 +++++++--- src/v1/stage0/src/cli_run.rs | 8 ++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index 26ddf0b1847..572c91d4454 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -7,7 +7,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } data observation_emit_census_note: String = "P3 of the progress-and-observation lane: the census wall. Every place the floor emits a progress line today is either a PROJECTION of the observation event stream (migrated, P1/P2) or a COUNTED FRONTIER ROW carrying a reason and a dissolve-on — the same discipline the site subsumption lane uses for unthemed colours. This module is the census authority: the classification is a closed sum, so a site cannot be half-classified, and the roster below names the structured-tag emit families that exist in the seed today. The executable hygiene witness is bidirectional: it reds when a rostered marker vanishes AND when a known live tag family is missing from the roster, so growth after the snapshot cannot silently understate." -data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and live-log review (run 30142403230): tagged migrations landed (floor-memory, gantt, governor, typecheck-attribution, measurement, shell with named-intent subjects; Ambient ShellTrace Suppressed at Normal). Post-census growth tags ([floor-drain], [gate-warm-cost], [receipt], [file]) are CountedFrontierSite rows so the roster cannot silently understate. Unmarked raw eprintln residue in claim_executor remains counted separately." +data observation_emit_census_sequencing_note: String = "Sequencing, per the operator ruling and live-log review (run 30142403230): tagged migrations landed (floor-memory, gantt, governor, typecheck-attribution, measurement, shell with named-intent subjects; Ambient ShellTrace Suppressed at Normal). Post-census growth tags ([floor-drain], [gate-warm-cost], [receipt], [file], and #7205's [resolve-split]/[assembly-split]) are CountedFrontierSite rows so the roster cannot silently understate. Unmarked raw eprintln residue in claim_executor remains counted separately." type EmitSiteDisposition = MigratedToObservation { via: NonEmptyStr } @@ -105,6 +105,24 @@ data file_trace_site: CensusedEmitSite = CensusedEmitSite { } } +data resolve_split_site: CensusedEmitSite = CensusedEmitSite { + marker: "[resolve-split]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "per-entry resolve-stage timing split (#7205 / #6848 cost instrumentation) — load/parse/resolve/normalize/typecheck/parent_envs/assembly/ownership" as NonEmptyStr, + dissolve_on: "fold into a MeasurementSegment / StatusPulse receipt projection under the floor-receipts group" as NonEmptyStr + } +} + +data assembly_split_site: CensusedEmitSite = CensusedEmitSite { + marker: "[assembly-split]" as NonEmptyStr, + source_file: "src/v1/stage0/src/bin/claim_executor.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "reconcile-assembly sub-split (#7205) — schedule/probe/registry/services/rewire/emit_info/residue timings that priced the type-export-set memo" as NonEmptyStr, + dissolve_on: "fold into the same MeasurementSegment receipt projection as resolve-split" as NonEmptyStr + } +} + data observation_emit_roster: List = [ floor_memory_site, typecheck_attribution_site, @@ -115,7 +133,9 @@ data observation_emit_roster: List = [ floor_drain_site, gate_warm_cost_site, receipt_site, - file_trace_site + file_trace_site, + resolve_split_site, + assembly_split_site ] data observation_emit_roster_completeness_disposition: Disposition = Scaffold { @@ -127,7 +147,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "Roster is bidirectional: every rostered marker must still exist in the seed, AND every known live tag family born after the initial snapshot must appear on the roster (operator finding 3, run 30142403230). Frontier count = 4 (floor-drain, gate-warm-cost, receipt, file). Six families MigratedToObservation. The unmarked eprintln sites remain the per-print completeness scaffold." +data observation_emit_roster_completeness_note: String = "Roster is bidirectional: every rostered marker must still exist in the seed, AND every known live tag family born after the initial snapshot must appear on the roster (operator finding 3, run 30142403230). Frontier count = 6 (floor-drain, gate-warm-cost, receipt, file, resolve-split, assembly-split). Six families MigratedToObservation. The unmarked eprintln sites remain the per-print completeness scaffold." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 1ec4c39ca2e..fff29c35804 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -23,6 +23,8 @@ import gunbc.observation_emit_census { gate_warm_cost_site, receipt_site, file_trace_site, + resolve_split_site, + assembly_split_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree @@ -112,11 +114,11 @@ fn w_shell_has_migrated() -> Bool { } fn w_the_census_states_post_snapshot_frontier_count() -> Bool { - observation_emit_frontier_count() == 4 && - count(observation_emit_roster) == 10 + observation_emit_frontier_count() == 6 && + count(observation_emit_roster) == 12 } -data w_post_snapshot_live_tags_are_rostered_bidirectional_note: String = "Operator finding 3 (run 30142403230): hygiene is bidirectional — growth tags born after the initial census snapshot must appear on the roster, not only the reverse (rostered markers still exist)." +data w_post_snapshot_live_tags_are_rostered_bidirectional_note: String = "Operator finding 3 (run 30142403230): hygiene is bidirectional — growth tags born after the initial census snapshot must appear on the roster, not only the reverse (rostered markers still exist). Extended for #7205 resolve/assembly split tags." fn w_post_snapshot_live_tags_are_rostered_bidirectional() -> Bool { census_marker_present(site: floor_drain_site) && @@ -126,7 +128,11 @@ fn w_post_snapshot_live_tags_are_rostered_bidirectional() -> Bool { census_marker_present(site: receipt_site) && emit_site_is_frontier(site: receipt_site) && census_marker_present(site: file_trace_site) && - emit_site_is_frontier(site: file_trace_site) + emit_site_is_frontier(site: file_trace_site) && + census_marker_present(site: resolve_split_site) && + emit_site_is_frontier(site: resolve_split_site) && + census_marker_present(site: assembly_split_site) && + emit_site_is_frontier(site: assembly_split_site) } fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 58a7800c88e..e5adf10e6b7 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -10640,6 +10640,14 @@ pub fn handle_run_with_options( std::process::exit(1); } + // Same install as claim_executor / claim_batch: without it, host-effect traces + // fall back to Full and the merge-admission stamp (`gunbc run … merge_admission_stamp`) + // dumps every Ambient Begin/Done as emoji shell noise (operator live-log + // 2026-07-25). Install before any Wet eval so ShellTrace Suppressed-at-Normal + // collapses Ambient scaffolding; Anomaly Failed still surfaces via disposition. + install_output_policy(&source_roots); + install_group_syntax(&source_roots); + if let Ok(secs) = std::env::var("GUNBC_FLATTEN_SITE_DUMP_SECS") { if let Ok(secs) = secs.parse::() { std::thread::spawn(move || loop { From 0b8bb67ab824adc72c2ab8362cc64ccf7643b4a8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 25 Jul 2026 07:01:46 +0000 Subject: [PATCH 38/39] WIP: Floor #6848: memoize the per-entry bare-reference fixpoint so post-flip --- src/v1/04_infer.dag | 7 +++---- src/v1/stage0/src/v1_compiler_infer.rs | 29 +++++++++----------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 1973bf182d2..25e75821a8c 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -7986,16 +7986,15 @@ type ExportIndexModuleAccum { seen_names: Set } +data export_index_canonical_is_the_fold_element_note: String = "§6 bare-minimum cost, same receipt as module_exported_type_names_cost_note. export_index_merge_module's canonical binding was `filter(bindings |> map_values, b => b.name == name) |> first` — a rescan of the WHOLE binding map (with a fresh Vec allocation) once per distinct name, i.e. O(|bindings|^2) per module per closure assembly. It is dead work by construction: the enclosing fold walks THAT SAME map_values sequence in order, and seen_names skips every repeat, so the first element whose name matches is always the fold's own current element. `canonical = binding` is therefore the identical value, not an approximation of it — the order both expressions read is one traversal of one map value, so the equality does not depend on map_values being stable ACROSS runs (§5941 determinism), only on the two reads of the same value agreeing, which the rewrite removes the need for entirely." + fn export_index_merge_module(acc: Map, m: TypedModule) -> Map { fold(m.type_env.bindings |> map_values, init: ExportIndexModuleAccum { index: acc, seen_names: empty_set() }, f: (state, binding) => let name = binding.name if set_contains(state.seen_names, name) { state } else { - let canonical = match filter(m.type_env.bindings |> map_values, b => b.name == name) |> first { - Present { value: b } => b - Absent => binding - } + let canonical = binding let seen_names = set_insert(state.seen_names, name) let index = match map_get(state.index, name) { Absent => diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index db02c10c8b1..ef66a07b76e 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -17705,6 +17705,15 @@ pub struct ExportIndexModuleAccum { pub seen_names: Rc>, } +pub fn export_index_canonical_is_the_fold_element_note() -> String { + thread_local! { + static CACHED: String = { + "§6 bare-minimum cost, same receipt as module_exported_type_names_cost_note. export_index_merge_module's canonical binding was `filter(bindings |> map_values, b => b.name == name) |> first` — a rescan of the WHOLE binding map (with a fresh Vec allocation) once per distinct name, i.e. O(|bindings|^2) per module per closure assembly. It is dead work by construction: the enclosing fold walks THAT SAME map_values sequence in order, and seen_names skips every repeat, so the first element whose name matches is always the fold's own current element. `canonical = binding` is therefore the identical value, not an approximation of it — the order both expressions read is one traversal of one map value, so the equality does not depend on map_values being stable ACROSS runs (§5941 determinism), only on the two reads of the same value agreeing, which the rewrite removes the need for entirely.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn export_index_merge_module( acc: Rc>>, m: Rc, @@ -17723,25 +17732,7 @@ pub fn export_index_merge_module( state.clone() } else { { - let canonical = match Rc::new({ - let mut __result = Vec::new(); - for b in - Rc::new(v1_rt::map_values(&m.type_env.clone().bindings.clone())) - .iter() - .cloned() - { - if (b.name.clone() == name.clone()) { - __result.push(b); - } - } - __result - }) - .first() - .cloned() - { - Some(b) => b.clone(), - None => binding.clone(), - }; + let canonical = binding.clone(); let seen_names = v1_rt::rc_set_insert(state.seen_names.clone(), name.clone()); let index = match v1_rt::map_get(&state.index.clone(), name.clone()) { From 4a4fcc0f2a1d6f0086d612dddbe985c87460012a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 25 Jul 2026 07:11:16 +0000 Subject: [PATCH 39/39] Re-merge #7205 tip + native shell.Env.Get (kill printenv Anomaly clutter) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull the two new #7205 commits (export_index canonical = fold element — kills the O(|bindings|^2) rescan). Route OnTarget shell.Env.Get through wet_env_var instead of printenv so optional floor_diff injections (GUNBC_CI_DIFF_*) no longer paint ❌ Anomaly Failed when unset — reading this process's env is not a host effect (§3(b) / shell-to-dag census 0b). Co-authored-by: Brian Searls --- src/v1/stage0/src/v1_interpreter.rs | 53 ++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 8f20af0a853..75974f840a0 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -5109,14 +5109,10 @@ fn realize_clock_unix_secs_transport() -> Result Option { let s = std::env::var(name).ok()?.trim().to_string(); if s.is_empty() { @@ -5337,6 +5333,16 @@ fn dispatch_service_wet( ctx: &InterpContext, intent: &str, ) -> InterpResult { + // Local Env.Get: reading THIS process's own environment is not a host effect + // (shell-to-dag residual census §0b / DESIGN §3(b)). `printenv` was the wrong + // single hardwired transport — unset vars exited 1 and, under ExpectSuccess, + // every optional floor_diff_observe injection (GUNBC_CI_DIFF_*) painted Anomaly + // Failed lines (operator live-log 2026-07-25). Native handler; shell printenv + // remains the remote-target realization. + if intent == "shell.Env.Get" { + return dispatch_env_get_native(op_node, param_env, ctx); + } + if is_shell_transport(transport.clone()) { let result = dispatch_shell(transport, param_env, ctx, intent)?; return map_shell_outputs(&result, op_node, ctx); @@ -5350,6 +5356,37 @@ fn dispatch_service_wet( dispatch_rest(service_node, op_node, transport, param_env, ctx) } +/// Native realization of `shell.Env.Get` for OnTarget locality — same Absent/Present +/// semantics as printenv (unset/empty → Null optional; value trimmed), with no +/// ObservationEvent and no child process. +fn dispatch_env_get_native( + op_node: &Rc, + param_env: &Rc, + ctx: &InterpContext, +) -> InterpResult { + let name = match param_env.lookup(ctx.sym("name")) { + Some(Value::Str(s)) => s.clone(), + Some(other) => { + return Err(InterpError::TypeError { + msg: format!("shell.Env.Get name must be String, got {other}"), + }); + } + None => { + return Err(InterpError::TypeError { + msg: "shell.Env.Get missing name parameter".to_string(), + }); + } + }; + let value = match wet_env_var(&name) { + Some(s) => Value::Str(s), + None => Value::Null, + }; + Ok(Value::Record { + type_name: ctx.sym(&authored_name_at(ctx.si(), op_node.clone())), + fields: Rc::new(vec![(ctx.sym("value"), value)]), + }) +} + fn build_service_param_env( op_node: &Rc, args: &[(Option, Value)],