diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 29625373569..c3cdf9a5997 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -39155,26 +39155,9 @@ fn str_list(items: impl IntoIterator) -> v1_interpreter::Value { )) } -fn record_value( - ctx: &v1_interpreter::InterpContext, - type_name: &str, - fields: Vec<(&str, v1_interpreter::Value)>, -) -> v1_interpreter::Value { - v1_interpreter::Value::Record { - type_name: ctx.sym(type_name), - fields: Rc::new( - fields - .into_iter() - .map(|(n, v)| (ctx.sym(n), v)) - .collect::>(), - ), - } -} - -/// The module whose `required_floor_attempt` folds the manifest. Named once: the manifest is -/// evaluated in its OWN exact scope, exactly as every claim is, so the program that decides -/// which claims exist is not itself privileged with a wider namespace than the claims it -/// admits. +/// The module carrying the floor's authored thresholds and the long-home roster. Named once, +/// and evaluated in its OWN exact scope exactly as every claim is, so the module that supplies +/// the admission facts is not privileged with a wider namespace than the claims they admit. const REQUIRED_FLOOR_MANIFEST_MODULE: &str = "v2.workflow.required_floor"; /// THE REQUIRED FLOOR, AS ONE ATTEMPT. @@ -39396,6 +39379,21 @@ fn floor_value_shape(v: Option<&v1_interpreter::Value>) -> String { // subject, and the recursive spelling already in this file for dependency edges would recurse // once per element -- fine for a handful of edges, a stack overflow here. Depth belongs on the // heap when the depth is the population size. +/// One nullary `.dag` Int authority, decoded. The floor's per-claim thresholds are authored in +/// `v2.workflow.required_floor` and read here rather than re-spelled in Rust: the host owns when +/// a budget is applied, never what it is. +fn floor_required_int(ctx: &v1_interpreter::InterpContext, func: &str) -> Result { + let qualified = format!("v2.workflow.required_floor.{func}"); + match v1_interpreter::run_in_context(ctx, &qualified, false) { + Ok(v1_interpreter::Value::Int(n)) if n >= 0 => Ok(n as u64), + Ok(other) => Err(format!( + "{qualified}: expected a non-negative Int, got {}", + floor_value_shape(Some(&other)) + )), + Err(e) => Err(format!("{qualified}: {e}")), + } +} + fn floor_decode_list<'a>( ctx: &v1_interpreter::InterpContext, v: Option<&'a v1_interpreter::Value>, @@ -39842,137 +39840,41 @@ pub fn run_required_floor( floor_seam("site-projection"); let projection_started = std::time::Instant::now(); let files = &prepared.witness_files; - // THE SITE CARRIES ITS MODULE. `InventoryWitnessFile` already holds `module_path`, read from - // the same inventory entry in the same iteration, so the site is complete at construction. - // - // This replaced a second projection of the WHOLE inventory into an entry-to-module list -- - // 3,680 records marshalled into the interpreter -- which the .dag manifest then JOINED back - // onto the sites by `entry`. Both lists came from one collection keyed by one file path, so - // the join re-derived inside the fold a fact this loop held two lines earlier, and its answer - // was always "exactly one". The refusal for the other cases had no reachable producer. - // EVERY DISCOVERED SITE IS OFFERED TO THE MANIFEST, and the manifest decides which are - // required-floor claims. + // ONE DECISION, MADE ONCE, WHERE THE FACTS ALREADY ARE. // - // This host previously partitioned the population itself, by testing whether a file's PATH - // contained the long home. That made a directory the admission authority — the 2026-08-04 - // ruling's exact root cause — and it decided at file grain, so one expensive witness took - // every cheap sibling in its file out of the floor with it. It was also a SECOND authority: - // `WitnessDisposition` already modelled the answer in `.dag` and nothing consulted it. + // WHAT THIS REPLACED, and why it was not an optimisation. The required outcome of this whole + // region is exactly: exclude a witness whose AUTHORED module sits in the long home, exclude + // one that reads the live tree, claim everything else Hermetic. That decision used to be + // made TWICE — once by `required_floor_manifest` over 10,498 records marshalled into the + // interpreter, and again here in Rust, applying the same prefix test to explain the + // difference between sites offered and claims returned. Between the two sat an interpreted + // fold whose only product was a population this host could compute directly from facts it + // already held in `prepared.witness_files`. // - // The disposition now lives where it is authored — `v2.workflow.required_floor` - // `disposition_for_module`, a grant over a namespace subtree joined by the prefix relation, - // with `RequiredFloor` as the fail-closed default. Discovery's job is to report what exists; - // deciding what that means is the manifest's. - // - // The exclusion is still counted, and counted from the claim list rather than from a - // predicate this host applies: sites offered minus claims returned is what the manifest - // declined, so the number cannot drift from the decision that produced it. - let mut sites: Vec = Vec::new(); - // THE SAME POPULATION, KEPT IN HOST SHAPE, so the decline can be reported as a PARTITION - // rather than as a subtraction. `sites_offered - claims.len()` says how many rows vanished - // and nothing about why; two independent hacks decline rows here, and a single difference - // cannot distinguish them, cannot notice a row declined by neither, and cannot notice a row - // counted by both. - let mut site_facts: Vec<(String, String, bool)> = Vec::new(); - for file in files { - for function in &file.functions { - site_facts.push(( - format!("{}.{}", file.module_path, function), - file.module_path.clone(), - file.reads_live_tree, - )); - sites.push(record_value( - &hermetic, - "WitnessSite", - vec![ - ("entry", v1_interpreter::str_value(file.path.clone())), - ("function", v1_interpreter::str_value(function.clone())), - ( - "module_path", - v1_interpreter::str_value(file.module_path.clone()), - ), - ( - "reads_live_tree", - v1_interpreter::Value::Bool(file.reads_live_tree), - ), - ], - )); - } - } - let sites_offered = sites.len(); - eprintln!( - "[floor-phase] phase=site-projection state=completed wall_ms={} sites={} files={}", - projection_started.elapsed().as_millis(), - sites_offered, - files.len() - ); - let subject = record_value( - &hermetic, - "ObservedSubjectIdentity", - vec![ - ("commit", v1_interpreter::str_value(commit.to_string())), - ( - "source_digest", - v1_interpreter::str_value(prepared.subject_digest.clone()), - ), - ( - "modules_resolved", - v1_interpreter::Value::Int(prepared.modules_resolved as i64), - ), - ( - "modules_excluded", - v1_interpreter::Value::Int(prepared.modules_excluded as i64), - ), - ], - ); - let empty_sites = - v1_interpreter::Value::List(Rc::new(Vec::::new().into())); - floor_seam("manifest-evaluation"); - let manifest_eval_started = std::time::Instant::now(); - let admission = v1_interpreter::run_in_context_with_args( - &hermetic, - "required_floor_attempt", - &[ - (Some("subject".to_string()), subject), - ( - Some("hermetic_sites".to_string()), - v1_interpreter::Value::List(Rc::new(sites.into())), - ), - (Some("wet_sites".to_string()), empty_sites), - ], - false, - ) - .map_err(|e| format!("required_floor_attempt: {e}"))?; - eprintln!( - "[floor-phase] phase=manifest-evaluation state=completed wall_ms={}", - manifest_eval_started.elapsed().as_millis() - ); - floor_seam("admission-decode"); - let admission_decode_started = std::time::Instant::now(); - let mut claims = required_floor_claims_from_admission(&hermetic, &admission)?; - eprintln!( - "[floor-phase] phase=admission-decode state=completed wall_ms={} claims={}", - admission_decode_started.elapsed().as_millis(), - claims.len() - ); - // WHAT THE MANIFEST DECLINED, derived from the two populations rather than recomputed. A - // site offered and not returned as a claim carries a non-required disposition, and those - // rows are NOT covered by this floor — reported every run in those words, because the - // gunbc#7762 failure was not that rows were deferred but that the deferral was silent. - // THE DECLINE IS A PARTITION, AND IT IS PROVED HERE RATHER THAN ASSERTED. + // THE AUTHORED FACTS STAY AUTHORED. `long_home_prefixes`, the claim budget and the warn + // threshold are still read from `v2.workflow.required_floor`, so the prefix list and both + // thresholds remain .dag authorities and are not re-spelled in Rust. What is deleted is the + // reconstruction, never the fact. // - // Two hacks decline rows: the long-home prefix test and the live-tree declaration. A single - // `offered - claims` difference cannot tell them apart, cannot see a row declined by - // NEITHER (which would be a silent disappearance — the exact gunbc#7762 failure), and - // cannot see a row that both would claim. So the three populations are computed by identity - // and required to reconstruct the whole: + // THE TEST IS ON THE MODULE'S AUTHORED NAME, never its path — the 2026-08-04 ruling's actual + // requirement, and the reason the inventory carries `module_path` beside each file. A + // directory deciding admission was that ruling's root cause; reading the declaration is what + // fixes it, and that is preserved here exactly. // - // offered = executed ⊎ long-home-declined ⊎ live-tree-declined + // THE PARTITION IS NOW EXACT BY CONSTRUCTION, not by reconciliation. Every site takes exactly + // one arm below, so `claims + declined == offered` holds because the loop cannot do + // otherwise. The former `SitePartitionInexact` and unexplained-decline refusals existed to + // catch the two computations disagreeing; with one computation they have no reachable + // producer, so they are deleted rather than left standing as walls nothing can trip. // - // The prefixes are DECODED from the `.dag` authority rather than restated here, because a - // host-side copy of that list is a second authority that would drift the moment either side - // moved — and the drift would show up as this partition disagreeing, which is a confusing - // way to learn about a copy-paste. + // DUPLICATE ENROLLMENT IS UNCHANGED AND UNMOVED, and this is the one invariant the deleted + // fold genuinely carried. It is caught downstream by `receipt_identities`, a HashSet keyed on + // the same qualified name: two claims sharing a name give executed=2, receipted=1, and + // `ClaimIdentityCountsDisagree` refuses. The manifest's enrollment map was a second mechanism + // for that one invariant — and, being a growing `Value::Map` re-hashed once per fold step, + // was the whole of the phase's quadratic cost. + let claim_budget_ms = floor_required_int(&hermetic, "required_floor_claim_budget_ms")?; + let claim_warn_ms = floor_required_int(&hermetic, "required_floor_claim_warn_ms")?; let long_home_prefixes: Vec = { let value = v1_interpreter::run_in_context( &hermetic, @@ -39981,83 +39883,67 @@ pub fn run_required_floor( ) .map_err(|e| format!("long_home_prefixes: {e}"))?; let items = floor_decode_list(&hermetic, Some(&value)) - .map_err(|e| format!("long_home_prefixes: {e}"))?; + .map_err(|why| format!("long_home_prefixes decode: {why}"))?; let mut out = Vec::new(); for item in items { match item { v1_interpreter::Value::Str(s) => out.push(s.to_string()), other => { return Err(format!( - "long_home_prefixes: expected a module-path prefix, got {}", + "long_home_prefixes: expected String rows, got {}", floor_value_shape(Some(other)) - )); + )) } } } + if out.is_empty() { + return Err( + "REQUIRED-FLOOR REFUSAL cause=LongHomePrefixesEmpty — the long-home roster \ + decoded to zero prefixes, which would silently admit the long home into the \ + required floor" + .to_string(), + ); + } out }; - { - let executed: HashSet<&str> = claims.iter().map(|c| c.qualified.as_str()).collect(); - let mut long_declined = 0usize; - let mut live_declined = 0usize; - let mut both = 0usize; - let mut unexplained: Vec<&str> = Vec::new(); - for (qualified, module_path, reads_live_tree) in &site_facts { - if executed.contains(qualified.as_str()) { + let mut claims: Vec = Vec::new(); + let mut long_declined = 0usize; + let mut live_declined = 0usize; + let mut sites_offered = 0usize; + for file in files { + let long_home = long_home_prefixes + .iter() + .any(|prefix| file.module_path.starts_with(prefix.as_str())); + for function in &file.functions { + sites_offered += 1; + if long_home { + long_declined += 1; continue; } - let long_home = long_home_prefixes - .iter() - .any(|p| module_path.starts_with(p)); - // ASSIGNED IN A FIXED ORDER SO THE ARMS STAY DISJOINT, with the overlap counted - // separately rather than hidden by whichever test ran first. A row that is both - // long-home and live-tree is a real thing and it is worth knowing how many there - // are, but it must be attributed once. - match (long_home, *reads_live_tree) { - (true, true) => { - long_declined += 1; - both += 1; - } - (true, false) => long_declined += 1, - (false, true) => live_declined += 1, - // DECLINED BY NEITHER HACK. This is the arm that must stay empty: the manifest - // dropped a row for a reason this host cannot name, which is precisely a silent - // narrowing of the roster. - (false, false) => unexplained.push(qualified.as_str()), + if file.reads_live_tree { + live_declined += 1; + continue; } - } - let declined = long_declined + live_declined + unexplained.len(); - if declined > 0 { - eprintln!( - "[floor-disposition] {declined} of {sites_offered} discovered site(s) are NOT \ - required-floor and NOT RUN HERE: {long_declined} long-home-declined \ - ({both} of them also declare ReadsLiveTree), {live_declined} live-tree-declined \ - — each needs an executing consumer on another cadence, and none exists yet on \ - this branch" - ); - } - if !unexplained.is_empty() { - unexplained.sort_unstable(); - let shown: Vec<&str> = unexplained.iter().copied().take(20).collect(); - return Err(format!( - "REQUIRED-FLOOR REFUSAL cause=SiteDeclinedWithoutDisposition count={} — a \ - discovered site was neither executed nor declined by a named disposition, so \ - the roster narrowed for a reason nothing can report; first {}: {}", - unexplained.len(), - shown.len(), - shown.join(", ") - )); - } - if claims.len() + declined != sites_offered { - return Err(format!( - "REQUIRED-FLOOR REFUSAL cause=SitePartitionInexact offered={} executed={} \ - declined={} — the three populations must reconstruct the offered set exactly", - sites_offered, - claims.len(), - declined - )); + claims.push(RequiredFloorClaim { + qualified: format!("{}.{}", file.module_path, function), + module_path: file.module_path.clone(), + function: function.clone(), + execution_mode: v1_interpreter::ExecutionMode::Hermetic, + budget_ms: claim_budget_ms, + warn_ms: claim_warn_ms, + }); } } + eprintln!( + "[floor-phase] phase=site-projection state=completed wall_ms={} sites={} files={} \ + claims={} declined_long={} declined_live={}", + projection_started.elapsed().as_millis(), + sites_offered, + files.len(), + claims.len(), + long_declined, + live_declined + ); // THE EXPECTED-RED ROSTER, read from its .dag authority in the manifest's own frame — it // must be decoded BEFORE that frame is dropped below, and it is a separate evaluation from @@ -40220,7 +40106,6 @@ pub fn run_required_floor( // the next run says, and if the step survives then the cost is elsewhere and this was still // correct — an unread value held across the longest phase of the program has no defence. drop(hermetic); - drop(admission); drop(manifest_scope); // ── 4. fold the manifest ────────────────────────────────────────────────────────────── @@ -40901,233 +40786,6 @@ fn witness_eval_verdict_from_claim_outcome( } } -/// Read the `.dag` admission. A refusal is reported with every offending row rather than the -/// first, because a manifest with three conflicting declarations should take one round-trip to - -fn required_floor_claims_from_admission( - ctx: &v1_interpreter::InterpContext, - admission: &v1_interpreter::Value, -) -> Result, String> { - // EVERY STEP MARKED, BECAUSE THE ONE UNMARKED REGION IS WHERE THE TIME WAS. - // - // Run 31942605651 completed manifest-evaluation at 11:14:12 and was cancelled at 13:48:13 - // having printed neither arm's entry marker -- so 2h34m elapsed inside this function before - // reaching either. That region was left unmarked on my reasoning that it is a sym_eq and two - // binary searches and "cannot cost hours on its face". That was an assumption about cost - // stated as a bound, and it was wrong. Each step now emits, so no step can absorb time - // silently. - let entered = std::time::Instant::now(); - eprintln!("[floor-phase] phase=admission-decode state=started"); - let v1_interpreter::Value::Variant { - variant_name, - fields, - .. - } = admission - else { - return Err( - "required_floor_attempt did not answer a RequiredFloorAdmission variant".to_string(), - ); - }; - eprintln!( - "[floor-phase-progress] phase=admission-decode step=destructured wall_ms={} {}", - entered.elapsed().as_millis(), - floor_resource_sample() - ); - let refused = ctx.sym_eq(*variant_name, "RequiredFloorRefused"); - eprintln!( - "[floor-phase-progress] phase=admission-decode step=arm-resolved refused={} wall_ms={} {}", - refused, - entered.elapsed().as_millis(), - floor_resource_sample() - ); - if refused { - // T5a — THE REFUSAL RENDER IS ITS OWN TERM because it is the arm that can cost more - // than the decode it reports on. `{r:?}` walks an interpreter `Value` structurally, and - // a refusal carries records that share substructure: Debug re-renders each occurrence - // rather than the shared node, so a cheap-looking format over a few thousand rows is not - // bounded by the row count. A run that refuses would otherwise spend that time inside a - // function whose seam says "decode", attributing a diagnostic's cost to the decode loop. - // THE SEAM MOVES WITH EXECUTION, or the heartbeat lies about where the run is. - // `floor_seam` was last set to "admission-decode" by the caller, and the heartbeat prints - // whatever it holds — so a run stalled inside THIS arm reported `phase=admission-decode` - // for its entire life, which is indistinguishable in the log from a run stalled BEFORE the - // arm was entered. A reader then cannot tell "never reached the render" from "spent three - // hours inside it", and the natural conclusion is that no progress instrument exists. - floor_seam("refusal-render"); - let refusal_render_started = std::time::Instant::now(); - let refusal_count = match ctx.field(fields, "refusals") { - Some(v1_interpreter::Value::List(items)) => items.len(), - _ => 0, - }; - eprintln!("[floor-phase] phase=refusal-render state=started total={refusal_count}"); - // The SAME progress obligation as the decode loop, and it was missing here first. A - // censored run keeps only what was already printed, so an arm with an entry marker and a - // completion timer and nothing between teaches nothing when it is the arm that stalls -- - // and a rate plus a denominator is a completion estimate obtainable WITHOUT completing. - // Every 16 rather than every 256 because a refusal render is few rows of possibly enormous - // output, so the interesting variance is per-row, not across thousands. - let rendered = match ctx.field(fields, "refusals") { - Some(v1_interpreter::Value::List(items)) => items - .iter() - .enumerate() - .map(|(i, r)| { - if i % 16 == 0 { - eprintln!( - "[floor-phase-progress] phase=refusal-render rendered={i} total={} wall_ms={} {}", - items.len(), - refusal_render_started.elapsed().as_millis(), - floor_resource_sample() - ); - } - format!("{r:?}") - }) - .collect::>() - .join("\n "), - _ => "".to_string(), - }; - eprintln!( - "[floor-phase] phase=refusal-render state=completed wall_ms={} total={} rendered_bytes={}", - refusal_render_started.elapsed().as_millis(), - refusal_count, - rendered.len() - ); - return Err(format!( - "REQUIRED-FLOOR REFUSAL cause=ManifestInadmissible — the claim manifest carries \ - refusals, so the floor does not run its clean subset and report on the rest:\n \ - {rendered}" - )); - } - if !ctx.sym_eq(*variant_name, "RequiredFloorRunnable") { - return Err("required_floor_attempt answered an unknown admission arm".to_string()); - } - let Some(v1_interpreter::Value::Record { fields: af, .. }) = ({ - let r = ctx.field(fields, "attempt"); - eprintln!( - "[floor-phase-progress] phase=admission-decode step=attempt-field wall_ms={} {}", - entered.elapsed().as_millis(), - floor_resource_sample() - ); - r - }) else { - return Err("RequiredFloorRunnable carries no attempt record".to_string()); - }; - // THE REFUSAL NAMES THE SHAPE IT OBSERVED, not merely that the shape was wrong. - // - // "the attempt carries no claim list" is true of an absent field, of a record, of a variant - // and of a scalar alike, so it identifies the seam and nothing else. Reaching this arm costs - // a full preparation plus manifest evaluation — measured at 28 minutes on the first run that - // ever got here — and spending that to learn only that the field was not a list makes the - // next attempt cost the same again. What discriminates the causes is the constructor. - let claims_field = ctx.field(af, "claims"); - let claims_shape = floor_value_shape(claims_field); - eprintln!( - "[floor-phase-progress] phase=admission-decode step=claims-field wall_ms={} observed={} {}", - entered.elapsed().as_millis(), - claims_shape, - floor_resource_sample() - ); - let rows = floor_decode_list(ctx, claims_field).map_err(|why| { - format!("the attempt's `claims` field is not a list — {why} (field shape {claims_shape})") - })?; - // T5b — the ordinary decode loop, reported separately from the refusal arm above so that - // "the decode is slow" and "the refusal diagnostic is slow" are never one number. - // Same obligation as the refusal arm: carry the seam into the loop so the heartbeat names - // the loop rather than the phase that called it. Without this the decode's own ticks and the - // heartbeat disagree about the phase name, and a reader filtering the log on - // "admission-decode" — the name the heartbeat kept printing — silently excludes every - // `phase=claim-decode` progress line and concludes the loop has no counter at all. - floor_seam("claim-decode"); - let decode_loop_started = std::time::Instant::now(); - eprintln!( - "[floor-phase] phase=claim-decode state=started total={}", - rows.len() - ); - let mut out = Vec::with_capacity(rows.len()); - // PROGRESS, NOT JUST COMPLETION. A timer printed after a loop reports the loop's cost only if - // the loop terminates; a run killed inside it leaves "entered" and "finished" equally silent, - // so decoded-0 and decoded-9000 present identically. The tick is what separates them. - for (decoded, row) in rows.iter().enumerate() { - if decoded % 256 == 0 { - eprintln!( - "[floor-phase-progress] phase=claim-decode decoded={decoded} total={} wall_ms={} {}", - rows.len(), - decode_loop_started.elapsed().as_millis(), - floor_resource_sample() - ); - } - let v1_interpreter::Value::Record { fields: cf, .. } = row else { - return Err("a manifest claim is not a record".to_string()); - }; - let Some(v1_interpreter::Value::Record { fields: df, .. }) = ctx.field(cf, "declaration") - else { - return Err("a manifest claim carries no declaration identity".to_string()); - }; - let module_path = match ctx.field(df, "module_path") { - Some(v1_interpreter::Value::Str(s)) => s.to_string(), - _ => return Err("a claim identity carries no module path".to_string()), - }; - let function = match ctx.field(df, "function") { - Some(v1_interpreter::Value::Str(s)) => s.to_string(), - _ => return Err("a claim identity carries no function name".to_string()), - }; - let budget_ms = match ctx.field(cf, "budget_ms") { - Some(v1_interpreter::Value::Int(n)) if *n > 0 => *n as u64, - // A non-positive budget is refused rather than defaulted: a zero deadline that - // reads as "no limit" is the absorbing fallback, and one that reads as "refuse - // instantly" fails every claim for a reason unrelated to the claim. - other => { - return Err(format!( - "claim {module_path}.{function} carries a non-positive budget ({other:?})" - )) - } - }; - let warn_ms = match ctx.field(cf, "warn_ms") { - Some(v1_interpreter::Value::Int(n)) if *n > 0 => *n as u64, - other => { - return Err(format!( - "claim {module_path}.{function} carries a non-positive warn threshold ({other:?})" - )) - } - }; - let execution_mode = match ctx.field(cf, "execution_mode") { - Some(v1_interpreter::Value::Variant { - variant_name: m, .. - }) => { - if ctx.sym_eq(*m, "Hermetic") { - v1_interpreter::ExecutionMode::Hermetic - } else if ctx.sym_eq(*m, "Wet") { - v1_interpreter::ExecutionMode::Wet - } else if ctx.sym_eq(*m, "Record") { - v1_interpreter::ExecutionMode::Record - } else { - return Err(format!( - "claim {module_path}.{function} names an unknown execution mode" - )); - } - } - _ => { - return Err(format!( - "claim {module_path}.{function} carries no execution mode" - )) - } - }; - out.push(RequiredFloorClaim { - qualified: format!("{module_path}.{function}"), - module_path, - function, - execution_mode, - budget_ms, - warn_ms, - }); - } - eprintln!( - "[floor-phase] phase=claim-decode state=completed wall_ms={} claims={}", - decode_loop_started.elapsed().as_millis(), - out.len() - ); - Ok(out) -} - pub fn run_required_regen( candidate_dir_rel: &str, receipt_rel: &str, diff --git a/src/v2/workflow/required_floor.dag b/src/v2/workflow/required_floor.dag index 4070170358d..f89d7eeb337 100644 --- a/src/v2/workflow/required_floor.dag +++ b/src/v2/workflow/required_floor.dag @@ -1,8 +1,10 @@ module v2.workflow.required_floor -import v2.std.algebra { Cons, Empty, TailAbsent, TailFound, fold_list, length, list_append, list_map, list_reverse, list_snoc_item } -import v2.std.collection { List, Map, empty_map, map_insert, map_lookup } +import v2.std.algebra { Cons, Empty } +import v2.std.collection { List } import std.dissolution { DissolutionCondition, unbound_dissolution } +import v2.std.text { String } + // THE ROSTER IS IMPORTED HERE SO IT IS REACHABLE, NOT SO IT IS RE-EXPORTED. // // The host evaluates the expected-red roster in the SAME frame it evaluates this manifest in, @@ -15,14 +17,6 @@ import std.dissolution { DissolutionCondition, unbound_dissolution } // thin accessor here would be a second name for one authority, and this module would then own // a fact it does not produce. import v2.workflow.floor_expected_red { floor_expected_red_roster } -import v2.std.logic { Bool } -import v2.std.text { CharAbsent, CharFound, String, string_head, string_is_empty, string_tail } -import std.execution_mode { ExecutionMode, Hermetic, Record, Wet, execution_mode_eq } -import v2.workflow.floor2_prepared_subject { - WitnessIdentity, - WitnessSite, - witness_identity_qualified_name -} // BARE-NAME AMBIGUITY IS COMPUTED AND DISCARDED, one rung below where the machinery already // stands. This is an ACKNOWLEDGEMENT of debt the floor cut discovered on its base, not debt the @@ -57,90 +51,6 @@ import v2.workflow.floor2_prepared_subject { // component, because the qualified key is collision-free by construction. data floor_bare_name_ambiguity_next_rung_trigger: DissolutionCondition = unbound_dissolution(description: "dissolve-on: bare-name binding stops being decided by insertion order. Reached when ambiguous references carry their qualified module path (v2.compiler.emit.emit rather than emit), which routes through the collision-free qualified key in the interpreter's index and needs no change to the frozen component. Owned by the namespace cut, gunbc#8282; the nine production emit call sites were handed over and taken 2026-08-17. Until then [floor-bare-name-ambiguity] counts the population every run and the three emit renames on integration/floor-cut are interim debt reverting on that landing. Rung today: mitigatable — the ambiguous set is computed and countable, and the wrong binding is loud at the caller as a contract mismatch rather than silent. Ceiling: structural impossibility — a qualified reference has exactly one declarer, so no ambiguous binding is writable.") -// THE REQUIRED FLOOR IS ONE ATTEMPT: read the sources once, prepare one subject, fold one -// exact claim manifest, reduce one receipt. This module is that program. -// -// It deliberately does NOT reach the generic schedule vocabulary. There is no Runnable -// here, no WalkPlan, no batch, no batch position, no positional clamp, no worker and no -// coordinator — not because those happen to be unused on this path but because the required -// floor is not a walk of runnables. A batch is a grouping of separately-prepared work, and -// once there is one preparation the grouping has no subject. Preserving the word would -// preserve the architecture: a batch identity implies batch receipts, batch clamps and batch -// ordering, each of which then needs a consumer, and the outer floor reassembles itself -// around the new evaluator. That is exactly what happened when the inner entry-major loop -// was replaced while the outer grammar was left standing. -// -// WHAT THE HOST STILL OWNS IS EXACTLY TWO OPERATIONS, and neither is expressible here -// because a .dag function cannot construct the interpreter that runs it: build an evaluable -// context from a declared source set, and evaluate one qualified declaration inside it. -// Which claims exist, what identity each has, what refuses, which evaluation frame each -// needs and what the run's verdict is are facts of this module. - -// THE SUBJECT AS THE RUN OBSERVED IT, which is deliberately NOT `v2.workflow.ci_attempt` -// `CiAttemptRequest`. -// -// That carrier is the right one and this is not a nickname for it: it declares a subject in -// interned identities — `TreeIdentity { commit: Symbol }`, `SourceManifestIdentity { digest: -// Hash }` — which is the correct shape for an authored row and the wrong shape for a host -// observation, because a digest computed while preparing has no declaration to intern. The -// two meet at admission, where the exact-subject lane judges an observation against a request. -// This carries what one preparation measured, and nothing it did not. -// -// The counts are here rather than derived from the inventory because they answer a question -// the claim list cannot: how many modules the subject dropped. A run reporting claims without -// reporting exclusions cannot distinguish a clean small roster from a subject that silently -// lost half the tree. -type ObservedSubjectIdentity { - commit: String - source_digest: String - modules_resolved: Int - modules_excluded: Int -} - -type RequiredFloorAttempt { - subject: ObservedSubjectIdentity - claims: List -} - -// EXECUTION MODE IS DECLARED ON THE CLAIM, and it is deliberately NOT derived from -// `reads_live_tree`. Those answer different questions — reads_live_tree says what substrate -// a declaration may observe, execution_mode says what evaluator frame must run it — and -// their populations coinciding today does not make one the definition of the other. Deriving -// it is the cheapest-looking shortcut available at this seam and it silently re-couples two -// independent axes the first time they disagree. -// -// There is no enrollment-origin field. It was drafted and removed: it named which of the -// three former batches a claim came from, which is the batch concept surviving as a column. -// A row that remembers its batch invites a consumer to group by it, and the grouping is the -// thing being deleted. -// -// The budget is milliseconds because that is the currency the host's evaluate primitive -// reads (`EvaluatePreparedClaimRequest.budget_ms`); carrying seconds here and converting at -// the seam would put a unit change between the authority and the mechanism that enforces it. -type RequiredFloorClaim { - declaration: WitnessIdentity - execution_mode: ExecutionMode - budget_ms: Int - warn_ms: Int -} - -// THE DISPOSITION OF EVERY DECLARED WITNESS, total by construction. -// -// There is no `Deferred` arm, and its absence is the point. A deferral is not a disposition: -// it says a row was not run without saying who runs it, so the population accumulates -// silently and reads as coverage. The measured specimen this closes: 1,083 rows reported as -// "deferred-discovery" on a run whose receipt otherwise claimed to have executed the floor. -// -// `ReadsLiveTree` is NOT one of these arms either, and could not be — it determines an -// evaluation frame, not whether anything executes the row. -type WitnessDisposition - = RequiredFloor - | ScheduledWet - | ScheduledLong - | KnownRedQuarantine - | DeletedNotAWitness - -// LONG-HOME EXCLUSION, and it is a hack rather than a model (operator, 2026-08-17). // // THREE PREFIXES, NOT ONE, and the count is what found that. A single `test.claim.long.` test // excluded 42 rows of an expected ~310, because the long home is not one namespace: 64 modules @@ -163,9 +73,13 @@ type WitnessDisposition // authored all along and the host was reading a shadow of it. // // That is the whole change. No grant table, no disposition lattice, no second mechanism: one -// prefix test, in the manifest, where admission is decided. `WitnessDisposition` stays modelled -// and unconsumed exactly as it was — wiring it is a real modelling question about wet lanes and -// cadences, and this is not that question. +// prefix test over the roster below, applied once by the host that already holds every witness's +// authored module name. +// +// `WitnessDisposition` — a five-arm coproduct with no consumer, whose one predicate +// `disposition_is_required_floor` was never called — was deleted with the manifest fold rather +// than left standing as a model of a decision nothing made. Wiring wet lanes and cadences is a +// real modelling question, and when it is asked it will be asked against a live consumer. // // These rows are NOT COVERED by anything. The host reports the count every run. fn long_home_prefixes() -> List { @@ -181,73 +95,6 @@ fn long_home_prefixes() -> List { } } -fn string_starts_with(s: String, prefix: String) -> Bool { - match string_head(s: prefix) { - CharAbsent => true - CharFound { value: p } => - match string_head(s: s) { - CharAbsent => false - CharFound { value: c } => - match c == p { - false => false - true => - match string_tail(s: s) { - TailAbsent => string_is_empty(s: prefix) - TailFound { tail: s_rest } => - match string_tail(s: prefix) { - TailAbsent => true - TailFound { tail: p_rest } => - string_starts_with(s: s_rest, prefix: p_rest) - } - } - } - } - } -} - -fn module_is_long_home(module_path: String) -> Bool { - fold_list( - xs: long_home_prefixes(), - empty: false, - cons: fn(acc, prefix) { - match acc { - true => true - false => string_starts_with(s: module_path, prefix: prefix) - } - } - ) -} - -fn disposition_is_required_floor(d: WitnessDisposition) -> Bool { - match d { - RequiredFloor => true - ScheduledWet => false - ScheduledLong => false - KnownRedQuarantine => false - DeletedNotAWitness => false - } -} - -// The manifest refuses rather than deduplicating. Silently merging populations is how a -// declaration enrolled twice under conflicting modes becomes one arbitrary winner — and the -// losing mode is precisely the fact worth having. Each arm names the exact qualified -// declaration, so the refusal is located rather than counted. -type ManifestRefusal - = DeclarationEnrolledTwice { qualified: String } - | DeclarationEnrolledWithConflictingModes { qualified: String } - -// ADMISSION IS ALL-OR-NOTHING. A manifest carrying any refusal does not run its clean subset -// and report on the rest: a narrowed roster presented as a roster is the silent narrowing -// every other wall on this path exists to prevent. -type RequiredFloorAdmission - = RequiredFloorRunnable { attempt: RequiredFloorAttempt } - | RequiredFloorRefused { refusals: List } - -data no_required_floor_claims: List = [] - -data no_manifest_refusals: List = [] - -data no_seen_qualified_names: List = [] // THE EVALUATION DEADLINE FOR ONE CLAIM IS WALL TIME, AND IT HAS TWO TIERS. // @@ -302,321 +149,3 @@ fn required_floor_claim_budget_ms() -> Int { fn required_floor_claim_warn_ms() -> Int { 100 } - -fn required_floor_claim_qualified(claim: RequiredFloorClaim) -> String { - witness_identity_qualified_name(identity: claim.declaration) -} - -// A resolved witness becomes a claim, or contributes its refusal. Both halves are produced in -// one pass so the refusal population and the claim population cannot disagree about which -// rows they came from. -type ClaimScan { - claims: List - refusals: List -} - -// THE SCAN ACCUMULATES BY PREPENDING AND REVERSES ONCE AT THE END. -// -// It appended with `list_snoc_item`, and that is quadratic in ALLOCATION, not merely in time. -// `list_snoc_item(xs, item)` is `list_append(left: xs, right: [item])`, and `list_append` folds -// over its LEFT argument rebuilding every cell — so appending one row to a list of length k -// allocates k+1 cells and abandons the k that were there. Appending n rows one at a time -// allocates about n squared over two. -// -// On the measured subject that is 10,444 claims, so roughly 54.6 MILLION cons cells for the -// claim list alone, each a variant carrying a boxed field vector. Manifest evaluation was -// observed peaking around 38 GB — about 29 GB above the prepared graph it starts from — to -// produce 10,444 records holding a module path, a function name, a mode and an integer. Nothing -// about the RESULT is large; all of it was the intermediates. -// -// WHY THIS SURVIVED UNTIL THE CUT, because it is the more useful half of the finding: the -// deleted floor packed workers to a memory envelope and prepared a separate small subject per -// worker, so no execution ever accumulated one 10,444-element list this way. The quadratic was -// always here; the governor kept it under the size where it is visible. Folding the whole roster -// once is what turned a latent cost shape into a measured peak, which is the deletion surfacing -// what the old structure was hiding rather than a regression the cut introduced. -// -// Prepending is O(1) per row and `list_reverse` is one linear prepend fold, so the population is -// built in time and allocation linear in its own size. Both populations are REVERSED-ORDER while -// the scan runs and are restored exactly once, in `required_floor_manifest`. The field names say -// so, because a list whose order is a lie is worse than a slow one: the obligation is discharged -// in a single named place rather than assumed by each reader. -type ClaimScanReversed { - claims_reversed: List - refusals_reversed: List -} - -data claim_scan_reversed_empty: ClaimScanReversed = ClaimScanReversed { - claims_reversed: no_required_floor_claims, - refusals_reversed: no_manifest_refusals - } - -// A SITE BECOMES A CLAIM DIRECTLY. There is no identity step between them any more, because the -// site arrives carrying the module the host read it from, so identification cannot fail. -// -// The refusal arm this replaced matched `WitnessIdentityRefused` and produced -// `DeclarationIdentityAmbiguous`. It had no reachable producer: sites and the entry-to-module -// list were projected from ONE inventory keyed by the same file path, so the join always found -// exactly one module. Keeping an arm for it would be validation of a state with no constructor. -// -// `refusals_reversed` survives because duplicate enrollment is a real refusal, discovered after -// the scan rather than during it. -// THE MANIFEST DECIDES ADMISSION, not the discovery that fed it. A long-home site produces no -// claim row, so the exclusion happens where the module's own name can be read rather than in a -// host-side test on its file path. -// -// A LIVE-TREE SITE IS EXCLUDED FOR A DIFFERENT REASON, and the difference matters. The long -// home is a hack. This one is a fact about the witness: a module declaring -// `LiveTreeDisposition = ReadsLiveTree` reads state outside its resolved closure, so it CANNOT -// execute in the hermetic frame this floor runs — it does not fail because it is broken, it -// fails because it was handed the wrong evaluator. Forcing it hermetic produced 172 refusals -// against missing mocks, and every one of them was the floor asking a live-tree witness to -// answer without the tree. -// -// Excluding them is parity with what the old floor already did: main defers 1,073 rows from -// per-PR discovery, 613 of them declaring exactly this. These rows were never covered per-PR -// before this cut and are not covered now — what changes is that the reason is an authored -// declaration read from the module's own source rather than a deferral nobody could see. -// -// This is NOT a disposition in the modelled sense and must not be mistaken for one: it names no -// executing consumer, so the coverage owed is real and outstanding. It dissolves when a wet -// lane exists to run them. -fn scan_site( - acc: ClaimScanReversed, - site: WitnessSite, - mode: ExecutionMode -) -> ClaimScanReversed { - match module_is_long_home(module_path: site.module_path) || site.reads_live_tree { - true => acc - false => - ClaimScanReversed { - claims_reversed: Cons { - head: RequiredFloorClaim { - declaration: WitnessIdentity { - module_path: site.module_path, - function: site.function - }, - execution_mode: mode, - budget_ms: required_floor_claim_budget_ms(), - warn_ms: required_floor_claim_warn_ms() - }, - tail: acc.claims_reversed - }, - refusals_reversed: acc.refusals_reversed - } - } -} - -fn scan_site_list( - acc: ClaimScanReversed, - sites: List, - mode: ExecutionMode -) -> ClaimScanReversed { - fold_list(xs: sites, empty: acc, cons: fn(inner, s) { - scan_site(acc: inner, site: s, mode: mode) - }) -} - -// COMPLETENESS IS AN IDENTITY JOIN, NEVER A COUNT. Two populations of equal size can name -// different declarations, and a count comparison reports that as clean. So the duplicate check -// asks about a qualified declaration's ENROLLMENTS, never about how many rows exist. -// -// ENROLLMENT IS A STATE PER NAME, not a group to be re-collected. The former shape asked, for -// each first-seen name, `claims_named(claims, qualified)` — a fold over the WHOLE claim list — -// while also testing membership with `contains` over a linearly growing `seen` list that was -// itself built by quadratic append. On 10,444 claims with mostly distinct names that is three -// separate quadratic populations layered on one pass, and none of them retains anything the -// verdict needs: the group was collected only to take its length and compare its modes. -// -// `QualifiedEnrollment` carries exactly what those questions consume, and it is a state machine -// over occurrences rather than a collection of them. Absence from the map is the zero case. -// The reference mode is the FIRST occurrence's, which is what the group form compared against, -// and once conflicting it stays conflicting — so a declaration enrolled Hermetic, Hermetic, Wet -// reports the conflict rather than reporting agreement from a prefix. -type QualifiedEnrollment - = FirstEnrollment { mode: ExecutionMode } - | RepeatedSameMode { mode: ExecutionMode } - | RepeatedConflictingModes - -fn enrollment_after(prior: QualifiedEnrollment, mode: ExecutionMode) -> QualifiedEnrollment { - match prior { - FirstEnrollment { mode: first } => - if execution_mode_eq(left: first, right: mode) { - RepeatedSameMode { mode: first } - } else { - RepeatedConflictingModes - } - RepeatedSameMode { mode: first } => - if execution_mode_eq(left: first, right: mode) { - RepeatedSameMode { mode: first } - } else { - RepeatedConflictingModes - } - RepeatedConflictingModes => RepeatedConflictingModes - } -} - -// The two duplicate arms are kept APART on purpose. The same declaration twice under one mode -// is a roster defect: it would run twice and the receipt would carry two rows for one -// subject. The same declaration under two modes is a contradiction about how it must execute, -// and the tempting repair — pick one — discards the fact worth knowing. Both refuse; only the -// second is unrecoverable without a decision. A name enrolled exactly once refuses nothing. -// Prepends onto the accumulator rather than returning a fragment to be appended: a zero-or-one -// fragment joined with `list_append` would rebuild the accumulator once per name, which is the -// same quadratic being removed above, merely over a smaller population. "n is small here" is not -// a fact that stays true — this list is one row per DUPLICATED declaration today because the -// roster is mostly clean, and the shape must not depend on the roster staying that way. -fn enrollment_refusal_prepend( - acc: List, - qualified: String, - enrollment: QualifiedEnrollment -) -> List { - match enrollment { - FirstEnrollment { mode: _ } => acc - RepeatedSameMode { mode: _ } => - Cons { head: DeclarationEnrolledTwice { qualified: qualified }, tail: acc } - RepeatedConflictingModes => - Cons { - head: DeclarationEnrolledWithConflictingModes { qualified: qualified }, - tail: acc - } - } -} - -type ManifestScan { - enrollments: Map - first_seen_reversed: List -} - -data manifest_scan_empty: ManifestScan = ManifestScan { - enrollments: empty_map(), - first_seen_reversed: no_seen_qualified_names - } - -// ONE refusal per distinct duplicated declaration, ordered by FIRST occurrence — the same -// contract the group form had, reached without collecting the group. `first_seen_reversed` -// records a name only when it is new, so it is both the dedup and the order, and a declaration -// enrolled three times still reports one refusal about itself rather than three. -// `first_seen_reversed` is walked in reverse — last-seen first — and the prepending fold flips -// it back, so the emitted refusals come out in FIRST-OCCURRENCE order with one reversal rather -// than two. -fn duplicate_refusals(claims: List) -> List { - let scan = fold_list(xs: claims, empty: manifest_scan_empty, cons: fn(acc, c) { - let qualified = required_floor_claim_qualified(claim: c) - match map_lookup(m: acc.enrollments, key: qualified) { - Absent => ManifestScan { - enrollments: map_insert( - m: acc.enrollments, - key: qualified, - value: FirstEnrollment { mode: c.execution_mode } - ), - first_seen_reversed: Cons { head: qualified, tail: acc.first_seen_reversed } - } - Present { value: prior } => ManifestScan { - enrollments: map_insert( - m: acc.enrollments, - key: qualified, - value: enrollment_after(prior: prior, mode: c.execution_mode) - ), - first_seen_reversed: acc.first_seen_reversed - } - } - }) - fold_list( - xs: scan.first_seen_reversed, - empty: no_manifest_refusals, - cons: fn(acc, qualified) { - match map_lookup(m: scan.enrollments, key: qualified) { - Present { value: e } => - enrollment_refusal_prepend(acc: acc, qualified: qualified, enrollment: e) - Absent => acc - } - } - ) -} - -// THE MANIFEST. One fold over the observed populations, producing one claim list and the -// refusals that make it inadmissible. -// -// `hermetic_sites` are the discovered witnesses; `wet_sites` are the rows whose subject IS a -// live effect and which therefore need a dispatching frame. They arrive as separate lists -// rather than one list with a flag because the flag would have to be derived from something, -// and the only available something is `reads_live_tree` — the derivation this module refuses -// to make. -// THIS IS THE ONE PLACE REVERSED ORDER IS DISCHARGED. Both scan populations were prepended and -// are restored here, once each, before anything reads them. Hermetic claims precede wet ones and -// each class keeps its site order, exactly as the appending form produced them. -fn required_floor_manifest( - hermetic_sites: List, - wet_sites: List -) -> ClaimScan { - let after_hermetic = scan_site_list( - acc: claim_scan_reversed_empty, - sites: hermetic_sites, - mode: Hermetic - ) - let after_wet = scan_site_list( - acc: after_hermetic, - sites: wet_sites, - mode: Wet - ) - let claims = list_reverse(xs: after_wet.claims_reversed) - ClaimScan { - claims: claims, - refusals: list_append( - left: list_reverse(xs: after_wet.refusals_reversed), - right: duplicate_refusals(claims: claims) - ) - } -} - -fn required_floor_attempt( - subject: ObservedSubjectIdentity, - hermetic_sites: List, - wet_sites: List -) -> RequiredFloorAdmission { - let scan = required_floor_manifest( - hermetic_sites: hermetic_sites, - wet_sites: wet_sites - ) - if length(xs: scan.refusals) == 0 { - RequiredFloorRunnable { - attempt: RequiredFloorAttempt { subject: subject, claims: scan.claims } - } - } else { - RequiredFloorRefused { refusals: scan.refusals } - } -} - -// ONE PREPARATION PER ATTEMPT, and it holds by SHAPE rather than by counting. An attempt -// names exactly one subject, and a claim carries only an identity — no source root, no -// index, no resolver, no entry path — so nothing a consumer of this model holds can name a -// second compiler world. An entry-private preparation is unrepresentable here, not merely -// absent, and that is why there is no cache to look in and no memo to hide repeated demands. -fn required_floor_preparation_count(attempt: RequiredFloorAttempt) -> Int { - 1 -} - -fn required_floor_claim_count(attempt: RequiredFloorAttempt) -> Int { - length(xs: attempt.claims) -} - -// The attempt's effect envelope, derived from its claims rather than declared beside them. A -// separately declared envelope drifts: a manifest gains a Wet row while a hand-written -// Hermetic declaration keeps saying the run reaches no live effects. It widens rather than -// narrows, which is the fail-closed direction — an envelope narrower than some claim would -// let a consumer treat a live-effect run as inert. Record is widest because it dispatches AND -// captures. -fn required_floor_effect_envelope(attempt: RequiredFloorAttempt) -> ExecutionMode { - fold_list(xs: attempt.claims, empty: Hermetic, cons: fn(acc, c) { - match acc { - Record => Record - Wet => match c.execution_mode { - Record => Record - Wet => Wet - Hermetic => Wet - } - Hermetic => c.execution_mode - } - }) -}