diff --git a/.github/workflows/witnesses.yml b/.github/workflows/witnesses.yml index 91081c52c23..dfd94a9e3d1 100644 --- a/.github/workflows/witnesses.yml +++ b/.github/workflows/witnesses.yml @@ -404,9 +404,91 @@ jobs: retention-days: 14 include-hidden-files: true if: always() && steps.heal_commit_push.outputs.heal_author_commit_required == '1' + required-v2-native: + runs-on: [self-hosted, linux, arm64] + timeout-minutes: 180 + steps: + - name: Record runner filesystem at job start + run: | + # dissolve-on: toolchain_filesystem_probe -- delete the start/end runner-filesystem instrument after its joined readings identify and the fleet fixes the toolchain deleter, OR after per-job runner microVMs make the shared filesystem eviction class impossible + GUNBC_TOOLCHAIN_FS_PREFIX='GUNBC_TOOLCHAIN_FS phase=start' + echo "$GUNBC_TOOLCHAIN_FS_PREFIX time_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unavailable) host=$(hostname 2>/dev/null || echo unavailable) runner=${RUNNER_NAME:-unset}" + echo "$GUNBC_TOOLCHAIN_FS_PREFIX runner_temp=${RUNNER_TEMP:-unset} rustup_home=${RUSTUP_HOME:-unset} cargo_home=${CARGO_HOME:-unset} uptime_seconds=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || echo unavailable)" + echo "$GUNBC_TOOLCHAIN_FS_PREFIX loadavg=$(tr ' ' ',' < /proc/loadavg 2>/dev/null || echo unavailable)" + df -Pk "${RUNNER_TEMP:-/path-that-does-not-exist}" 2>&1 | while IFS= read -r GUNBC_TOOLCHAIN_FS_LINE; do echo "$GUNBC_TOOLCHAIN_FS_PREFIX df_blocks=$GUNBC_TOOLCHAIN_FS_LINE"; done + df -Pik "${RUNNER_TEMP:-/path-that-does-not-exist}" 2>&1 | while IFS= read -r GUNBC_TOOLCHAIN_FS_LINE; do echo "$GUNBC_TOOLCHAIN_FS_PREFIX df_inodes=$GUNBC_TOOLCHAIN_FS_LINE"; done + for GUNBC_TOOLCHAIN_FS_PATH in "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin/rustc "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin/rustfmt "${RUNNER_TEMP:-/path-that-does-not-exist}"/cargo/bin/cargo; do if [ -e "$GUNBC_TOOLCHAIN_FS_PATH" ]; then GUNBC_TOOLCHAIN_FS_STAT=$(stat -c 'size=%s mtime_epoch=%Y mtime=%y inode=%i' "$GUNBC_TOOLCHAIN_FS_PATH" 2>&1 || echo stat-unavailable); echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=file path=$GUNBC_TOOLCHAIN_FS_PATH exists=yes $GUNBC_TOOLCHAIN_FS_STAT"; else echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=file path=$GUNBC_TOOLCHAIN_FS_PATH exists=no size=unavailable mtime_epoch=unavailable mtime=unavailable inode=unavailable"; fi; done + for GUNBC_TOOLCHAIN_FS_PATH in "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains "${RUNNER_TEMP:-/path-that-does-not-exist}"/cargo/bin; do if [ -d "$GUNBC_TOOLCHAIN_FS_PATH" ]; then GUNBC_TOOLCHAIN_FS_STAT=$(stat -c 'mtime_epoch=%Y mtime=%y inode=%i' "$GUNBC_TOOLCHAIN_FS_PATH" 2>&1 || echo stat-unavailable); echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=directory path=$GUNBC_TOOLCHAIN_FS_PATH exists=yes $GUNBC_TOOLCHAIN_FS_STAT"; else echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=directory path=$GUNBC_TOOLCHAIN_FS_PATH exists=no mtime_epoch=unavailable mtime=unavailable inode=unavailable"; fi; done + if: always() + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + ref: ${{ inputs.expected_healed_sha || github.sha }} + - name: Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA + run: | + EXPECTED_HEALED_SHA="${{ inputs.expected_healed_sha }}" + RUN_SUBJECT_HEAD="${{ github.sha }}" + if [ -n "$EXPECTED_HEALED_SHA" ]; then + ACTUAL_HEAD="$(git rev-parse HEAD)" + if ! [ "$RUN_SUBJECT_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: workflow run subject $RUN_SUBJECT_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + if ! [ "$ACTUAL_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: checkout head $ACTUAL_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + echo "heal revalidation preflight: checkout names expected healed head $EXPECTED_HEALED_SHA" + fi + - name: Isolate toolchain homes + run: | + # dissolve-on: ci_toolchain_home_isolation_script -- orch-emitted foreign-executor prelude step wiping and setting HOME/CARGO_HOME/RUSTUP_HOME under RUNNER_TEMP so concurrent runner slots stop sharing one toolchain; leaf rm/echo strings remain until a typed per-job filesystem-and-environment effect lands on host_effect_apply (shell-to-intent Phase 2). This obligation covers THIS carrier and ci_isolate_toolchain_script, which share that terminal construction; ci_pin_rustup_default_script carries its own obligation because it does not + rm -rf "$RUNNER_TEMP/rustup" "$RUNNER_TEMP/cargo" + echo "HOME=$RUNNER_TEMP" >> "$GITHUB_ENV" + echo "CARGO_HOME=$RUNNER_TEMP/cargo" >> "$GITHUB_ENV" + echo "RUSTUP_HOME=$RUNNER_TEMP/rustup" >> "$GITHUB_ENV" + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1.16.0 + with: + components: rustfmt + cache: false + - name: Pin rustup default (isolated RUSTUP_HOME has no default toolchain) + run: | + # dissolve-on: ci_pin_rustup_default_script -- orch-emitted foreign-executor step selecting a rustup default toolchain inside an isolated RUSTUP_HOME, which starts with none, and resolving the cargo binary that selection implies. The leaf rustup/command/echo strings remain until a typed TOOLCHAIN-SELECTION effect lands on host_effect_apply -- NOT the filesystem-and-environment effect ci_toolchain_home_isolation_script waits on, which is why this is a separate obligation: that effect landing alone would leave this carrier standing + rustup default "$(rustup show active-toolchain | awk '{print $1; exit}')" + if [ -x "$CARGO_HOME/bin/cargo" ]; then CARGO_BIN="$CARGO_HOME/bin/cargo"; else CARGO_BIN="$(command -v cargo || true)"; fi + if [ -z "$CARGO_BIN" ]; then echo "::error::no cargo binary: neither the isolated $CARGO_HOME/bin/cargo shim nor PATH carries one"; exit 1; fi + echo "CARGO_BIN=$CARGO_BIN" >> "$GITHUB_ENV" + - name: Build the witness fold + id: build_witness_fold + run: | + cargo build --release -p v1-compiler --bin claim_executor --bin gunbc + - name: Require isolated toolchain homes + run: | + if [ -z "${CARGO_HOME:-}" ] || [ -z "${RUSTUP_HOME:-}" ]; then echo "::error::ToolchainHomesNotIsolated CARGO_HOME=${CARGO_HOME:-unset} RUSTUP_HOME=${RUSTUP_HOME:-unset} -- the isolation step did not run, so this job shares a toolchain with every other runner slot on this host and a concurrent install can replace a binary mid-run" >&2; exit 1; fi + if: "!cancelled()" + - name: "Required CI: v2-native lane (emitted-native compiler executes the derived v2.test.* universe)" + run: | + ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) + cd "$ROOT" + 'target/release/claim_executor' '--required-ci' '--source-root' 'dag' '--source-root' 'src/v2' '--required-lane' 'v2-native' + if: "!cancelled() && steps.build_witness_fold.outcome == 'success'" + - name: Record runner filesystem at job end + run: | + # dissolve-on: toolchain_filesystem_probe -- delete the start/end runner-filesystem instrument after its joined readings identify and the fleet fixes the toolchain deleter, OR after per-job runner microVMs make the shared filesystem eviction class impossible + GUNBC_TOOLCHAIN_FS_PREFIX='GUNBC_TOOLCHAIN_FS phase=end' + echo "$GUNBC_TOOLCHAIN_FS_PREFIX time_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unavailable) host=$(hostname 2>/dev/null || echo unavailable) runner=${RUNNER_NAME:-unset}" + echo "$GUNBC_TOOLCHAIN_FS_PREFIX runner_temp=${RUNNER_TEMP:-unset} rustup_home=${RUSTUP_HOME:-unset} cargo_home=${CARGO_HOME:-unset} uptime_seconds=$(cut -d' ' -f1 /proc/uptime 2>/dev/null || echo unavailable)" + echo "$GUNBC_TOOLCHAIN_FS_PREFIX loadavg=$(tr ' ' ',' < /proc/loadavg 2>/dev/null || echo unavailable)" + df -Pk "${RUNNER_TEMP:-/path-that-does-not-exist}" 2>&1 | while IFS= read -r GUNBC_TOOLCHAIN_FS_LINE; do echo "$GUNBC_TOOLCHAIN_FS_PREFIX df_blocks=$GUNBC_TOOLCHAIN_FS_LINE"; done + df -Pik "${RUNNER_TEMP:-/path-that-does-not-exist}" 2>&1 | while IFS= read -r GUNBC_TOOLCHAIN_FS_LINE; do echo "$GUNBC_TOOLCHAIN_FS_PREFIX df_inodes=$GUNBC_TOOLCHAIN_FS_LINE"; done + for GUNBC_TOOLCHAIN_FS_PATH in "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin/rustc "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin/rustfmt "${RUNNER_TEMP:-/path-that-does-not-exist}"/cargo/bin/cargo; do if [ -e "$GUNBC_TOOLCHAIN_FS_PATH" ]; then GUNBC_TOOLCHAIN_FS_STAT=$(stat -c 'size=%s mtime_epoch=%Y mtime=%y inode=%i' "$GUNBC_TOOLCHAIN_FS_PATH" 2>&1 || echo stat-unavailable); echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=file path=$GUNBC_TOOLCHAIN_FS_PATH exists=yes $GUNBC_TOOLCHAIN_FS_STAT"; else echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=file path=$GUNBC_TOOLCHAIN_FS_PATH exists=no size=unavailable mtime_epoch=unavailable mtime=unavailable inode=unavailable"; fi; done + for GUNBC_TOOLCHAIN_FS_PATH in "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains/*/bin "${RUNNER_TEMP:-/path-that-does-not-exist}"/rustup/toolchains "${RUNNER_TEMP:-/path-that-does-not-exist}"/cargo/bin; do if [ -d "$GUNBC_TOOLCHAIN_FS_PATH" ]; then GUNBC_TOOLCHAIN_FS_STAT=$(stat -c 'mtime_epoch=%Y mtime=%y inode=%i' "$GUNBC_TOOLCHAIN_FS_PATH" 2>&1 || echo stat-unavailable); echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=directory path=$GUNBC_TOOLCHAIN_FS_PATH exists=yes $GUNBC_TOOLCHAIN_FS_STAT"; else echo "$GUNBC_TOOLCHAIN_FS_PREFIX kind=directory path=$GUNBC_TOOLCHAIN_FS_PATH exists=no mtime_epoch=unavailable mtime=unavailable inode=unavailable"; fi; done + if: always() witnesses: runs-on: [self-hosted, linux, arm64] - needs: [required-witnesses-build, required-witnesses-floor] + needs: [required-witnesses-build, required-witnesses-floor, required-v2-native] timeout-minutes: 5 if: always() steps: @@ -416,16 +498,17 @@ jobs: if '[' "$EVENT_NAME" '=' 'pull_request' ']'; then HEAD_STANDING='unobserved'; fi 'echo' 'required floor attempt: pull_request='"$PULL_REQUEST"' measured_head='"$MEASURED_HEAD"' run_id='"$RUN_ID"' head_standing='"$HEAD_STANDING" VERDICT='stands-green' - if '[' "$BUILD" '!=' 'success' ']' || '[' "$FLOOR" '!=' 'success' ']'; then VERDICT='stands-unestablished'; fi - if '[' "$BUILD" '=' 'failure' ']' || '[' "$FLOOR" '=' 'failure' ']'; then VERDICT='stands-red'; fi + if '[' "$BUILD" '!=' 'success' ']' || '[' "$FLOOR" '!=' 'success' ']' || '[' "$V2_NATIVE" '!=' 'success' ']'; then VERDICT='stands-unestablished'; fi + if '[' "$BUILD" '=' 'failure' ']' || '[' "$FLOOR" '=' 'failure' ']' || '[' "$V2_NATIVE" '=' 'failure' ']'; then VERDICT='stands-red'; fi MECHANISM='unestablished' ATTRIBUTION='unestablished' - 'echo' 'required lanes: build='"$BUILD"' floor='"$FLOOR"' verdict='"$VERDICT"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION" - if '[' "$VERDICT" '=' 'stands-red' ']'; then 'echo' '::error::a required lane concluded failure; that alone does not establish whether its subject was evaluated, so read the log before assuming a defect in the diff (build='"$BUILD"' floor='"$FLOOR"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION"') - open that job'\''s log' >&2; exit 1; fi - if '[' "$VERDICT" '=' 'stands-unestablished' ']'; then 'echo' '::error::a required lane produced no conclusion of its own, so this head'\''s floor verdict is unobservable rather than failed; rerun it (build='"$BUILD"' floor='"$FLOOR"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION"') - open that job'\''s log' >&2; exit 1; fi + 'echo' 'required lanes: build='"$BUILD"' floor='"$FLOOR"' v2-native='"$V2_NATIVE"' verdict='"$VERDICT"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION" + if '[' "$VERDICT" '=' 'stands-red' ']'; then 'echo' '::error::a required lane concluded failure; that alone does not establish whether its subject was evaluated, so read the log before assuming a defect in the diff (build='"$BUILD"' floor='"$FLOOR"' v2-native='"$V2_NATIVE"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION"') - open that job'\''s log' >&2; exit 1; fi + if '[' "$VERDICT" '=' 'stands-unestablished' ']'; then 'echo' '::error::a required lane produced no conclusion of its own, so this head'\''s floor verdict is unobservable rather than failed; rerun it (build='"$BUILD"' floor='"$FLOOR"' v2-native='"$V2_NATIVE"' mechanism='"$MECHANISM"' attribution='"$ATTRIBUTION"') - open that job'\''s log' >&2; exit 1; fi env: BUILD: ${{ needs['required-witnesses-build'].result }} FLOOR: ${{ needs['required-witnesses-floor'].result }} + V2_NATIVE: ${{ needs['required-v2-native'].result }} PULL_REQUEST: ${{ github.event.pull_request.number }} EVENT_NAME: ${{ github.event_name }} MEASURED_HEAD: ${{ github.event.pull_request.head.sha }} diff --git a/dag/gunbc/fabric/fabric_witness_run.dag b/dag/gunbc/fabric/fabric_witness_run.dag index 7d75ec6e013..46af6e1d325 100644 --- a/dag/gunbc/fabric/fabric_witness_run.dag +++ b/dag/gunbc/fabric/fabric_witness_run.dag @@ -20,7 +20,7 @@ import product.fabric.work { work_for, } import std.content_hash { ContentHash, content_hash_of_value, serialize_content_hash } -import gunbc.required_ci_phase_roster { BuildLane, WitnessesLane, required_ci_lane_name } +import gunbc.required_ci_phase_roster { BuildLane, WitnessesLane, V2NativeLane, required_ci_lane_name } import extdeps.git.object_store { GitObjectId, git_object_id_wire_hex, git_object_id_content_hash, git_sha1_object_id } import product.fabric.supply { SupplierOffer, OfferFungibility, DemandOfferAffordability, GrantDuration, EstimatedGrantDuration, @@ -555,6 +555,15 @@ fn build_lane_run_command() -> ArgvCommand { required_ci_lane_command(lane: required_ci_lane_build, measurement_receipt: "") } +data required_ci_lane_v2_native: String = required_ci_lane_name(l: V2NativeLane) + +// The v2-native lane: the emitted-native compiler executes the derived v2.test.* universe +// through its own whole-source-root Eval driver. The job contract and admission authority are +// gunbc.witness_v2_native_route; this command is only the argv the workflow step runs. +fn v2_native_lane_run_command() -> ArgvCommand { + required_ci_lane_command(lane: required_ci_lane_v2_native, measurement_receipt: "") +} + // THE FLOOR IS MODELED AS AN ORDINARY PRICED DEMAND. NO PRICE IS CLEARED TODAY. // // The intent is unchanged: gunbc's own CI enters the market on the same terms as any other demand, diff --git a/dag/gunbc/instruments/e0599_emitter_decision_census.dag b/dag/gunbc/instruments/e0599_emitter_decision_census.dag index 7bb2d0f7fae..f36a6ed888b 100644 --- a/dag/gunbc/instruments/e0599_emitter_decision_census.dag +++ b/dag/gunbc/instruments/e0599_emitter_decision_census.dag @@ -133,7 +133,7 @@ data e0599_im_vector_inherent_impl: E0599ExternalAuthority = E0599ExternalAuthor crate_version: "15.1.0", impl_site: "im::vector impl Vector (src/vector/mod.rs:180)", impl_bound: "impl Vector", - member_site: "im::vector Vector::is_empty | Vector::iter", + member_site: "im::vector Vector::is_empty | Vector::skip", cited_upstream: Absent, } @@ -181,11 +181,11 @@ data e0599_lowering_rows: List = [ ownership_alternative: "NotApplicable - a read; no copy is requested and no ownership predicate gates it", source_construct: "match over the FreeMonoid coproduct — the Cons { tail } binding", emitter_authority: "v1.compiler.emit_rust freemonoid_tail_let_from_fm", - emitted_literal: "let : Rc> = Rc::new((*__fm).iter().skip(1).cloned().collect()); ", + emitted_literal: "let : Rc> = Rc::new(__fm.skip(1)); ", cause: TargetApiRequirement, required_trait: "Clone", authority: e0599_im_vector_inherent_impl, - rationale: "iter() is in the same impl block as is_empty. rustc reports the failure at .iter() and never reaches the .cloned() that follows it, so this site MASKS a co-located OwnedDeconstructionRequirement on the same expression; both discharge on the same A: Clone, but B1 must not read this row as evidence that the tail extract needs no owned element.", + rationale: "skip() is in the same impl block as is_empty (it is self.clone().split_off(count)), so the bound survives the 2026-09-09 relowering; what that relowering removed was the O(n) tail MATERIALIZATION the old iter().skip(1).cloned().collect() site paid per cons-match — measured quadratic on the required-v2-native lane's whole-corpus lex fold (22KB file: 23.3s; 899B file: 70ms). The structural skip is O(log n) and shares the RRB tree, so the tail binding is now the read the source construct always was.", }, E0599LoweringRow { operation: FreeMonoidHeadExtract, @@ -284,7 +284,7 @@ fn e0599_receiver_is_field_access(receiver_expr: String) -> Bool { fn e0599_lowering_operation_for(method: String, receiver_expr: String) -> E0599LoweringOperation { if method == "is_empty" && receiver_expr == "__fm" { FreeMonoidEmptyTest - } else if method == "iter" && receiver_expr == "(*__fm)" { + } else if method == "skip" && receiver_expr == "__fm" { FreeMonoidTailIterate } else if method == "clone" && receiver_expr == "(*__fm)[0]" { FreeMonoidHeadExtract diff --git a/dag/gunbc/machine_intake/megarac_media_attach.dag b/dag/gunbc/machine_intake/megarac_media_attach.dag index ed36eee5f03..8b5298a800d 100644 --- a/dag/gunbc/machine_intake/megarac_media_attach.dag +++ b/dag/gunbc/machine_intake/megarac_media_attach.dag @@ -416,8 +416,12 @@ fn presented_media_standing(body: String, image_name: NonEmptyStr) -> PresentedM // merely still mounting. The bounded read-back decides; the status code only distinguishes which // unconfirmed state is reported. // -// STARTREFUSED IS RESERVED for a start that failed AND whose configurations document is itself -// unreadable as JSON — that is, for when the read-back cannot even be asked. +// THE READ-BACK DECIDES, INCLUDING WHEN START REPORTED FAILURE. This firmware answers HTTP 500 +// on writes that take effect; mapping start.success == false to MegaRacStartRefused refused an +// attach that was merely still mounting, and contradicted the comment this function already +// carried. A missing confirmation after any start response is pending (Unconfirmed). StartRefused +// is reserved for a start that failed AND whose configurations document is itself unreadable +// as JSON — that is, when we cannot even ask the read-back. func megarac_start_and_confirm( bmc_host: NonEmptyStr, cookie_jar: NonEmptyStr, diff --git a/dag/gunbc/required_ci_phase_roster.dag b/dag/gunbc/required_ci_phase_roster.dag index e6e086be083..c4f5c6f58bd 100644 --- a/dag/gunbc/required_ci_phase_roster.dag +++ b/dag/gunbc/required_ci_phase_roster.dag @@ -14,25 +14,28 @@ import std.types { String, Bool, List } // // THE HOST REALIZATION IS A DECLARED PARALLEL REPRESENTATION WITH AN EXECUTING JOIN, NOT A TWIN // LEFT TO DRIFT. claim_executor's RequiredCiPhase enum realizes this coproduct, and every -// --required-ci run joins the two BEFORE running any phase: set equality over this declaration's -// variant names, both directions, so "declared here and not realized" and "realized and not -// declared" are separate refusals that stop the line (the join shape -// v1_compiler.namespace_wave_admission vocabulary_findings executes against its disposition -// authority). The join deliberately does NOT cover lane ownership -- the host lane match is not -// readable from the declaration index -- and that residue is honest for enrolment truth: both lanes -// execute in every required run, so a lane divergence can move a phase between two jobs that both -// run; it cannot un-enrol one. The terminal state is the census's dissolution row: the workflow -// emission consumes this roster whole and the host enum, lane match and REQUIRED_CI_PHASES list -// delete atomically. Until then the parallel representation is this module's declared debt and the -// per-run join is its wall. +// --required-ci run joins the two BEFORE running any phase, at two grains: set equality over this +// declaration's variant names (existence, read off the declaration index on the parse phase's +// ingestion), and set equality over required_ci_lane_phase_rows' (lane, phase) pairs (ownership, +// evaluated in EVERY lane at run start, because a match arm is not a declaration and the index +// cannot read one). The pair join carries two further refusals: a selected lane whose +// authority-expected phase set is empty stops rather than greening over zero phases, and the +// observed ran set must equal the authority-expected set exactly. Lane ownership is load-bearing +// now that the workflow is three independently selected lanes with the native route isolated in +// its own. The terminal state is the census's dissolution row: the workflow emission consumes +// this roster whole and the host enum, lane match and REQUIRED_CI_PHASES list delete atomically. +// Until then the parallel representation is this module's declared debt and the per-run joins are +// its wall. type RequiredCiLane = BuildLane | WitnessesLane + | V2NativeLane fn required_ci_lane_name(l: RequiredCiLane) -> String { match l { BuildLane => "build" WitnessesLane => "witnesses" + V2NativeLane => "v2-native" } } @@ -61,6 +64,7 @@ type RequiredCiPhase | GeneratedArtifactPhase | RegenFixedPointPhase | FloorPhase + | V2NativePhase // TWO NAMED ARMS, NEVER A WIDENED PATH PREDICATE. A path that neither authority owns is in // neither arm; inability to produce either arm refuses GeneratedArtifactPhase. The registry arm @@ -106,6 +110,7 @@ fn required_ci_phase_name(p: RequiredCiPhase) -> String { GeneratedArtifactPhase => "generated-artifact" RegenFixedPointPhase => "regen-fixed-point" FloorPhase => "floor" + V2NativePhase => "v2-native" } } @@ -118,6 +123,7 @@ fn required_ci_phase_lane(p: RequiredCiPhase) -> RequiredCiLane { GeneratedArtifactPhase => BuildLane RegenFixedPointPhase => BuildLane FloorPhase => WitnessesLane + V2NativePhase => V2NativeLane } } @@ -131,6 +137,7 @@ data required_ci_phases: List = [ GeneratedArtifactPhase, RegenFixedPointPhase, FloorPhase, + V2NativePhase, ] fn required_ci_phase_is(a: RequiredCiPhase, b: RequiredCiPhase) -> Bool { @@ -149,3 +156,22 @@ fn lane_phase_names(lane: RequiredCiLane, phases: List) -> List |> filter(p => required_ci_lane_name(l: required_ci_phase_lane(p: p)) == required_ci_lane_name(l: lane)) |> map(p => required_ci_phase_name(p: p)) } + +// THE (LANE, PHASE) ROWS THE HOST'S OWNERSHIP JOIN EVALUATES, AS ONE NULLARY CALL. A match arm +// is not a declaration, so lane ownership cannot leave this module through the declaration index; +// it leaves through this function, in the CLI spelling the host matches on. The host joins these +// pairs against its own enum-plus-lane-match in both directions at every required run's start, +// refuses a selected lane whose expected set here is empty, and requires the observed ran set to +// equal the expected set exactly. +type RequiredCiLanePhaseRow { + lane: String + phase: String +} + +fn required_ci_lane_phase_rows() -> List { + required_ci_phases + |> map(p => RequiredCiLanePhaseRow { + lane: required_ci_lane_name(l: required_ci_phase_lane(p: p)), + phase: required_ci_phase_name(p: p) + }) +} diff --git a/dag/gunbc/witness/v2_native_route.dag b/dag/gunbc/witness/v2_native_route.dag new file mode 100644 index 00000000000..b0336d076e9 --- /dev/null +++ b/dag/gunbc/witness/v2_native_route.dag @@ -0,0 +1,858 @@ +module gunbc.witness_v2_native_route + +import v2.std.algebra { any, filter, fold_list, is_empty, length, list_snoc_item } +import v2.std.collection { List } +import v2.std.integer { Int } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.optional { Absent, Optional, Present } +import v2.compiler.compile { + NativeTestVerdict, NativeTestPassed, NativeTestReturnedFalse, NativeTestReturnedOther, NativeTestRefused, + NativeTestStage, NativeTestStageContext, NativeTestStagePrepare, NativeTestStageEntry, NativeTestStageEval, + NativeTestFileRefusal +} +import v2.workflow.compile_door_ledger { + DiagnosticGrain, FatalGrain, HeadGrain, cause_ownership_lookup +} +import v2.workflow.floor_expected_red { floor_expected_red_roster } +import v2.workflow.floor_route_gap { floor_route_gap_roster } + +// THE LANE AUTHORITY FOR THE required-v2-native CI JOB (operator authorization 2026-09-09: one +// additional required job, modelled beside the workflow authority that projects it — never a +// hand-edit of the generated YAML). The job's subject is the compiler/test EXECUTION ROUTE +// itself: the emitted-native compiler binary, invoked by explicit path, executing a derived +// v2.test.* population through its own whole-source-root Eval driver +// (std.compiler_entry SourceRootEvalDriver — the v2.compiler.compile native_test_* fold), and +// reaching a TERMINAL TEST VERDICT per identity. The 32 native-only construct arms are +// explicitly NOT this proof — they show generated Rust programs ran natively while the claim +// executor stayed seed-backed; this lane's receipt must show which binary answered, and what +// verdict each named test reached. +// +// THE TERMINAL SUBJECT IS THE TEST IDENTITY, NOT THE MODULE. A module accepted for translation +// proves the front door opened; it says nothing about whether any declaration inside it was +// located, evaluated, or returned its expected verdict. Every row of this lane's population is +// keyed by the qualified `.` identity — the same grain the required floor +// gates on — and the verdict carried is the driver's own NativeTestVerdict, imported from +// v2.compiler.compile rather than re-declared here (DESIGN 3: one authority for the verdict +// vocabulary; a second route-local verdict type would be a nickname). +// +// THE ROUTE IS EVAL, NOT TRANSLATION. The driver resolves the exact test declaration inside a +// whole-ingest context, infers it, and evaluates it under eval_default_interpretation — the +// same hermetic effect policy the floor's execution mode runs (every host-effect boundary +// disallowed). A test that reaches for a host effect refuses at the Eval stage on this route, +// exactly as it gaps on the floor; the floor_route_gap roster is the independently established +// expectation that pairing consumes. + +// THE JOB IDENTITY IS ONE ROW. The workflow job materialization, the receipt, and the witnesses +// all cite this declaration; no second spelling of the job name anywhere. +data required_v2_native_job_name: String = "required-v2-native" + +// THE UNIVERSE IS A PREFIX, NOT A ROSTER. The population is DERIVED from the tree's test +// discovery by prefix membership — a hand-listed roster would silently drift from the tree it +// claims to cover. The prefix is the same `v2.test.` the required floor gates on +// (v2.workflow.required_floor required_gate_prefixes): the native lane's universe is the floor's +// witness universe asked of a different route. The universe is derived by the host harness from +// the floor's own discovery producer (v2.workflow.floor_discovery_producer) joined against each +// entry's authored module header, and carried on the receipt; admission checks its shape and +// joins the observed population against it exactly. +data native_route_universe_prefix: String = "v2.test." + +fn native_route_universe_member(module_qn: String) -> Bool { + starts_with(s: module_qn, prefix: native_route_universe_prefix) +} + +// ONE TEST IDENTITY, STRUCTURED. The module qualified name and the declaration name are separate +// observations — the emitted binary reports them separately, and the dotted spelling is DERIVED +// here (never parsed back out of a string), the same derivation the floor's +// witness_identity_qualified_name makes. Function names carry no dots, so the dotted form's last +// segment is always the declaration; the split is unambiguous in both directions. +type NativeRouteTestIdentity { + module: String + declaration: String +} + +fn native_route_identity_qualified(identity: NativeRouteTestIdentity) -> String { + concat(concat(identity.module, "."), identity.declaration) +} + +fn native_route_identity_eq(a: NativeRouteTestIdentity, b: NativeRouteTestIdentity) -> Bool { + a.module == b.module && a.declaration == b.declaration +} + +fn native_route_identity_member(target: NativeRouteTestIdentity, xs: List) -> Bool { + any(xs: xs, predicate: fn(x) { native_route_identity_eq(a: x, b: target) }) +} + +// NO DUPLICATE IDENTITIES, asked the same way of the universe and of the observed population: an +// identity appearing twice means the derivation double-counted a declaration or the harness +// recorded one run twice, and both are harness defects, not results. Quadratic by construction — +// the interpreter measured this shape over the full derived population at tens of seconds, and +// the standing bare-minimum-cost ruling prices the fold, not the realized n. +fn native_route_identities_unique(xs: List) -> Bool { + !any(xs: xs, predicate: fn(x) { + length(xs: filter(xs: xs, predicate: fn(y) { native_route_identity_eq(a: x, b: y) })) > 1 + }) +} + +// THE REFERENCE EXPECTATION, PER MEMBER, FROM THE FLOOR'S OWN ROSTERS. Pairing a native verdict +// against nothing proves nothing about agreement; the independently established expectation is +// the required floor's, joined at the exact qualified identity. Three arms, derived — never +// transcribed: route-gap membership is asked FIRST because an identity on both rosters is a +// route-gap fact (the floor executes it and it gaps), and expected-red second; the remainder is +// the floor's standing expectation for every discovered v2.test.* member — pass — which is the +// expectation a green floor asserts of every unrostered identity by construction. +type NativeRouteReference + = FloorExpectedPass + | FloorExpectedRed + | FloorRouteGapRefusal + +fn native_route_reference_for( + qualified: String, + expected_red: List, + route_gap: List +) -> NativeRouteReference { + if any(xs: route_gap, predicate: fn(r) { r == qualified }) { + FloorRouteGapRefusal + } else if any(xs: expected_red, predicate: fn(r) { r == qualified }) { + FloorExpectedRed + } else { + FloorExpectedPass + } +} + +// ONE ROW PER UNIVERSE MEMBER: the identity and the verdict the emitted native binary observed +// for it. The verdict is the driver's NativeTestVerdict verbatim — Passed, ReturnedFalse, +// ReturnedOther, or Refused at a named stage with the fatal reason — so the receipt carries what +// the route observed, not a summary of it. +type NativeRouteMemberRow { + identity: NativeRouteTestIdentity + verdict: NativeTestVerdict +} + +// THE MODULE → SOURCE-PATH RELATION, OVER THE UNIVERSE'S OWN MODULES. A Context-stage row is a +// derived classification minted by the host from a file refusal, and the derivation's join key +// is this relation: the observation names a module, the file refusal names a path, and only the +// module → path row binds the two. The receipt carries it (restricted to the universe's modules +// — the whole domain the classification can touch) so admission can require the backing file +// refusal to belong to the ROW'S OWN module, not to some file anywhere with the same reason. +// The path is a Symbol because the join partner — NativeTestFileRefusal.path — is one. +type NativeRouteModuleSource { + module: String + path: Symbol +} + +// THE EXCLUSION TAXONOMY, CLASSIFIED FROM STAGE AND PROVENANCE — never from the reason symbol +// alone (the review's standing instruction: the same symbol is a driver limit on one route and a +// resolver defect on another). A refused member is excluded for exactly one of these causes: +// the compiler's own frontier (attributed to its owning lane through the door ledger's +// known_frontier_causes at FATAL grain — this lane delegates to that table, it does not +// re-enumerate it); the driver's entry resolution (the declaration was not an evaluable arrow — +// a test data row or an absent name — a driver fact the door never faces, owned here); the +// hermetic effect boundary (the test reached a host effect the Eval interpretation does not +// bind); or NOTHING — an unattributed refusal, which is never silently bucketed: it blocks +// admission until someone owns it, the same fail-closed reading the door ledger gives an +// unowned head reason. +type NativeRouteExclusionCause + = CompilerFrontierAttributed { reason: Symbol } + | NativeEntryResolutionLimit { reason: Symbol } + | NativeEvalBoundary { reason: Symbol } + | UnattributedRefusal { reason: Symbol } + +// The driver's entry stage refuses for exactly these reasons today: the named declaration was +// not found in the prepared module, or it was found and is not an arrow (a test data row carries +// no body to evaluate). The roster is data so a driver widening deletes a row rather than +// editing a predicate; an entry-stage refusal with any other reason is unattributed, not +// assumed. +data native_route_entry_limit_reasons: List = [ + ^native_test_entry_declaration_not_found, + ^native_test_entry_declaration_not_arrow +] + +// The hermetic boundary's reason family: eval_default_interpretation binds no host effect, so a +// test that reaches one refuses with one of these. An Eval-stage refusal with any OTHER reason +// is not a boundary observation — it is an eval defect or a compiler frontier cause, and it is +// classified through the door ledger or left unattributed, never waved through as a boundary. +data native_route_effect_boundary_reasons: List = [ + ^eval_rejected_effect_io_unbound, + ^eval_rejected_effect_io_write_payload_mismatch, + ^eval_rejected_effect_io_roundtrip_arity +] + +fn native_route_reason_member(reason: Symbol, roster: List) -> Bool { + any(xs: roster, predicate: fn(r) { r == reason }) +} + +// THE CLASSIFICATION QUESTION IS ASKED AT FATAL GRAIN for the compiler-frontier arm: the fatal +// reason is the cause that actually refused, so its ownership row is the FatalGrain one. Asking +// the head-grain question of a fatal reason was the grain crossing the review named — the same +// symbol can be owned at one grain and unowned at the other, and only the fatal-grain answer +// classifies an exclusion. +fn classify_native_refusal(stage: NativeTestStage, reason: Symbol) -> NativeRouteExclusionCause { + match stage { + NativeTestStageEntry => + if native_route_reason_member(reason: reason, roster: native_route_entry_limit_reasons) { + NativeEntryResolutionLimit { reason: reason } + } else { + UnattributedRefusal { reason: reason } + } + NativeTestStageEval => + if native_route_reason_member(reason: reason, roster: native_route_effect_boundary_reasons) { + NativeEvalBoundary { reason: reason } + } else { + match cause_ownership_lookup(cause: reason, grain: FatalGrain) { + Present { value: _ } => CompilerFrontierAttributed { reason: reason } + Absent => UnattributedRefusal { reason: reason } + } + } + NativeTestStageContext => + match cause_ownership_lookup(cause: reason, grain: FatalGrain) { + Present { value: _ } => CompilerFrontierAttributed { reason: reason } + Absent => UnattributedRefusal { reason: reason } + } + NativeTestStagePrepare => + match cause_ownership_lookup(cause: reason, grain: FatalGrain) { + Present { value: _ } => CompilerFrontierAttributed { reason: reason } + Absent => UnattributedRefusal { reason: reason } + } + } +} + +// THE DISPOSITION OF ONE MEMBER: the (verdict, reference) pair, decided whole. A VERDICT the +// native route reached must AGREE with the reference — a pass where the floor expects red, a +// false where the floor expects pass, a non-Boolean return, or any reached verdict on a +// route-gap identity (the hermetic native route reaching what the hermetic floor route could not +// is a discrepancy between two routes that share an effect policy, and it reds) — and every +// disagreement is a DIVERGENCE, never an exclusion. A REFUSAL is a capability limit, not a +// verdict: it agrees only where the reference is the route gap and the refusal is the hermetic +// boundary itself (Eval stage, effect-boundary reason); every other refusal is an EXCLUSION, +// typed and counted by its classified cause. +type NativeRouteDivergenceCause + = NativePassedExpectedRed + | NativeFalseExpectedPass + | NativeReturnedOtherVerdict + | NativeReachedRouteGap + +type NativeRouteDisposition + = NativeAgreementPass + | NativeAgreementExpectedRed + | NativeAgreementRouteGap + | NativeExclusion { cause: NativeRouteExclusionCause } + | NativeDivergence { cause: NativeRouteDivergenceCause } + +fn native_route_disposition( + verdict: NativeTestVerdict, + reference: NativeRouteReference +) -> NativeRouteDisposition { + match verdict { + NativeTestPassed => + match reference { + FloorExpectedPass => NativeAgreementPass + FloorExpectedRed => NativeDivergence { cause: NativePassedExpectedRed } + FloorRouteGapRefusal => NativeDivergence { cause: NativeReachedRouteGap } + } + NativeTestReturnedFalse => + match reference { + FloorExpectedPass => NativeDivergence { cause: NativeFalseExpectedPass } + FloorExpectedRed => NativeAgreementExpectedRed + FloorRouteGapRefusal => NativeDivergence { cause: NativeReachedRouteGap } + } + NativeTestReturnedOther => NativeDivergence { cause: NativeReturnedOtherVerdict } + NativeTestRefused { stage: stage, reason: reason } => + match reference { + FloorRouteGapRefusal => + match stage { + NativeTestStageEval => + if native_route_reason_member(reason: reason, roster: native_route_effect_boundary_reasons) { + NativeAgreementRouteGap + } else { + NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + } + NativeTestStageContext => NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + NativeTestStagePrepare => NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + NativeTestStageEntry => NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + } + FloorExpectedPass => NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + FloorExpectedRed => NativeExclusion { cause: classify_native_refusal(stage: stage, reason: reason) } + } + } +} + +// THE COUNTED PARTITION the contract names ("every excluded member typed and counted"), extended +// with the agreement and divergence counts the pairing requires. The census is DERIVED from the +// rows, never accumulated beside them — a counter that disagrees with the population it +// summarizes has no constructor here. The rosters are folded in once (the caller derives them +// once) so the per-member reference question is a membership ask, not a re-derivation. +type NativeRouteCensus { + agreement_pass: Int + agreement_expected_red: Int + agreement_route_gap: Int + excluded_compiler_frontier: Int + excluded_entry_limit: Int + excluded_eval_boundary: Int + excluded_unattributed: Int + divergences: Int +} + +fn native_route_census( + rows: List, + expected_red: List, + route_gap: List +) -> NativeRouteCensus { + fold_list( + xs: rows, + empty: NativeRouteCensus { + agreement_pass: 0, + agreement_expected_red: 0, + agreement_route_gap: 0, + excluded_compiler_frontier: 0, + excluded_entry_limit: 0, + excluded_eval_boundary: 0, + excluded_unattributed: 0, + divergences: 0 + }, + cons: fn(acc, row) { + let reference = native_route_reference_for( + qualified: native_route_identity_qualified(identity: row.identity), + expected_red: expected_red, + route_gap: route_gap + ) + match native_route_disposition(verdict: row.verdict, reference: reference) { + NativeAgreementPass => NativeRouteCensus { + agreement_pass: acc.agreement_pass + 1, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + NativeAgreementExpectedRed => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red + 1, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + NativeAgreementRouteGap => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap + 1, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + NativeExclusion { cause: cause } => + match cause { + CompilerFrontierAttributed { reason: _ } => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier + 1, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + NativeEntryResolutionLimit { reason: _ } => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit + 1, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + NativeEvalBoundary { reason: _ } => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary + 1, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + } + UnattributedRefusal { reason: _ } => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed + 1, + divergences: acc.divergences + } + } + NativeDivergence { cause: _ } => NativeRouteCensus { + agreement_pass: acc.agreement_pass, + agreement_expected_red: acc.agreement_expected_red, + agreement_route_gap: acc.agreement_route_gap, + excluded_compiler_frontier: acc.excluded_compiler_frontier, + excluded_entry_limit: acc.excluded_entry_limit, + excluded_eval_boundary: acc.excluded_eval_boundary, + excluded_unattributed: acc.excluded_unattributed, + divergences: acc.divergences + 1 + } + } + } + ) +} + +// THE CONTROLS ARE OBSERVATIONS, NOT CONSTRUCTORS. Each control records what the emitted binary +// was OBSERVED to do; admission pattern-matches the observation, so a harness that cannot +// produce the observation cannot green the clause. +// +// THE FALSE AND TRUE CONTROLS PROVE THE VERDICT PATH IS LIVE. The lane's admission refuses +// unless the emitted binary observed the false control RETURN FALSE (a route that reports it +// Passed — or refuses it, or loses it — has a dead or diverted verdict arm, and every pass in +// the population is then worthless) and the true control RETURN TRUE (a route that falsifies or +// refuses everything cannot green either). Both controls live in v2.native_lane_fixture.control +// — outside the universe prefix, plain `fn` so floor discovery enrolls no rows for them — and +// the host names them to the binary explicitly, beside the derived universe. Absent is +// representable so a missing observation refuses by name rather than pattern-matching to a +// default. +// +// THE MALFORMED CONTROL PROVES THE FRONT-END REFUSAL ARM IS LIVE. The host materializes the +// committed poison specimen as a `.dag` under the lane's scratch root — the committed carrier +// is not a `.dag` file, because the changed-witness observation parses every changed `.dag` +// path in a diff and the deliberate tokenize failure would refuse it — and runs the same +// binary over that scratch root as the run's only source root. The run's single consumed +// output is the poison path's refusal, so rooting it at the full corpus would pay a second +// whole-corpus context fold for one observation. An honest front-end collects a per-file refusal +// for it (the collecting fold refuses the FILE, never the whole context), and the receipt records +// the refusal it observed. MalformedSpecimenAccepted — no refusal row for the poison path — means +// the route "accepted" poisoned source and every refusal verdict in its census is worthless. +type NativeRouteMalformedControl + = MalformedSpecimenRefused { path: String, reason: Symbol } + | MalformedSpecimenAccepted + +// THE NO-FALLBACK CONTROL IS A RECEIVED FACT, NOT AN ASSUMPTION. After the emitted compiler +// exists, the old compiler/interpreter route is made unavailable — or an executing falsifier is +// armed that fails if the old route is touched — and the receipt records WHICH control held and +// the identity of what was withdrawn or armed. OldRouteControlAbsent is representable so +// admission can refuse it by name: a receipt that cannot say how fallback was closed has not +// closed it. The empty-string arm is refused alongside the absent arm: a control whose identity +// names nothing is not an observation. +type NativeRouteOldRouteControl + = OldRouteWithdrawn { withdrawn_executable: String } + | OldRouteFalsifierArmed { falsifier_identity: String } + | OldRouteControlAbsent + +// THE TERMINAL OBSERVATION. Every identity the contract names is a field: the tested tree; the +// seed used ONLY to prepare the emitted compiler (recorded honestly — this job does not claim +// native bootstrap); the emitted closure's identity; the executed binary's path AND content +// identity (route identity is observed, never synthesized from the intended command); the +// DERIVED universe; the module → source-path relation over the universe's modules (the context +// reclassification's join key, carried so admission can re-ask the join, not trust it); the +// OBSERVED population; the per-file front-end refusals the collecting fold gathered; and every +// control. +type NativeRouteReceipt { + job: String + tested_tree: String + preparation_seed_identity: String + emitted_closure_identity: String + executable_path: String + executable_identity: String + universe: List + module_source_index: List + population: List + file_refusals: List + false_control: Optional + true_control: Optional + malformed_control: NativeRouteMalformedControl + old_route_control: NativeRouteOldRouteControl +} + +// ADMISSION IS THE CONTRACT, ONE AUTHORITY, ONE CLAUSE PER PREDICATE. The job greens only when +// all hold: the receipt is well-formed (right job, every identity recorded); the universe is +// derived and well-formed (non-empty, duplicate-free, prefix-closed); the observed population +// JOINS the universe exactly (no duplicates, no foreign rows, no omissions — "no silent +// remainder" as an executing check, not a comment); the positive population is non-empty (an +// all-refused census proves the route refused, not that it routed); every identity on the +// required-native ratchet roster is observed passing natively again (a regression of an already +// admitted native pass blocks, however its refusal would classify); no divergence is observed; +// every exclusion is classified; every file refusal is owned at both grains; every Context-stage +// refusal is backed by its own module's observed file refusal; and all four controls hold. The fold collects +// EVERY violated clause rather than stopping at the first, so one red run names everything that +// must change before the next run. +type NativeRouteAdmissionRefusalCause + = JobNameMismatch + | TestedTreeUnrecorded + | PreparationSeedUnrecorded + | EmittedClosureUnrecorded + | RouteIdentityUnrecorded + | UniverseUnderived + | UniverseDuplicatesPresent + | UniversePrefixViolationsPresent + | PopulationDuplicatesPresent + | PopulationForeignRowsPresent + | PopulationOmissionsPresent + | PositivePopulationEmpty + | RequiredNativePassRegressed + | DivergencesPresent + | UnattributedExclusionsPresent + | UnattributedFileRefusalsPresent + | UnattributedHeadAdvisoriesPresent + | ContextRefusalsUnbacked + | FalseControlNotObserved + | TrueControlNotObserved + | MalformedControlAcceptedByRoute + | OldRouteControlNeverInstalled + +type NativeRouteAdmission + = NativeRouteAdmitted + | NativeRouteRefused { causes: List } + +// ONE CLAUSE, ONE PREDICATE, ONE ROW. The check record couples a clause's verdict to the cause +// its failure appends, so the admission fold is one pass and the cause list can never drift from +// the predicate list. +type NativeRouteClauseCheck { + holds: Bool + cause: NativeRouteAdmissionRefusalCause +} + +fn native_route_job_name_matches(receipt: NativeRouteReceipt) -> Bool { + receipt.job == required_v2_native_job_name +} + +fn native_route_tested_tree_recorded(receipt: NativeRouteReceipt) -> Bool { + receipt.tested_tree != "" +} + +// The seed and the closure are DIFFERENT observations and refuse by DIFFERENT names: folding +// them into one predicate made EmittedClosureUnrecorded unreachable (a receipt with a recorded +// seed and an unrecorded closure reported preparation_seed_unrecorded), which is the +// grain-mismatch class DESIGN 4b(3) names. One predicate per observation. +fn native_route_preparation_seed_recorded(receipt: NativeRouteReceipt) -> Bool { + receipt.preparation_seed_identity != "" +} + +fn native_route_emitted_closure_recorded(receipt: NativeRouteReceipt) -> Bool { + receipt.emitted_closure_identity != "" +} + +fn native_route_identity_recorded(receipt: NativeRouteReceipt) -> Bool { + receipt.executable_path != "" && receipt.executable_identity != "" +} + +fn native_route_universe_derived(receipt: NativeRouteReceipt) -> Bool { + !is_empty(xs: receipt.universe) +} + +fn native_route_universe_unique(receipt: NativeRouteReceipt) -> Bool { + native_route_identities_unique(xs: receipt.universe) +} + +fn native_route_universe_prefix_closed(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.universe, predicate: fn(i) { + !native_route_universe_member(module_qn: i.module) + }) +} + +// The population's identity projection: the uniqueness and coverage questions range over +// identities, not rows, so the projection is named once beside the join that asks it. +fn native_route_row_identities(rows: List) -> List { + fold_list(xs: rows, empty: [], cons: fn(acc, row) { list_snoc_item(xs: acc, item: row.identity) }) +} + +fn native_route_population_unique(receipt: NativeRouteReceipt) -> Bool { + native_route_identities_unique( + xs: native_route_row_identities(rows: receipt.population) + ) +} + +// THE EXACT JOIN, BOTH DIRECTIONS. "No silent remainder" is two questions: every universe member +// carries exactly one observed row (no omissions), and every observed row names a universe +// member (no foreign rows — a cherry-picked or padded population refuses here). With uniqueness +// on both sides, the two directions together are set equality at identity grain. +fn native_route_population_covers_universe(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.universe, predicate: fn(member) { + !any(xs: receipt.population, predicate: fn(row) { + native_route_identity_eq(a: row.identity, b: member) + }) + }) +} + +fn native_route_population_no_foreign(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.population, predicate: fn(row) { + !native_route_identity_member(target: row.identity, xs: receipt.universe) + }) +} + +fn native_route_positive_population_present(census: NativeRouteCensus) -> Bool { + census.agreement_pass > 0 +} + +// THE REQUIRED-NATIVE RATCHET. The positive-population clause alone is an existential canary: +// a test that passes natively today could regress tomorrow to an already-known compiler refusal +// and disappear into the attributed-exclusion population while the lane stays green — coverage +// could collapse from hundreds of identities to one. This roster is the convergence ratchet +// that forbids the collapse: every identity ADMITTED as a native agreement-pass is enrolled +// here, and every subsequent head must observe it passing natively again. The frontier census +// still types every not-yet-native test as an exclusion; the distinction the roster draws is +// not yet admitted natively → typed exclusion allowed +// already admitted natively → a refusal is a regression and blocks +// Membership changes are explicit authority edits — a removal or redisposition is a reviewed +// row change beside its reason, never a silent shrink. A count floor would not do: one identity +// can replace another while capability is lost, so the join is at identity grain. +data native_route_required_pass_identities: List = [ + NativeRouteTestIdentity { + module: "v2.test.native_lane_smoke", + declaration: "native_route_positive_population_smoke" + } +] + +// One clause, asked of the observed population with the floor's rosters already derived: every +// rostered identity must be present with disposition NativeAgreementPass. The disposition fold +// is reused whole rather than re-asked as a raw verdict match, so a rostered identity whose +// reference expectation ever changes is judged by the same pairing every other member gets. +fn native_route_required_passes_hold( + rows: List, + expected_red: List, + route_gap: List +) -> Bool { + !any(xs: native_route_required_pass_identities, predicate: fn(required) { + !any(xs: rows, predicate: fn(row) { + native_route_identity_eq(a: row.identity, b: required) + && match native_route_disposition( + verdict: row.verdict, + reference: native_route_reference_for( + qualified: native_route_identity_qualified(identity: row.identity), + expected_red: expected_red, + route_gap: route_gap + ) + ) { + NativeAgreementPass => true + NativeAgreementExpectedRed => false + NativeAgreementRouteGap => false + NativeExclusion { cause: _ } => false + NativeDivergence { cause: _ } => false + } + }) + }) +} + +fn native_route_no_divergences(census: NativeRouteCensus) -> Bool { + census.divergences == 0 +} + +fn native_route_exclusions_all_classified(census: NativeRouteCensus) -> Bool { + census.excluded_unattributed == 0 +} + +// EVERY FILE REFUSAL IS OWNED AT FATAL GRAIN: the fatal reason is the cause that refused the +// file, so its ownership row is the FatalGrain one. An unowned fatal cause is a new front-end +// frontier fact surfacing where nobody counted it, and it blocks admission by its own name. +fn native_route_file_refusals_attributed(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.file_refusals, predicate: fn(fr) { + match cause_ownership_lookup(cause: fr.fatal_reason, grain: FatalGrain) { + Present { value: _ } => false + Absent => true + } + }) +} + +// THE HEAD REASON IS NOT DEAD RECEIPT DATA. rejected_with_pending prepends the non-fatal +// advisory, so the head reason is a real observation of the frontier at a different grain — the +// same observation the door ledger's own rows carry. Every file refusal's head reason must be +// owned at HEAD grain: an unowned advisory is a new frontier cause surfacing where nobody +// counted it, and it blocks admission by its own name rather than riding along unclassified. +fn native_route_head_advisories_attributed(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.file_refusals, predicate: fn(fr) { + match cause_ownership_lookup(cause: fr.head_reason, grain: HeadGrain) { + Present { value: _ } => false + Absent => true + } + }) +} + +// A CONTEXT-STAGE REFUSAL IS A DERIVED CLASSIFICATION, AND DERIVED CLASSIFICATIONS ARE FORGEABLE +// UNTIL BOUND TO THEIR OBSERVATION — AT SUBJECT GRAIN. The host reclassifies a member whose +// entry file never loaded as Context-stage with the file's fatal reason; the receipt therefore +// carries the module → source-path relation beside the file-refusal population, and every +// Context-stage row must join BOTH: its module's own source path (through module_source_index) +// AND an observed file refusal at exactly that path carrying exactly that fatal reason. A join +// on the reason alone is forgeable by any unrelated file sharing a common parser reason; a join +// on the module alone says nothing about which refusal backs it. The triple is the observation. +fn native_route_context_refusals_backed(receipt: NativeRouteReceipt) -> Bool { + !any(xs: receipt.population, predicate: fn(row) { + match row.verdict { + NativeTestRefused { stage: stage, reason: reason } => + match stage { + NativeTestStageContext => + !any(xs: receipt.module_source_index, predicate: fn(ms) { + ms.module == row.identity.module + && any(xs: receipt.file_refusals, predicate: fn(fr) { + fr.path == ms.path && fr.fatal_reason == reason + }) + }) + NativeTestStagePrepare => false + NativeTestStageEntry => false + NativeTestStageEval => false + } + NativeTestPassed => false + NativeTestReturnedFalse => false + NativeTestReturnedOther => false + } + }) +} + +fn native_route_false_control_holds(receipt: NativeRouteReceipt) -> Bool { + match receipt.false_control { + Present { value: v } => + match v { + NativeTestReturnedFalse => true + NativeTestPassed => false + NativeTestReturnedOther => false + NativeTestRefused { stage: _, reason: _ } => false + } + Absent => false + } +} + +fn native_route_true_control_holds(receipt: NativeRouteReceipt) -> Bool { + match receipt.true_control { + Present { value: v } => + match v { + NativeTestPassed => true + NativeTestReturnedFalse => false + NativeTestReturnedOther => false + NativeTestRefused { stage: _, reason: _ } => false + } + Absent => false + } +} + +fn native_route_malformed_control_holds(receipt: NativeRouteReceipt) -> Bool { + match receipt.malformed_control { + MalformedSpecimenRefused { path: path, reason: _ } => path != "" + MalformedSpecimenAccepted => false + } +} + +fn native_route_old_route_closed(receipt: NativeRouteReceipt) -> Bool { + match receipt.old_route_control { + OldRouteWithdrawn { withdrawn_executable: p } => p != "" + OldRouteFalsifierArmed { falsifier_identity: f } => f != "" + OldRouteControlAbsent => false + } +} + +fn native_route_admission(receipt: NativeRouteReceipt) -> NativeRouteAdmission { + let expected_red = floor_expected_red_roster() + let route_gap = floor_route_gap_roster() + let census = native_route_census( + rows: receipt.population, + expected_red: expected_red, + route_gap: route_gap + ) + let checks: List = [ + NativeRouteClauseCheck { holds: native_route_job_name_matches(receipt: receipt), cause: JobNameMismatch }, + NativeRouteClauseCheck { holds: native_route_tested_tree_recorded(receipt: receipt), cause: TestedTreeUnrecorded }, + NativeRouteClauseCheck { holds: native_route_preparation_seed_recorded(receipt: receipt), cause: PreparationSeedUnrecorded }, + NativeRouteClauseCheck { holds: native_route_emitted_closure_recorded(receipt: receipt), cause: EmittedClosureUnrecorded }, + NativeRouteClauseCheck { holds: native_route_identity_recorded(receipt: receipt), cause: RouteIdentityUnrecorded }, + NativeRouteClauseCheck { holds: native_route_universe_derived(receipt: receipt), cause: UniverseUnderived }, + NativeRouteClauseCheck { holds: native_route_universe_unique(receipt: receipt), cause: UniverseDuplicatesPresent }, + NativeRouteClauseCheck { holds: native_route_universe_prefix_closed(receipt: receipt), cause: UniversePrefixViolationsPresent }, + NativeRouteClauseCheck { holds: native_route_population_unique(receipt: receipt), cause: PopulationDuplicatesPresent }, + NativeRouteClauseCheck { holds: native_route_population_no_foreign(receipt: receipt), cause: PopulationForeignRowsPresent }, + NativeRouteClauseCheck { holds: native_route_population_covers_universe(receipt: receipt), cause: PopulationOmissionsPresent }, + NativeRouteClauseCheck { holds: native_route_positive_population_present(census: census), cause: PositivePopulationEmpty }, + NativeRouteClauseCheck { holds: native_route_required_passes_hold(rows: receipt.population, expected_red: expected_red, route_gap: route_gap), cause: RequiredNativePassRegressed }, + NativeRouteClauseCheck { holds: native_route_no_divergences(census: census), cause: DivergencesPresent }, + NativeRouteClauseCheck { holds: native_route_exclusions_all_classified(census: census), cause: UnattributedExclusionsPresent }, + NativeRouteClauseCheck { holds: native_route_file_refusals_attributed(receipt: receipt), cause: UnattributedFileRefusalsPresent }, + NativeRouteClauseCheck { holds: native_route_head_advisories_attributed(receipt: receipt), cause: UnattributedHeadAdvisoriesPresent }, + NativeRouteClauseCheck { holds: native_route_context_refusals_backed(receipt: receipt), cause: ContextRefusalsUnbacked }, + NativeRouteClauseCheck { holds: native_route_false_control_holds(receipt: receipt), cause: FalseControlNotObserved }, + NativeRouteClauseCheck { holds: native_route_true_control_holds(receipt: receipt), cause: TrueControlNotObserved }, + NativeRouteClauseCheck { holds: native_route_malformed_control_holds(receipt: receipt), cause: MalformedControlAcceptedByRoute }, + NativeRouteClauseCheck { holds: native_route_old_route_closed(receipt: receipt), cause: OldRouteControlNeverInstalled } + ] + let causes = fold_list( + xs: checks, + empty: [], + cons: fn(acc, check) { + if check.holds { acc } else { concat(acc, [check.cause]) } + } + ) + if is_empty(xs: causes) { NativeRouteAdmitted } else { NativeRouteRefused { causes: causes } } +} + +// The job's terminal print renders causes by name; the projection lives beside the coproduct, +// so a new cause refuses to compile until it is named (exhaustive match). The membership probe +// is how a witness — and the job's own report — asks for one clause's verdict without +// re-matching the whole coproduct. +fn native_route_admission_refusal_cause_name(cause: NativeRouteAdmissionRefusalCause) -> String { + match cause { + JobNameMismatch => "job_name_mismatch" + TestedTreeUnrecorded => "tested_tree_unrecorded" + PreparationSeedUnrecorded => "preparation_seed_unrecorded" + EmittedClosureUnrecorded => "emitted_closure_unrecorded" + RouteIdentityUnrecorded => "route_identity_unrecorded" + UniverseUnderived => "universe_underived" + UniverseDuplicatesPresent => "universe_duplicates_present" + UniversePrefixViolationsPresent => "universe_prefix_violations_present" + PopulationDuplicatesPresent => "population_duplicates_present" + PopulationForeignRowsPresent => "population_foreign_rows_present" + PopulationOmissionsPresent => "population_omissions_present" + PositivePopulationEmpty => "positive_population_empty" + RequiredNativePassRegressed => "required_native_pass_regressed" + DivergencesPresent => "divergences_present" + UnattributedExclusionsPresent => "unattributed_exclusions_present" + UnattributedFileRefusalsPresent => "unattributed_file_refusals_present" + UnattributedHeadAdvisoriesPresent => "unattributed_head_advisories_present" + ContextRefusalsUnbacked => "context_refusals_unbacked" + FalseControlNotObserved => "false_control_not_observed" + TrueControlNotObserved => "true_control_not_observed" + MalformedControlAcceptedByRoute => "malformed_control_accepted_by_route" + OldRouteControlNeverInstalled => "old_route_control_never_installed" + } +} + +fn native_route_admitted(admission: NativeRouteAdmission) -> Bool { + match admission { + NativeRouteAdmitted => true + NativeRouteRefused { causes: _ } => false + } +} + +fn native_route_admission_refused_with( + admission: NativeRouteAdmission, + cause_name: String +) -> Bool { + match admission { + NativeRouteAdmitted => false + NativeRouteRefused { causes: causes } => + any(xs: causes, predicate: fn(c) { + native_route_admission_refusal_cause_name(cause: c) == cause_name + }) + } +} + +// The terminal print of one run: every violated clause, one line's worth. Derived from the +// causes, never accumulated beside them. +fn native_route_admission_summary(admission: NativeRouteAdmission) -> String { + match admission { + NativeRouteAdmitted => "admitted" + NativeRouteRefused { causes: causes } => concat( + "refused: ", + fold_list( + xs: causes, + empty: "", + cons: fn(acc, c) { + let name = native_route_admission_refusal_cause_name(cause: c) + if acc == "" { name } else { concat(acc, ", ", name) } + } + ) + ) + } +} diff --git a/dag/gunbc/witness/witness_floor_workflow.dag b/dag/gunbc/witness/witness_floor_workflow.dag index a10403476f8..fea864cf13f 100644 --- a/dag/gunbc/witness/witness_floor_workflow.dag +++ b/dag/gunbc/witness/witness_floor_workflow.dag @@ -69,7 +69,8 @@ import gunbc.ci_spec { import gunbc.repo_identity { gunbc_default_branch_name } import gunbc.merge_admission_produce { ci_repo_root_shell } import gunbc.ci_layer_roots { witness_layer_roots } -import gunbc.fabric_witness_run { witnesses_lane_run_command, witnesses_lane_measurement_command, build_lane_run_command } +import gunbc.fabric_witness_run { witnesses_lane_run_command, witnesses_lane_measurement_command, build_lane_run_command, v2_native_lane_run_command } +import gunbc.witness_v2_native_route { required_v2_native_job_name } import extdeps.exec.command { ArgvCommand, shell_command_render } import gunbc.required_lanes_gate { RequiredLane, required_lanes_gate_stmts } import extdeps.github.expressions { ContextAccess, Github, Needs, interpolate } @@ -1675,6 +1676,105 @@ fn witness_floor_job() -> Job { } } +// THE NATIVE LANE JOB (operator authorization 2026-09-09: ONE additional required job, wired — +// not an enrolment promise). The three questions a new job must answer before it claims a paid +// runner on every push: +// +// THE MEASURED WALL. The lane pays: the shared prepared steps (toolchain + two-bin seed build), +// the v2 closure emission, one cargo build of the emitted compiler crate, the universe +// derivation fold (the floor's own discovery producer over the full module inventory), and the +// native run itself — one process over the derived v2.test.* universe plus the three controls. +// Measured 2026-09-10 on the 4-core cloud VM, after the lexer accumulation fix +// (v2.compiler.tokenize lexeme_grow): the native run dominates the envelope — ~70 minutes for +// the whole-corpus context fold plus the 3467-row eval, observed by the +// gunbc.witness_v2_native_route census — beside ~10 minutes of emission and ~10 minutes of +// cargo build. The envelope is 180 minutes, the floor lane's own value, so the merge queue's +// check_response_timeout (witness_floor_lane_timeout plus margin) still bounds it. +// +// WHAT RED DISCRIMINATES. The admission authority is gunbc.witness_v2_native_route: the observed +// population must join the derived universe exactly (no duplicates, no foreign rows, no +// omissions), the positive population must be non-empty, every refusal must be classified from +// stage and provenance, every native verdict must agree with the floor-derived reference at the +// same identity, and all four controls must hold — the false control observed false, the true +// control observed passed, the malformed specimen refused, the old route withdrawn. A step +// folded into an existing lane could not carry this: its red would land on that lane's receipt, +// conflating two routes' subjects. +// +// WHY A JOB AND NOT A STEP. The no-fallback control withdraws the old compiler/interpreter route +// after the emitted compiler exists; the floor lane's own execution IS the old route, so the +// control and the floor cannot share a job. The lane also builds a second crate (the emitted +// compiler), a capability envelope the existing lanes do not declare. A job is the smallest +// boundary that keeps both facts honest. +data v2_native_lane_timeout: Minute = minute(count: 180) + +fn v2_native_lane_build_script() -> String { + repo_self_build_command(bins: witness_floor_required_bins) +} + +fn v2_native_lane_run_script() -> String { + required_lane_run_script(command: v2_native_lane_run_command()) +} + +fn v2_native_lane_run_step() -> Step { + RunStep { + name: Present { value: "Required CI: v2-native lane (emitted-native compiler executes the derived v2.test.* universe)" }, + id: none, + run: v2_native_lane_run_script(), + shell: none, + env: none, + working_directory: none, + if_condition: Present { value: witness_floor_precondition() }, + continue_on_error: none, + timeout_minutes: none + } +} + +fn v2_native_lane_bound_steps() -> List { + append( + prepared_bound_steps(build_script: v2_native_lane_build_script()), + items: [ + WitnessFloorBoundStep { + step: witness_toolchain_homes_isolated_guard_step(), + role: capability_neutral, + step_name: "Require isolated toolchain homes", + }, + WitnessFloorBoundStep { + step: v2_native_lane_run_step(), + role: consumes_only(capabilities: [CargoCapability, RustcCapability, RustfmtCapability]), + step_name: "Required CI: v2-native lane (emitted-native compiler executes the derived v2.test.* universe)", + }, + WitnessFloorBoundStep { + step: witness_toolchain_filesystem_end_probe_step(), + role: capability_neutral, + step_name: "Record runner filesystem at job end", + }, + ], + ) +} + +fn v2_native_lane_steps() -> List { + list_map(xs: v2_native_lane_bound_steps(), f: fn(b) { b.step }) +} + +fn v2_native_lane_needs() -> List { [] } + +fn v2_native_lane_job() -> Job { + Job { + id: required_v2_native_job_name, + name: none, + runner: gunbc_ci_selected_runner_spec(), + steps: v2_native_lane_steps(), + needs: v2_native_lane_needs(), + env: none, + outputs: none, + if_condition: none, + timeout_minutes: minute_count(v2_native_lane_timeout), + continue_on_error: none, + concurrency: none, + permissions: none + } +} + // THE LANE'S EXECUTION ENVELOPE, NAMED BECAUSE A SECOND CONSUMER NOW READS IT. The value did not // change; it stopped being a literal. gunbc.repo_ruleset derives the merge queue's // check_response_timeout from this row, because a queue whose response timeout is BELOW the @@ -2279,6 +2379,7 @@ fn required_lanes_run_id_expression() -> String { // all. A `Rejected` fold can therefore never reach a published step. data required_lanes_gate_build_var: String = "BUILD" data required_lanes_gate_floor_var: String = "FLOOR" +data required_lanes_gate_v2_native_var: String = "V2_NATIVE" // THE REQUIRED LANE ROSTER, AND IT IS ONE ROSTER READ TWICE RATHER THAN TWO LISTS THAT AGREE. // @@ -2303,6 +2404,11 @@ fn required_lanes_roster() -> FreeSemigroup { job_id: floor_lane_job_id, label: "floor", var_name: required_lanes_gate_floor_var + }, + RequiredLane { + job_id: required_v2_native_job_name, + label: "v2-native", + var_name: required_lanes_gate_v2_native_var } ] } @@ -2536,12 +2642,12 @@ fn witness_floor_triggers() -> List { // THE AGGREGATE IS DELIBERATELY NOT A MEMBER. It checks out nothing and runs no witness: it reads // the other lanes' results. Every consumer of this list is asking about lanes that carry a subject, // and the aggregate joined that population only as something to filter back out. -type WitnessFloorLane = WitnessFloorBuildLane | WitnessFloorFloorLane | WitnessFloorHealLane +type WitnessFloorLane = WitnessFloorBuildLane | WitnessFloorFloorLane | WitnessFloorHealLane | WitnessFloorV2NativeLane fn witness_floor_lane_roster() -> FreeSemigroup { FreeSemigroup { head: WitnessFloorBuildLane, - tail: [WitnessFloorFloorLane, WitnessFloorHealLane] + tail: [WitnessFloorFloorLane, WitnessFloorHealLane, WitnessFloorV2NativeLane] } } @@ -2550,6 +2656,7 @@ fn witness_floor_lane_job(lane: WitnessFloorLane) -> Job { WitnessFloorBuildLane => build_lane_job() WitnessFloorFloorLane => witness_floor_job() WitnessFloorHealLane => heal_generated_artifacts_job() + WitnessFloorV2NativeLane => v2_native_lane_job() } } @@ -2568,6 +2675,7 @@ fn witness_floor_lane_job_id(lane: WitnessFloorLane) -> String { WitnessFloorBuildLane => build_lane_job_id WitnessFloorFloorLane => floor_lane_job_id WitnessFloorHealLane => heal_generated_artifacts_job_id + WitnessFloorV2NativeLane => required_v2_native_job_name } } diff --git a/dag/std/compiler_entry.dag b/dag/std/compiler_entry.dag index da56d492bf7..4957df5f373 100644 --- a/dag/std/compiler_entry.dag +++ b/dag/std/compiler_entry.dag @@ -15,9 +15,9 @@ module std.compiler_entry // // THE DRIVER SHAPE IS A SECOND FACT AND IS NOT DERIVABLE FROM THE FIRST. DESIGN section 3 separates // interface, realization and policy: "this module is the compiler pipeline entry" is the interface -// fact, and WHICH DRIVER REALIZES IT is realization. Two realizations exist in the tree today and -// both are load-bearing, so the sum below is closed at two rather than opened for futures nobody -// has. A third would have to be authored deliberately, which is the point of a closed vocabulary. +// fact, and WHICH DRIVER REALIZES IT is realization. Three realizations exist in the tree today and +// all are load-bearing, so the sum below is closed at three rather than opened for futures nobody +// has. A fourth would have to be authored deliberately, which is the point of a closed vocabulary. // // RetainedHostCliKernel -- the seed's own entry: a clap CLI whose compile verb dispatches into // hand-authored host Rust (v1_compiler.cli_run). Only a crate that CONTAINS that retained host @@ -28,7 +28,20 @@ module std.compiler_entry // // DirectIngestDriver -- a driver that reaches the emitted compiler's own ingest entry, taking // source text and a target and returning emitted text, with no retained host kernel between them. +// ONE SOURCE, NO PEERS, SYNTHETIC SUBJECT: it is a census instrument for the standalone native +// front door, not a repository execution route -- a module whose imports resolve only against +// sibling roots cannot pass through it. +// +// SourceRootEvalDriver -- the repository test-execution route. The rendered main reads a universe +// of qualified test identities (module path + declaration name, one per line) and the declared +// source roots, builds the SourceRootIngest itself (the filesystem read is the host boundary the +// driver owns), then hands the whole ingest to the pipeline: assemble, resolve, infer ONCE, and +// per universe member resolve the named declaration and Eval its body, reporting one typed +// verdict row per member. This is the driver the required-v2-native CI lane executes; its +// contract is that every universe member reaches exactly one terminal observation, never a +// silent remainder. type CompilerEntryDriver = RetainedHostCliKernel | DirectIngestDriver + | SourceRootEvalDriver diff --git a/dag/test/claim/e0599_emitter_decision_census_witness_test.dag b/dag/test/claim/e0599_emitter_decision_census_witness_test.dag index 6f5c7e980e6..21f9fb9bdd9 100644 --- a/dag/test/claim/e0599_emitter_decision_census_witness_test.dag +++ b/dag/test/claim/e0599_emitter_decision_census_witness_test.dag @@ -46,8 +46,8 @@ test fn e0599_b0_freemonoid_empty_test_is_target_api() -> Bool { } test fn e0599_b0_freemonoid_tail_iterate_is_target_api() -> Bool { - e0599_lowering_operation_label(operation: e0599_lowering_operation_for(method: "iter", receiver_expr: "(*__fm)")) == "FreeMonoidTailIterate" - && e0599_requirement_cause_label(cause: e0599_cause_for(method: "iter", receiver_expr: "(*__fm)")) == "TargetApiRequirement" + e0599_lowering_operation_label(operation: e0599_lowering_operation_for(method: "skip", receiver_expr: "__fm")) == "FreeMonoidTailIterate" + && e0599_requirement_cause_label(cause: e0599_cause_for(method: "skip", receiver_expr: "__fm")) == "TargetApiRequirement" } test fn e0599_b0_freemonoid_head_extract_is_owned_deconstruction() -> Bool { @@ -81,7 +81,7 @@ test fn e0599_b0_head_extract_and_empty_test_are_different_causes() -> Bool { test fn e0599_b0_target_api_is_not_ownership_removable() -> Bool { e0599_requirement_cause_label(cause: e0599_cause_for(method: "is_empty", receiver_expr: "__fm")) != "CloneSharedRequirement" - && e0599_requirement_cause_label(cause: e0599_cause_for(method: "iter", receiver_expr: "(*__fm)")) != "CloneSharedRequirement" + && e0599_requirement_cause_label(cause: e0599_cause_for(method: "skip", receiver_expr: "__fm")) != "CloneSharedRequirement" } test fn e0599_b0_owned_deconstruction_is_not_ownership_removable() -> Bool { diff --git a/dag/test/claim/witness_floor_workflow_consolidation_witness_test.dag b/dag/test/claim/witness_floor_workflow_consolidation_witness_test.dag index 3fc72261ca3..37df9718e0f 100644 --- a/dag/test/claim/witness_floor_workflow_consolidation_witness_test.dag +++ b/dag/test/claim/witness_floor_workflow_consolidation_witness_test.dag @@ -3,7 +3,8 @@ module test.claim.witness_floor_workflow_consolidation_witness_test import std.types { Bool, List, String } import v2.std.algebra { count_where, length, non_empty_to_list } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import gunbc.witness_floor_workflow { expected_witness_floor_yml, WitnessFloorGenerated, WitnessFloorGenerationRefused, build_lane_job_id, floor_lane_job_id, witness_floor_workflow_job_id, required_ci_measurement_publish_condition, required_ci_measurement_bound_steps, witness_floor_run_step, required_lanes_roster, witness_floor_lane_job_ids, build_lane_needs, witness_floor_lane_needs } +import gunbc.witness_floor_workflow { expected_witness_floor_yml, WitnessFloorGenerated, WitnessFloorGenerationRefused, build_lane_job_id, floor_lane_job_id, witness_floor_workflow_job_id, required_ci_measurement_publish_condition, required_ci_measurement_bound_steps, witness_floor_run_step, required_lanes_roster, witness_floor_lane_job_ids, build_lane_needs, witness_floor_lane_needs, v2_native_lane_needs } +import gunbc.witness_v2_native_route { required_v2_native_job_name } import gunbc.fabric_witness_run { required_ci_lane_build, required_ci_lane_witnesses } import gunbc.required_lanes_gate { required_lanes_gate_unrenderable_stmts } import gunbc.fleet_converge_workflow { fleet_converge_workflow } @@ -253,13 +254,20 @@ test fn w_RED_floor_measurement_publishes_before_separate_adjudication() -> Bool test fn w_RED_lane_contexts_collide_with_no_other_emitted_workflow() -> Bool { !any(fleet_converge_workflow.jobs, j => j.id == build_lane_job_id) && !any(fleet_converge_workflow.jobs, j => j.id == floor_lane_job_id) + && !any(fleet_converge_workflow.jobs, j => j.id == required_v2_native_job_name) && build_lane_job_id != floor_lane_job_id + && build_lane_job_id != required_v2_native_job_name && build_lane_job_id != witness_floor_workflow_job_id + && floor_lane_job_id != required_v2_native_job_name && floor_lane_job_id != witness_floor_workflow_job_id + && required_v2_native_job_name != witness_floor_workflow_job_id } -// NEITHER LANE WAITS ON THE OTHER, WHICH IS THE ENTIRE CONTENT OF "PARALLEL" -- and the -// aggregation job waits on both, which is what keeps the split fail-closed. +// NO LANE WAITS ON ANOTHER, WHICH IS THE ENTIRE CONTENT OF "PARALLEL" -- and the +// aggregation job waits on all of them, which is what keeps the split fail-closed. The +// roster was two lanes at the split (build, floor); the required v2-native lane's enrolment +// (gunbc.witness_v2_native_route) makes it three, and the witness name keeps the split's +// original identity rather than re-authoring the row. // // THE EARLIER FORM OF THIS ROW ASSERTED THAT THE FILE CONTAINED NO `needs:` AT ALL, and that // was the right claim for a workflow shape that was WRONG. Review of gunbc#9203 found it: the @@ -271,17 +279,18 @@ test fn w_RED_lane_contexts_collide_with_no_other_emitted_workflow() -> Bool { // // The claim is therefore stated at the grain that actually matters. The typed lane jobs must // carry no `needs`, while the one roster from which the aggregate derives its `needs` must carry -// exactly both lane identities. Reading those narrow producers avoids serializing the aggregate's +// exactly the three lane identities. Reading those narrow producers avoids serializing the aggregate's // large gate program merely to recover fields that existed before serialization. This remains a // strictly sharper discriminator than the old token count: it distinguishes the serialization // being forbidden from the aggregation being demanded, which "no needs anywhere" could not. // -// It is a RED that can be authored in both directions: giving either lane job a `needs` row +// It is a RED that can be authored in both directions: giving any lane job a `needs` row // fails the negatives, and dropping the aggregator's fails the positive. test fn w_RED_neither_lane_waits_on_the_other() -> Bool { length(xs: build_lane_needs()) == 0 && length(xs: witness_floor_lane_needs()) == 0 - && length(xs: non_empty_to_list(xs: required_lanes_roster())) == 2 + && length(xs: v2_native_lane_needs()) == 0 + && length(xs: non_empty_to_list(xs: required_lanes_roster())) == 3 && count_where( xs: non_empty_to_list(xs: required_lanes_roster()), predicate: fn(lane) { lane.job_id == build_lane_job_id }, @@ -290,6 +299,10 @@ test fn w_RED_neither_lane_waits_on_the_other() -> Bool { xs: non_empty_to_list(xs: required_lanes_roster()), predicate: fn(lane) { lane.job_id == floor_lane_job_id }, ) == 1 + && count_where( + xs: non_empty_to_list(xs: required_lanes_roster()), + predicate: fn(lane) { lane.job_id == required_v2_native_job_name }, + ) == 1 } // THE FABRIC LANE STILL RUNS AND NO LONGER GATES, AND THIS ROW ASSERTS BOTH HALVES. diff --git a/fixtures/native_lane_malformed/poison.dag.poisoned b/fixtures/native_lane_malformed/poison.dag.poisoned new file mode 100644 index 00000000000..1a1dae2b9fc --- /dev/null +++ b/fixtures/native_lane_malformed/poison.dag.poisoned @@ -0,0 +1,18 @@ +module v2.native_lane_malformed.poison + +// THE required-v2-native LANE'S MALFORMED-SOURCE CONTROL (authority: +// gunbc.witness_v2_native_route). This file is deliberately NOT well-formed: the string below +// never terminates, so any honest front-end refuses it at tokenize. The lane's host harness +// materializes these bytes as `poison.dag` under the lane's scratch root, runs the emitted +// native binary with that scratch directory as the only source root, and REQUIRES a per-file +// refusal — a native route that "accepts" a poisoned context has a dead front-end refusal arm, +// and every refusal verdict in its census is then worthless. +// +// THE COMMITTED CARRIER DOES NOT CARRY THE PLAIN `.dag` EXTENSION, because the bytes are not a +// dag program and the extension would claim otherwise to every repo-wide reader: the floor +// lane's changed-witness observation parses every changed `.dag` path in a diff, and this +// file's deliberate tokenize failure refused the whole observation (run 34471447387). It also +// lives outside dag/ and src/v2/ so the broken bytes never enter an honest ingest: the required +// parse sweep walks only the declared source roots, and the materialized copy joins a source +// root set only in the lane's own control invocation. +data broken: String = "this string never terminates diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 653586c9f91..70a317abaf4 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -489,8 +489,19 @@ fn rust_type_is_rc_wrapped(type_name: String) -> Bool { sharing_type_is_wrapped_for_target(target: Rust, type_name: type_name) } +// Rc::new(panic!(...)) is unreachable_code under -D warnings: panic diverges, so the +// constructor call never returns. Native cargo of the 00_compile closure refused on +// v2_std_collection wrapping the keyed-container inhabitant panic. A diverging inner +// is already a value of every type; wrapping it is not a sharing realization. fn rust_shared_wrap_ctor(inner_expr: String) -> String { - sharing_wrap_ctor_for_target(target: Rust, inner_expr: inner_expr) + if starts_with(s: inner_expr, prefix: "panic!(") + || starts_with(s: inner_expr, prefix: "compile_error!(") + || starts_with(s: inner_expr, prefix: "unreachable!(") + { + inner_expr + } else { + sharing_wrap_ctor_for_target(target: Rust, inner_expr: inner_expr) + } } fn rust_shared_wrap_ctor_prefix() -> String { @@ -8195,6 +8206,9 @@ fn find_shared_enum_fields(children: List, env: TypeEnv) -> List { // THE ACCESSOR RETURNS T BY VALUE FROM &self, so `__val.clone()` must be T::clone, not the // reference's; without `T: Clone` on the impl rustc clones the `&T` (E0308 expected `T`, found // `&T`, six generic carriers). The bound is on the impl the accessor lives in, not on the enum. +// A field whose type rides a recursion cycle is stored Box (rust_field_carrier_final_type, same +// needs_box_wrapping predicate), so the match binds &Box while the accessor owes the declared T +// -- the struct-field projection already carries this deref, and the enum arm must carry it too. fn emit_enum_shared_accessors(name: String, type_params: String, generic_param_names: List, children: List, recursive_types: Set, shared_types: Set, env: TypeEnv, emit_info: EmitGraphInfo) -> String { let all_shared = find_shared_enum_fields(children: children, env: env) if all_shared |> count == 0 { return "" } @@ -8232,7 +8246,16 @@ fn emit_enum_shared_accessors(name: String, type_params: String, generic_param_n } else if variant_is_synthetic_positional_payload(fields: child.children, source_indices: env.source_indices) { concat(" ", variant_path, "(_) => panic!(\"no ", fname, " on positional-payload variant\"),") } else { - concat(" ", variant_path, " { ", emit_ident(name: fname, target: Rust), ": __val, .. } => ", apply_type_template1(template: sharing.clone_value, arg0: "__val"), ",") + let arm_field_boxed = match child.children |> filter(f => authored_name(env: env, node: f) == fname) |> first { + Present { value: f } => needs_box_wrapping(n: resolved_type(n: f), recursive_types: recursive_types, shared_types: shared_types, source_indices: env.source_indices) + Absent => false + } + let clone_expr = if arm_field_boxed { + apply_type_template1(template: sharing.deref_clone, arg0: "*__val") + } else { + apply_type_template1(template: sharing.clone_value, arg0: "__val") + } + concat(" ", variant_path, " { ", emit_ident(name: fname, target: Rust), ": __val, .. } => ", clone_expr, ",") } ) let arms_str = arms |> join(separator: "\n") @@ -10156,6 +10179,32 @@ fn collect_pattern_rc_variant_guards(pattern: MatchPattern, rc_analysis: RcPatte } } +// A let-else over `.as_ref()` is irrefutable when the pattern is structurally irrefutable +// (wildcard/bind) OR when the variant is the enum's only inhabitant. Rust treats a +// single-variant coproduct match as irrefutable (`-D irrefutable-let-patterns`); the +// dag-level `match_pattern_is_irrefutable` is false for every VariantPattern, which is how +// `DiscoveredSourceRefsDigestUnavailableCause` (one variant) emitted +// `let Cause::NoRefList {..} = cause.as_ref() else { unreachable!() }` and refused the native +// cargo under RUSTFLAGS=-D warnings. +fn rust_as_ref_let_is_irrefutable(pattern: MatchPattern, emit_info: EmitGraphInfo) -> Bool { + if match_pattern_is_irrefutable(pattern: pattern) { true } + else { + match pattern { + VariantPattern { name: n, parent_enum: parent, field_bindings: _ } => + let resolved = pattern_parent_enum(name: qualified_last_segment(name: n), parent_enum: parent, scrut_type: "", type_summaries: emit_info.type_summaries) + match resolved { + Present { value: enum_name } => + match map_get(emit_info.type_summaries, enum_name) { + Present { value: summary } => (sorted_map_keys(summary.variant_name_set) |> count) <= 1 + Absent => false + } + Absent => false + } + _ => false + } + } +} + fn rc_pattern_preludes(pattern: MatchPattern, rc_analysis: RcPatternAnalysis, shared_types: Set, source_indices: Map, emit_info: EmitGraphInfo) -> String { match pattern { VariantPattern { name: n, parent_enum: parent_enum, field_bindings: fbs } => @@ -10184,12 +10233,17 @@ fn rc_pattern_preludes(pattern: MatchPattern, rc_analysis: RcPatternAnalysis, sh let inner_analysis = analyze_rc_pattern(pattern: fb_pat_here, scrut_type: "", shared_types: shared_types, emit_info: emit_info, source_indices: source_indices) if inner_analysis.needs_rc_pattern { let aware = emit_pattern_rc_aware(pattern: fb_pat_here, path_prefix: list_push(list_push([], bare_n), fb_name), rc_analysis: inner_analysis, shared_types: shared_types, scrut_type: "", source_indices: source_indices, emit_info: emit_info) - let head = concat("let ", aware, " = ", emit_ident(name: fb_name, target: Rust), ".as_ref() else { unreachable!() };") + let tail = if rust_as_ref_let_is_irrefutable(pattern: fb_pat_here, emit_info: emit_info) { + ";" + } else { + " else { unreachable!() };" + } + let head = concat("let ", aware, " = ", emit_ident(name: fb_name, target: Rust), ".as_ref()", tail) let inner_preludes = rc_pattern_preludes(pattern: fb_pat_here, rc_analysis: inner_analysis, shared_types: shared_types, source_indices: source_indices, emit_info: emit_info) if inner_preludes == "" { [head] } else { [concat(head, " ", inner_preludes)] } } else { let bound_str = emit_pattern(pattern: fb_pat_here, path_prefix: list_push(list_push([], bare_n), fb_name), shared_types: shared_types, scrut_type: "", source_indices: source_indices, emit_info: emit_info) - let tail = if match_pattern_is_irrefutable(pattern: fb_pat_here) { + let tail = if rust_as_ref_let_is_irrefutable(pattern: fb_pat_here, emit_info: emit_info) { ";" } else { " else { unreachable!() };" @@ -12552,8 +12606,16 @@ fn arms_are_freemonoid_coproduct(arms: List, scrut_type: String, type_summ is_fm && empty_fillable && cons_fillable } +// THE TAIL IS A STRUCTURAL SKIP, NEVER A COPY. `im::Vector::skip` is O(log n) — it shares the +// RRB tree — while `iter().skip(1).cloned().collect()` materializes the whole tail: every +// cons-match in every emitted program paid O(n) per step, so every fold over a FreeMonoid was +// quadratic. Measured on the required-v2-native lane's first run (2026-09-09): the self-hosted +// lexer, which tails the remaining source per character and per rule attempt, parsed a 22KB +// file in 23.3s against 70ms for 899B (333x time for 25x size), projecting a multi-hour +// whole-corpus context fold — the lane's dominant term, ahead of the cargo build the envelope +// had priced as dominant. DESIGN section 6 prices a copied accumulator as always-fixed. fn freemonoid_tail_let_from_fm(tail_bind: String) -> String { - if tail_bind == "_" { "" } else { concat("let ", tail_bind, ": Rc> = Rc::new((*__fm).iter().skip(1).cloned().collect()); ") } + if tail_bind == "_" { "" } else { concat("let ", tail_bind, ": Rc> = Rc::new(__fm.skip(1)); ") } } fn freemonoid_empty_branch_body(empty_arm: Node?, catchall: Node?, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { @@ -16354,6 +16416,8 @@ data compiler_entry_driver_type_name: String = "CompilerEntryDriver" data compiler_entry_retained_host_driver: String = "RetainedHostCliKernel" +data compiler_entry_source_root_eval_driver: String = "SourceRootEvalDriver" + type CompilerPipelineEntryDecl { module_name: String driver: String @@ -16411,6 +16475,13 @@ fn compiler_pipeline_entry_is_retained_host(modules: List) -> Bool } } +fn compiler_pipeline_entry_is_source_root_eval(modules: List) -> Bool { + match compiler_pipeline_entry_decl(modules: modules) { + Present { value: d } => d.driver == compiler_entry_source_root_eval_driver + Absent => false + } +} + fn compiler_pipeline_entry_is_ambiguous(modules: List) -> Bool { (compiler_pipeline_entry_decls(modules: modules) |> count) > 1 } @@ -16482,6 +16553,9 @@ fn emit_main_rs(workflow_funcs: List, modules: List, ) } if corpus_has_compiler_pipeline(modules: modules) && (retained_host_pipeline == false) { + if compiler_pipeline_entry_is_source_root_eval(modules: modules) { + return emit_source_root_eval_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) + } return emit_direct_ingest_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) } if workflow_funcs |> count == 0 && retained_host_pipeline == false { @@ -16576,6 +16650,201 @@ fn emit_direct_ingest_driver_main_rs(crate_name: String, pipeline_module: String } } +// THE SOURCE-ROOT EVAL DRIVER'S MAIN, AND THE CONTRACT ITS PROCESS MAKES WITH THE LANE HOST. +// +// THE UNIVERSE FILE IS THE WHOLE CONTRACT. One line per qualified test identity -- +// `moduledeclaration` -- derived by the lane host from the exact tested tree. This binary +// performs NO discovery and NO filtering: it executes exactly the identities it is handed, so +// the host-side join (expected universe == observed rows, no duplicates, no foreign rows) is a +// join between what the host derived and what this process actually produced. A binary that +// discovered its own population would make that join tautological. +// +// PROCESS EXIT IS LANE-LEVEL, NEVER TEST-LEVEL. Exit 0 plus the terminal marker row means every +// universe member reached exactly one observation row; a failing or refused TEST is data +// (NativeTestReturnedFalse / NativeTestRefused), not a process failure. Exit 1 is a whole-context +// refusal (the ingest fold rejected -- one bad source anywhere fails the lane, never a silent +// skip). Exit 2 is harness-level: bad argv, unreadable files, malformed or duplicate universe +// lines. A crash mid-population shows up host-side as a missing terminal marker or a row-count +// shortfall, which is why the marker carries the count. +// +// THE PREPARE-REFUSAL FAN-OUT keeps the no-silent-remainder invariant when a whole module fails +// resolve/infer: every declaration of that module gets its own Prepare-stage refusal row naming +// the same head reason, so the row count still equals the universe size and the refusal stays +// located per test identity. +// +// THE FILESYSTEM READ IS THE HOST BOUNDARY THE DRIVER OWNS (std.compiler_entry): the rendered +// main walks the declared source roots, reads the bytes, and mints one DagSourceReadWitness per +// path -- the .dag pipeline itself never touches IO. +fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: String) -> TextFile { + let pipeline_mod = module_to_filename(name: pipeline_module) + TextFile { + path: concat(rust_source_root(), "main", rust_source_ext()), + content: concat( + "// Generated by v1 compiler -- do not edit.\n\n", + "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]\n\n", + "use std::rc::Rc;\n\n", + "use ", crate_name, "::extdeps_communication_medium::\{DecodeFidelity, Medium\};\n", + "use ", crate_name, "::", pipeline_mod, "::{\n", + " native_test_context_from_ingest, native_test_eval_one, native_test_prepare_module,\n", + " NativeTestObservation, NativeTestStage, NativeTestVerdict,\n", + "};\n", + "use ", crate_name, "::v2_compiler_source_authority::\{source_root_for_storage_path, DagSourceReadWitness\};\n", + "use ", crate_name, "::v2_std_artifact::\{Artifact, ArtifactKind\};\n", + "use ", crate_name, "::v2_std_diagnostic::Outcome;\n", + "\n", + "fn collect_dag_paths(dir: &std::path::Path, out: &mut Vec) -> Result<(), String> {\n", + " let entries = match std::fs::read_dir(dir) {\n", + " Ok(entries) => entries,\n", + " Err(cause) => return Err(format!(\"could not walk source root {}: \{cause\}\", dir.display())),\n", + " };\n", + " for entry in entries {\n", + " let entry = match entry {\n", + " Ok(entry) => entry,\n", + " Err(cause) => return Err(format!(\"could not read entry under {}: \{cause\}\", dir.display())),\n", + " };\n", + " let path = entry.path();\n", + " if path.is_dir() {\n", + " collect_dag_paths(&path, out)?;\n", + " } else if path.extension().map(|ext| ext == \"dag\").unwrap_or(false) {\n", + " out.push(path.to_string_lossy().into_owned());\n", + " }\n", + " }\n", + " Ok(())\n", + "}\n", + "\n", + "fn main() {\n", + " let mut args = std::env::args().skip(1);\n", + " let universe_path = match args.next() {\n", + " Some(path) => path,\n", + " None => {\n", + " eprintln!(\"REFUSED: no universe file given -- usage: ", crate_name, " ...\");\n", + " std::process::exit(2);\n", + " }\n", + " };\n", + " let source_roots: Vec = args.collect();\n", + " if source_roots.is_empty() {\n", + " eprintln!(\"REFUSED: no source roots given -- usage: ", crate_name, " ...\");\n", + " std::process::exit(2);\n", + " }\n", + " let universe_text = match std::fs::read_to_string(&universe_path) {\n", + " Ok(text) => text,\n", + " Err(cause) => {\n", + " eprintln!(\"REFUSED: could not read universe file \{universe_path\}: \{cause\}\");\n", + " std::process::exit(2);\n", + " }\n", + " };\n", + " let mut universe: Vec<(Rc>, String)> = Vec::new();\n", + " let mut seen: std::collections::HashSet = std::collections::HashSet::new();\n", + " for line in universe_text.lines() {\n", + " if line.is_empty() || line.starts_with('#') {\n", + " continue;\n", + " }\n", + " match line.split_once('\\t') {\n", + " Some((module, declaration)) if !module.is_empty() && !declaration.is_empty() => {\n", + " if !seen.insert(line.to_string()) {\n", + " eprintln!(\"REFUSED: duplicate universe line: \{line\}\");\n", + " std::process::exit(2);\n", + " }\n", + " universe.push((\n", + " Rc::new(module.split('.').map(|segment| segment.to_string()).collect::>()),\n", + " declaration.to_string(),\n", + " ));\n", + " }\n", + " _ => {\n", + " eprintln!(\"REFUSED: malformed universe line (expected moduledeclaration): \{line\}\");\n", + " std::process::exit(2);\n", + " }\n", + " }\n", + " }\n", + " if universe.is_empty() {\n", + " eprintln!(\"REFUSED: universe file \{universe_path\} carried no test identities\");\n", + " std::process::exit(2);\n", + " }\n", + " let mut paths: Vec = Vec::new();\n", + " for root in &source_roots {\n", + " if let Err(cause) = collect_dag_paths(std::path::Path::new(root), &mut paths) {\n", + " eprintln!(\"REFUSED: \{cause\}\");\n", + " std::process::exit(2);\n", + " }\n", + " }\n", + " paths.sort();\n", + " let mut reads: Vec> = Vec::new();\n", + " for path in &paths {\n", + " let text = match std::fs::read_to_string(path) {\n", + " Ok(text) => text,\n", + " Err(cause) => {\n", + " eprintln!(\"REFUSED: could not read \{path\}: \{cause\}\");\n", + " std::process::exit(2);\n", + " }\n", + " };\n", + " reads.push(Rc::new(DagSourceReadWitness {\n", + " source: Rc::new(Medium {\n", + " carried: text,\n", + " fidelity: DecodeFidelity::Lossless,\n", + " _phantom: std::marker::PhantomData,\n", + " }),\n", + " artifact: Rc::new(Artifact {\n", + " kind: ArtifactKind::SourceFile,\n", + " id: path.clone(),\n", + " file_path: path.clone(),\n", + " }),\n", + " compilation_unit: path.clone(),\n", + " source_root: source_root_for_storage_path(path.clone()),\n", + " }));\n", + " }\n", + " let context = match &*native_test_context_from_ingest(Rc::new(reads.into())) {\n", + " Outcome::Accepted { value, .. } => value.clone(),\n", + " Outcome::Rejected { diagnostics } => {\n", + " eprintln!(\"CONTEXT-REFUSED: \{diagnostics:#?\}\");\n", + " std::process::exit(1);\n", + " }\n", + " };\n", + " for refusal in context.file_refusals.iter() {\n", + " println!(\"{}\", serde_json::json!(\{ \"file_refusal\": refusal \}));\n", + " }\n", + " let mut module_order: Vec<(String, Rc>)> = Vec::new();\n", + " let mut module_decls: std::collections::HashMap> = std::collections::HashMap::new();\n", + " for (qn, declaration) in &universe {\n", + " let dotted = qn.iter().cloned().collect::>().join(\".\");\n", + " if !module_decls.contains_key(&dotted) {\n", + " module_order.push((dotted.clone(), qn.clone()));\n", + " module_decls.insert(dotted.clone(), Vec::new());\n", + " }\n", + " module_decls.get_mut(&dotted).unwrap().push(declaration.clone());\n", + " }\n", + " let mut rows: u64 = 0;\n", + " for (dotted, qn) in &module_order {\n", + " let declarations = &module_decls[dotted];\n", + " match &*native_test_prepare_module(context.clone(), qn.clone()) {\n", + " Outcome::Accepted { value: prepared, .. } => {\n", + " for declaration in declarations {\n", + " let observation = native_test_eval_one(prepared.clone(), declaration.clone());\n", + " println!(\"{}\", serde_json::to_string(&observation).unwrap());\n", + " rows += 1;\n", + " }\n", + " }\n", + " Outcome::Rejected { diagnostics } => {\n", + " let reason = diagnostics.tail.last().map(|d| d.reason.clone()).unwrap_or_else(|| diagnostics.head.reason.clone());\n", + " for declaration in declarations {\n", + " let observation = NativeTestObservation {\n", + " module: qn.clone(),\n", + " declaration: declaration.clone(),\n", + " verdict: Rc::new(NativeTestVerdict::NativeTestRefused {\n", + " stage: NativeTestStage::NativeTestStagePrepare,\n", + " reason: reason.clone(),\n", + " }),\n", + " };\n", + " println!(\"{}\", serde_json::to_string(&observation).unwrap());\n", + " rows += 1;\n", + " }\n", + " }\n", + " }\n", + " }\n", + " println!(\"{}\", serde_json::json!(\{ \"_terminal\": \"complete\", \"rows\": rows, \"file_refusals\": context.file_refusals.len() \}));\n", + "}\n") + } +} + // THE ENTRY POINT A PIPELINE-FREE CORPUS GETS, AND WHY IT REFUSES RATHER THAN RETURNING. // // This arm used to emit `fn main() {}`. That crate builds, runs, exits zero and does nothing -- diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 72ee804b9d8..2ef3095421a 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -565,6 +565,33 @@ fn run() -> Result { } } + // THE (PHASE, LANE) PAIR JOIN RUNS IN EVERY LANE, BEFORE ANY PHASE. The variant-set + // join rides the parse phase's index, but a match arm is not a declaration, so lane + // ownership is joined by evaluating the roster authority's + // `required_ci_lane_phase_rows`. Three lanes are independently selected and the native + // route's isolation is load-bearing: a host edit mapping a phase to the wrong lane + // (leaving the native job selecting zero phases), or a selected lane owning zero + // phases in the authority, stops the line here rather than greening over an + // unmeasured population. + let authority_lane_rows = + match v1_compiler::cli_run::authority_lane_phase_rows(&source_roots) { + Ok(rows) => Some(rows), + Err(e) => { + eprintln!("required-ci: lane-roster FAIL {e}"); + phase_failures.push(format!("lane-roster: {e}")); + None + } + }; + if let Some(rows) = &authority_lane_rows { + let findings = lane_roster_findings(rows, required_ci_lane); + for finding in &findings { + eprintln!("required-ci: lane-roster FAIL {finding}"); + } + if !findings.is_empty() { + phase_failures.push(format!("lane-roster ({} finding(s))", findings.len())); + } + } + // PHASE 1 — the .dag parse sweep, over every authored root (src/v1, dag, src/v2). // Independent of everything below it. The roster is // `cli_run::DAG_PARSE_SWEEP_ROOTS`, shared with the standalone bin so the cheapest @@ -1054,6 +1081,44 @@ fn run() -> Result { ran.push("floor"); } + // THE NATIVE ROUTE PHASE. The harness derives the v2.test.* universe with the floor's + // own discovery producer, prepares the emitted-native compiler through the emit-compile + // machinery, runs the universe plus controls through that binary by explicit path with + // the old route withdrawn, and hands the minted receipt to the admission authority — + // the phase greens exactly when gunbc.witness_v2_native_route admits it. + if required_ci_phase_selected(RequiredCiPhase::V2Native, required_ci_lane) { + eprintln!( + "required-ci: phase v2-native (emitted-native compiler executes the derived v2.test.* universe)" + ); + match v1_compiler::cli_run::run_required_v2_native(&source_roots) { + Ok(()) => {} + Err(e) => { + eprintln!("required-ci: v2-native refused: {e}"); + phase_failures.push(format!("v2-native refused: {e}")); + } + } + ran.push("v2-native"); + } + + // THE OBSERVED RAN SET IS THE AUTHORITY-EXPECTED SET, EXACTLY. The census below prints + // phases_run; this refusal covers the state where that number is legible but wrong — a + // phase silently never reached, or one executed that the authority does not expect in + // this lane. Equality over an empty expected set cannot mask a zero-phase run: the + // empty-expectation case already refused at run start. + if let Some(rows) = &authority_lane_rows { + let expected = expected_lane_phases(rows, required_ci_lane); + let expected_refs: std::collections::BTreeSet<&str> = + expected.iter().map(String::as_str).collect(); + let observed: std::collections::BTreeSet<&str> = ran.iter().copied().collect(); + if observed != expected_refs { + phase_failures.push(format!( + "lane-roster ran-set: the authority expects {:?} in this lane but the run \ + executed {:?}", + expected_refs, observed + )); + } + } + // COUNTER-KEY CENSUS (dashboard node adhoc-af8a3fe8-13d): this is the // phase population. The required-floor aggregate below counts claim outcomes, so the // formerly shared `failed` key gave one spelling two meanings in one process's output. @@ -1549,6 +1614,7 @@ fn emit_worker_terminal_before_return(code: ExitCode) -> ExitCode { enum RequiredCiLane { Build, Witnesses, + V2Native, } impl RequiredCiLane { @@ -1556,6 +1622,7 @@ impl RequiredCiLane { match self { RequiredCiLane::Build => "build", RequiredCiLane::Witnesses => "witnesses", + RequiredCiLane::V2Native => "v2-native", } } @@ -1566,8 +1633,9 @@ impl RequiredCiLane { match value { "build" => Ok(RequiredCiLane::Build), "witnesses" => Ok(RequiredCiLane::Witnesses), + "v2-native" => Ok(RequiredCiLane::V2Native), other => Err(format!( - "unknown --required-lane: {other} (expected build | witnesses)" + "unknown --required-lane: {other} (expected build | witnesses | v2-native)" )), } } @@ -1580,6 +1648,7 @@ enum RequiredCiPhase { GeneratedArtifact, RegenFixedPoint, Floor, + V2Native, } impl RequiredCiPhase { @@ -1590,6 +1659,7 @@ impl RequiredCiPhase { RequiredCiPhase::RegenFixedPoint => "regen-fixed-point", RequiredCiPhase::GeneratedArtifact => "generated-artifact", RequiredCiPhase::Floor => "floor", + RequiredCiPhase::V2Native => "v2-native", } } @@ -1615,21 +1685,29 @@ impl RequiredCiPhase { // measurement to reference and nothing to compare. RequiredCiPhase::RegenFixedPoint => RequiredCiLane::Build, RequiredCiPhase::Floor => RequiredCiLane::Witnesses, + // THE NATIVE ROUTE IS ITS OWN LANE, NOT A FLOOR RIDER: the lane's no-fallback + // control withdraws the old compiler/interpreter route's CLI for the spawn window, + // and the floor lane's own execution IS that route -- the two cannot share a job + // (lane authority: gunbc.witness_v2_native_route). + RequiredCiPhase::V2Native => RequiredCiLane::V2Native, } } } -// THE REQUIRED GATE IS FIVE PHASES. Four are the 2026-08-29 compiler-floor bankruptcy roster; +// THE REQUIRED GATE IS SIX PHASES. Four are the 2026-08-29 compiler-floor bankruptcy roster; // generated-artifact returned after its declared exposure produced a real stale projection on // main, and now also owns the former regen phase's stage0-mirror population. Keeping two phase // identities would preserve the independently-green outcomes this composition removes. The other -// three removed phases remain outside required CI and inside the declared drop. -const REQUIRED_CI_PHASES: [RequiredCiPhase; 5] = [ +// three removed phases remain outside required CI and inside the declared drop. The sixth, +// v2-native, is the 2026-09-09 operator-authorized native-route lane: the emitted-native +// compiler executes the derived v2.test.* universe (gunbc.witness_v2_native_route). +const REQUIRED_CI_PHASES: [RequiredCiPhase; 6] = [ RequiredCiPhase::Parse, RequiredCiPhase::NamespaceWaveAdmission, RequiredCiPhase::GeneratedArtifact, RequiredCiPhase::RegenFixedPoint, RequiredCiPhase::Floor, + RequiredCiPhase::V2Native, ]; /// The `.dag` coproduct this enum realizes, and the declaration whose variants it must equal. @@ -1641,12 +1719,13 @@ const PHASE_ROSTER_AUTHORITY_MODULE: &str = "gunbc.required_ci_phase_roster"; const PHASE_ROSTER_AUTHORITY_DECL: &str = "RequiredCiPhase"; /// Every phase this binary realizes, in the authority's own variant spelling. -const PHASE_ROSTER_VARIANT_LABELS: [&str; 5] = [ +const PHASE_ROSTER_VARIANT_LABELS: [&str; 6] = [ "ParsePhase", "NamespaceWaveAdmissionPhase", "GeneratedArtifactPhase", "RegenFixedPointPhase", "FloorPhase", + "V2NativePhase", ]; /// Refuse if the host phase enum and the `.dag` phase roster disagree — the same both-directions @@ -1655,10 +1734,11 @@ const PHASE_ROSTER_VARIANT_LABELS: [&str; 5] = [ /// nothing else joins these two. An absent authority module refuses too — that is the state in /// which nothing is checking the roster, not permission to proceed. /// -/// WHAT THIS JOIN DELIBERATELY DOES NOT COVER: lane ownership. The host `lane` match is not -/// readable from the declaration index, and a lane divergence cannot un-enrol a phase — both -/// lanes execute in every required run — so the residue is bounded to which job carries a phase, -/// and its terminal is the atomic deletion the roster's census row names. +/// WHAT THIS JOIN DOES NOT COVER: lane ownership. A match arm is not a declaration, so the +/// index this join reads cannot see which lane owns a phase. That half is joined by evaluation +/// instead — `authority_lane_phase_rows` against this host's enum-plus-lane-match, executed at +/// run start in EVERY lane (three lanes are independently selected now, and the native route's +/// isolation is load-bearing), with the empty-expectation and exact-ran-set refusals beside it. fn phase_roster_findings( index: &v1_compiler::cli_run::declaration_index::DeclarationIndex, ) -> Vec { @@ -1696,6 +1776,61 @@ fn phase_roster_findings( findings } +/// The authority-expected phase names for the selected lane — the whole roster when no lane was +/// selected (a lane-less run owns every phase). +fn expected_lane_phases( + rows: &[v1_compiler::cli_run::LanePhaseRow], + selected: Option, +) -> std::collections::BTreeSet { + rows.iter() + .filter(|r| selected.is_none_or(|l| r.lane == l.name())) + .map(|r| r.phase.clone()) + .collect() +} + +/// The (phase, lane) pair join, both directions, plus the empty-expectation refusal. The pair +/// join is what makes a host lane-match edit red: moving a phase between lanes changes the host +/// pair set while the authority's stays, and the difference names the divergence. The empty +/// refusal is what makes a zero-phase selected lane stop: `phases_run=0` can no longer render +/// as a successful required job. +fn lane_roster_findings( + rows: &[v1_compiler::cli_run::LanePhaseRow], + selected: Option, +) -> Vec { + use std::collections::BTreeSet; + let host: BTreeSet<(String, String)> = REQUIRED_CI_PHASES + .iter() + .map(|p| (p.lane().name().to_string(), p.name().to_string())) + .collect(); + let authority: BTreeSet<(String, String)> = rows + .iter() + .map(|r| (r.lane.clone(), r.phase.clone())) + .collect(); + let mut findings = Vec::new(); + for (lane, phase) in authority.difference(&host) { + findings.push(format!( + "the authority rosters phase `{phase}` in lane `{lane}` and this host does not \ + realize that pair — lane ownership diverged" + )); + } + for (lane, phase) in host.difference(&authority) { + findings.push(format!( + "this host runs phase `{phase}` in lane `{lane}` and the authority does not roster \ + that pair — lane ownership diverged" + )); + } + if expected_lane_phases(rows, selected).is_empty() { + findings.push(format!( + "the selected lane `{}` owns no phases in the authority roster — the run would \ + report success over zero phases", + selected + .map(|l| l.name()) + .unwrap_or("all (no --required-lane given)") + )); + } + findings +} + /// EVERY PHASE IS ACCOUNTED FOR IN EVERY RUN, whether or not this lane owns it. /// /// A lane that simply omitted the other lane's phases would produce a ledger in which "this @@ -2166,6 +2301,82 @@ fn main() -> ExitCode { mod tests { use super::*; + fn lane_phase_row(lane: &str, phase: &str) -> v1_compiler::cli_run::LanePhaseRow { + v1_compiler::cli_run::LanePhaseRow { + lane: lane.to_string(), + phase: phase.to_string(), + } + } + + /// The pairs `required_ci_lane_phase_rows` derives from the standing roster. + fn standing_authority_rows() -> Vec { + vec![ + lane_phase_row("witnesses", "parse"), + lane_phase_row("witnesses", "namespace-wave-admission"), + lane_phase_row("build", "generated-artifact"), + lane_phase_row("build", "regen-fixed-point"), + lane_phase_row("witnesses", "floor"), + lane_phase_row("v2-native", "v2-native"), + ] + } + + #[test] + fn the_standing_pairs_join_cleanly_in_every_lane() { + for lane in [ + RequiredCiLane::Build, + RequiredCiLane::Witnesses, + RequiredCiLane::V2Native, + ] { + assert!( + lane_roster_findings(&standing_authority_rows(), Some(lane)).is_empty(), + "lane {} must join cleanly", + lane.name() + ); + } + assert!(lane_roster_findings(&standing_authority_rows(), None).is_empty()); + } + + #[test] + fn a_lane_ownership_divergence_is_red_and_names_the_pair() { + // The review's scenario — the host mapping and the authority mapping disagree about + // which lane owns v2-native — simulated by moving the authority row; the pair + // difference is symmetric, so either side's edit fires both directions. + let mut rows = standing_authority_rows(); + rows.retain(|r| r.phase != "v2-native"); + rows.push(lane_phase_row("build", "v2-native")); + let findings = lane_roster_findings(&rows, Some(RequiredCiLane::V2Native)); + assert!( + findings + .iter() + .any(|f| f.contains("v2-native") && f.contains("build")), + "the divergence must name the pair: {findings:?}" + ); + assert!( + findings.iter().any(|f| f.contains("owns no phases")), + "the native lane's expectation is now empty and must refuse: {findings:?}" + ); + } + + #[test] + fn a_selected_lane_with_no_expected_phases_is_refused() { + let rows: Vec<_> = standing_authority_rows() + .into_iter() + .filter(|r| r.phase != "v2-native") + .collect(); + let findings = lane_roster_findings(&rows, Some(RequiredCiLane::V2Native)); + assert!(findings.iter().any(|f| f.contains("owns no phases"))); + } + + #[test] + fn the_expected_set_tracks_the_selected_lane() { + let rows = standing_authority_rows(); + assert_eq!( + expected_lane_phases(&rows, Some(RequiredCiLane::V2Native)), + ["v2-native".to_string()].into_iter().collect() + ); + assert_eq!(expected_lane_phases(&rows, None).len(), 6); + } + #[test] fn a_returned_subject_refusal_is_a_completed_measurement() { let blocker = RequiredCiBlocker { diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 05c5c3be423..f858c585bfd 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -89,14 +89,18 @@ mod census_heads; #[path = "declaration_index.rs"] pub mod declaration_index; pub mod derived_row_roster; +mod native_lane_runner; mod required_floor_runner; +mod required_lane_roster; pub mod rostered_row_join; mod serve_budget_refusal; +pub use native_lane_runner::run_required_v2_native; pub(crate) use required_floor_runner::*; pub use required_floor_runner::{ floor_discovery_path_excluded, make_eval_context, make_eval_context_with_runtime_options, run_claim_measured, run_required_floor, }; +pub use required_lane_roster::{authority_lane_phase_rows, LanePhaseRow}; mod entry_resolve; pub(crate) use active_workset::*; pub(crate) use entry_resolve::*; @@ -42368,10 +42372,10 @@ pub use emitted_closure_compile_host::{ cargo_verdict_stderr_tail, emit_compile_modules_reached, emit_compile_outcome_passed, emit_compile_outcome_summary, emit_compile_report, emit_compile_selection, emit_compile_selection_not_selected_digest, emit_compile_selection_selected_digest, - emit_compile_selection_universe_digest, local_emit_compile_probe_root, - required_ci_emit_compile_probe_root, required_emit_compile_entries, - retain_not_selected_identities, run_required_emit_compile, CargoVerdict, EmitCompileOutcome, - EmitCompileSelection, MutationVerdict, + emit_compile_selection_universe_digest, lane_emit_compile_probe_root, + local_emit_compile_probe_root, required_ci_emit_compile_probe_root, + required_emit_compile_entries, retain_not_selected_identities, run_required_emit_compile, + CargoVerdict, EmitCompileOutcome, EmitCompileSelection, MutationVerdict, }; /// THE FIXTURE ROUTE IS TEST-FACING ONLY, AND THAT IS WHY IT HAS ITS OWN `use` RATHER THAN A LINE diff --git a/src/v1/stage0/src/cli_run/native_lane_runner.rs b/src/v1/stage0/src/cli_run/native_lane_runner.rs new file mode 100644 index 00000000000..ed2a0dc7de9 --- /dev/null +++ b/src/v1/stage0/src/cli_run/native_lane_runner.rs @@ -0,0 +1,1140 @@ +//! The required-v2-native lane's host harness (lane authority: +//! `gunbc.witness_v2_native_route`; roster row: `gunbc.required_ci_phase_roster` +//! `V2NativePhase`). +//! +//! THE SUBJECT IS THE ROUTE, NOT THE MODEL. The `.dag` authority owns the admission contract; +//! this module is the harness that MINTS the receipt the contract admits or refuses: it derives +//! the `v2.test.*` universe with the floor's own discovery producer, prepares the emitted-native +//! compiler through the emit-compile phase's one crate writer, invokes that binary by explicit +//! path over the universe plus the controls, withdraws the old-route CLI for the duration of +//! every native spawn, and binds the observed verdicts into a `NativeRouteReceipt` that is then +//! admitted — or refused — by evaluating `native_route_admission` itself. Nothing here decides +//! admission in Rust: the host observes, the substrate judges. +//! +//! WHAT THE OLD-ROUTE CONTROL PROVES HERE, AND WHAT IT DOES NOT. The lane's verdicts are parsed +//! from the spawned emitted binary's stdout, so the execution route is a process boundary, not a +//! call convention. Withdrawing the seed's `gunbc` binary for the spawn window proves no +//! subprocess fallback to the seed CLI occurred; it does not prove this process never +//! interpreted a test — that discipline is the code's own (the interpreter contexts below +//! evaluate ONLY the discovery producer and the admission authority, never a universe test), +//! and the receipt's route identity (binary path + content hash) is what admission checks. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::rc::Rc; + +use crate::v1_interpreter::{self, str_value, Value}; + +/// The compiler entry whose closure becomes the lane's emitted-native compiler. Its +/// `compiler_pipeline_entry` is `SourceRootEvalDriver`, so the emitted crate's `main.rs` is the +/// whole-source-root Eval driver this lane exists to route through. +const NATIVE_COMPILE_ENTRY: &str = "src/v2/compiler/00_compile.dag"; + +/// The universe prefix — the same `v2.test.` the required floor gates on. The derivation below +/// filters the floor discovery producer's rows to it; the admission authority independently +/// checks prefix closure, so a drift between the two is a red, not a silent widening. +const UNIVERSE_PREFIX: &str = "v2.test."; + +/// The live-verdict control pair (v2.native_lane_fixture.control): plain `fn` declarations +/// outside the universe prefix, so floor discovery enrolls no rows for them and this harness +/// names them to the binary explicitly, beside the derived universe. +const CONTROL_MODULE: &str = "v2.native_lane_fixture.control"; +const FALSE_CONTROL_DECL: &str = "native_lane_false_control"; +const TRUE_CONTROL_DECL: &str = "native_lane_true_control"; + +/// The malformed control: deliberately unterminating bytes any honest front-end must refuse at +/// tokenize. THE COMMITTED CARRIER IS NOT A `.dag` FILE — the bytes are not a dag program, and +/// carrying the extension put them in the changed-witness observation's parse path, where the +/// deliberate tokenize failure refused the whole floor lane (run 34471447387). The harness +/// materializes the specimen as `poison.dag` under the lane's scratch root, and only that +/// materialized copy ever enters an ingest — the control run's own source root. +const MALFORMED_SPECIMEN_COMMITTED: &str = "fixtures/native_lane_malformed/poison.dag.poisoned"; +/// The control run's scratch source root and the materialized specimen's path under it, +/// workspace-relative like every path this harness hands the binary (its working directory is +/// the workspace root). The receipt records the observed refusal path; the admission authority +/// requires it non-empty, and this harness requires it to name the materialized specimen. +const MALFORMED_CONTROL_ROOT: &str = "target/v2-native-lane/malformed-control-root"; +const MALFORMED_MATERIALIZED_PATH: &str = "target/v2-native-lane/malformed-control-root/poison.dag"; + +/// The admission authority's entry file, for the interpreter context that judges the receipt. +const NATIVE_ROUTE_AUTHORITY_ENTRY: &str = "dag/gunbc/witness/v2_native_route.dag"; + +/// One observed verdict row, decoded from the emitted binary's stdout at the same grain the +/// receipt carries: qualified identity plus the driver's own verdict vocabulary. +struct NativeObservation { + module: String, + declaration: String, + verdict: NativeVerdictObserved, +} + +enum NativeVerdictObserved { + Passed, + ReturnedFalse, + ReturnedOther, + Refused { stage: String, reason: String }, +} + +/// One per-file front-end refusal the emitted binary's collecting context fold observed. +struct NativeFileRefusalObserved { + path: String, + head_reason: String, + fatal_reason: String, +} + +/// The emitted compiler, prepared: where the binary is, what its bytes are, and the identity of +/// the closure it was emitted from. +struct EmittedPreparation { + binary_path: PathBuf, + binary_identity: String, + closure_identity: String, + seed_identity: String, +} + +fn sha256_file(path: &Path) -> Result { + use sha2::Digest; + let bytes = std::fs::read(path).map_err(|e| { + format!( + "could not read {} for its content identity: {e}", + path.display() + ) + })?; + Ok(format!("{:x}", sha2::Sha256::digest(&bytes))) +} + +/// The emitted closure's identity: one digest over the crate's emitted sources, path and +/// content in sorted order, so the receipt names WHAT was compiled, not merely that something +/// was. +fn emitted_closure_identity(crate_dir: &Path) -> Result { + use sha2::Digest; + let src_dir = crate_dir.join("src"); + let mut files: Vec = std::fs::read_dir(&src_dir) + .map_err(|e| format!("could not list {}: {e}", src_dir.display()))? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|path| path.extension().map(|ext| ext == "rs").unwrap_or(false)) + .collect(); + files.sort(); + let mut hasher = sha2::Sha256::new(); + for path in &files { + let bytes = + std::fs::read(path).map_err(|e| format!("could not read {}: {e}", path.display()))?; + hasher.update( + path.file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default() + .as_bytes(), + ); + hasher.update(&bytes); + } + Ok(format!("{:x}", hasher.finalize())) +} + +/// The derived universe plus BOTH directions of the module index's path relation. The +/// discovery-row join needs path → module (producer rows key on the entry path); the context +/// reclassification needs module → path (observations key on the module). The module → path +/// direction carries no collision refusal of its own: `ModuleSourceIndex` is keyed by module +/// and its construction already refuses two files declaring one module, so the relation is a +/// function by construction — a second check here would have an unauthorable RED. +struct NativeUniverseDerivation { + universe: Vec<(String, String)>, + module_for_path: HashMap, + path_for_module: HashMap, +} + +/// THE UNIVERSE IS DERIVED BY THE FLOOR'S OWN PRODUCER, NOT BY A SECOND SCAN. The production +/// required floor folds `discover_floor_rows_for_source` over the full module inventory and +/// finalizes with `floor_discovery_finalize_source_outcomes`; this derivation makes the same +/// two calls over the same inventory and keeps the rows whose AUTHORED module (read off the +/// `module` header by the index, never path-derived) carries the universe prefix. A second +/// discovery beside the producer would be free to disagree with the floor about what a test is. +fn derive_native_universe(source_roots: &[String]) -> Result { + let (graph, indices) = + super::resolve_workspace_entry(source_roots, super::FLOOR_DISCOVERY_PRODUCER_ENTRY)?; + let ctx = super::make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); + let index = super::try_build_module_index(source_roots)?; + let mut inventory: Vec<(String, String, String)> = index + .iter() + .map(|(module_path, source)| { + ( + source.path.replace('\\', "/"), + module_path.clone(), + source.content.clone(), + ) + }) + .collect(); + inventory.sort(); + let module_for_path: HashMap = inventory + .iter() + .map(|(path, module, _)| (path.clone(), module.clone())) + .collect(); + let path_for_module: HashMap = inventory + .iter() + .map(|(path, module, _)| (module.clone(), path.clone())) + .collect(); + eprintln!( + "required-ci: v2-native deriving the universe — {} sources through the floor discovery producer", + inventory.len() + ); + let mut outcomes: Vec = Vec::with_capacity(inventory.len()); + for (path, _, content) in &inventory { + let args = [ + (Some("repo_path".to_string()), str_value(path)), + (Some("content".to_string()), str_value(content)), + ]; + let outcome = v1_interpreter::run_in_context_with_args( + &ctx, + "v2.workflow.floor_discovery_producer.discover_floor_rows_for_source", + &args, + false, + ) + .map_err(|e| { + format!( + "V2-NATIVE REFUSAL cause=UniverseDerivationUnevaluable source={path} — \ + discover_floor_rows_for_source: {e}" + ) + })?; + outcomes.push(outcome); + } + let finalized = v1_interpreter::run_in_context_with_args( + &ctx, + "v2.workflow.floor_discovery_producer.floor_discovery_finalize_source_outcomes", + &[( + Some("outcomes".to_string()), + super::list_value_from_vec(outcomes), + )], + false, + ) + .map_err(|e| { + format!( + "V2-NATIVE REFUSAL cause=UniverseDerivationUnevaluable — \ + floor_discovery_finalize_source_outcomes: {e}" + ) + })?; + let rows = + super::parse_floor_discovery_producer_result(&ctx, &finalized).map_err(|reason| { + format!("V2-NATIVE REFUSAL cause=UniverseDerivationRefused — {reason}") + })?; + let mut universe: Vec<(String, String)> = Vec::new(); + for row in &rows { + let Some(module) = module_for_path.get(row.entry.as_str()) else { + return Err(format!( + "V2-NATIVE REFUSAL cause=UniverseEntryOutsideSubject entry={} — the discovery \ + authority enrolled an entry the module index does not hold", + row.entry + )); + }; + if module.starts_with(UNIVERSE_PREFIX) { + universe.push((module.clone(), row.function.clone())); + } + } + // SORTED FOR CANONICAL TRANSPORT, NEVER DEDUPLICATED. A duplicated identity is a harness + // defect the admission authority refuses by name (UniverseDuplicatesPresent); normalizing + // it away here would erase the evidence before the authority can judge it. + universe.sort(); + if universe.is_empty() { + return Err( + "V2-NATIVE REFUSAL cause=UniverseUnderived — the floor discovery producer enrolled \ + no v2.test.* rows over the full module inventory" + .to_string(), + ); + } + Ok(NativeUniverseDerivation { + universe, + module_for_path, + path_for_module, + }) +} + +/// PREPARATION IS THE EMIT-COMPILE PHASE'S OWN MACHINERY, REUSED. The same emission entry +/// point, the same crate writer, the same cargo invocation the required emit-compile probes use +/// — a second emit-or-build path beside them would be free to disagree about what "the emitted +/// compiler" is. The seed is used exactly once here, in-process, to emit; the receipt records +/// the seed's identity honestly, and this job claims no native bootstrap. +fn prepare_emitted_compiler(source_roots: &[String]) -> Result { + let workspace = super::process_workspace_root(); + // The probe root follows the declared execution environment (per-job runner temp in CI, + // host temp locally) — the selection's authority and its receipt live beside the required + // phase's own root policy in `emitted_closure_compile_host`. + let probe_root = super::lane_emit_compile_probe_root(); + eprintln!("required-ci: v2-native emitting {NATIVE_COMPILE_ENTRY} (seed, in-process)"); + let run = super::compile_entry_emission( + source_roots, + NATIVE_COMPILE_ENTRY, + true, + crate::v1_compiler_artifact::RenderTarget::Rust, + ); + match &run.disposition { + super::CompileDisposition::Completed { .. } => {} + super::CompileDisposition::Refused { phase, cause } => { + return Err(format!( + "V2-NATIVE REFUSAL cause=EmissionRefused stage={phase} — {cause}" + )) + } + super::CompileDisposition::NotExecuted { + earlier_phase, + cause, + } => { + return Err(format!( + "V2-NATIVE REFUSAL cause=EmissionRefused stage={earlier_phase} — {cause}" + )) + } + } + let (crate_dir, written) = super::emitted_closure_compile_host::write_probe_crate( + &run, + &probe_root, + NATIVE_COMPILE_ENTRY, + ) + .map_err(|cause| format!("V2-NATIVE REFUSAL cause=EmittedCrateNotWritten — {cause}"))?; + let closure_identity = emitted_closure_identity(&crate_dir)?; + // THE BUILD'S PEAK MUST NOT STACK ON THE EMISSION'S RETAINED ARENA. The emission's resolved + // graph died inside `compile_entry_emission` and the emitted file texts die with `run` here, + // but glibc retains the freed arena — and the cargo build below needs gigabytes beside this + // process. Measured: the lane's first run held ~15GiB RSS into the build and was SIGKILLed + // (rc=137, no diagnostic). Drop, trim, and report in the same motion — the floor runner's + // full-inventory release is the pattern, and a trim that cannot release live memory doubles + // as the measurement that nothing here is still held. + drop(run); + let rss_before_kb = super::current_rss_bytes().map(|b| b / 1024); + let trim_reclaimed_kb = super::trim_retained_heap(); + let rss_after_kb = super::current_rss_bytes().map(|b| b / 1024); + eprintln!( + "required-ci: v2-native emitted {written} files into {} (closure {closure_identity}); \ + emission arena released (rss_kb_before={rss_before_kb:?} trim_reclaimed_kb={trim_reclaimed_kb:?} \ + rss_kb_after={rss_after_kb:?}); cargo build", + crate_dir.display() + ); + let verdict = super::emitted_closure_compile_host::run_cargo( + &crate_dir, + &workspace, + "v2_native_lane_carries_no_mutation_probe", + ); + if !super::emitted_closure_compile_host::cargo_verdict_compiled(&verdict) { + return Err(format!( + "V2-NATIVE REFUSAL cause=EmittedCompilerBuildFailed — {}", + super::emitted_closure_compile_host::cargo_verdict_summary(&verdict) + )); + } + let binary_path = workspace.join("target").join("release").join( + super::emitted_closure_compile_host::probe_package_name(NATIVE_COMPILE_ENTRY), + ); + if !binary_path.is_file() { + return Err(format!( + "V2-NATIVE REFUSAL cause=EmittedCompilerAbsent — cargo reported success but {} is \ + not a file", + binary_path.display() + )); + } + let binary_identity = sha256_file(&binary_path)?; + let seed_identity = sha256_file(&std::env::current_exe().map_err(|e| { + format!("V2-NATIVE REFUSAL cause=SeedIdentityUnreadable — current_exe: {e}") + })?)?; + eprintln!( + "required-ci: v2-native emitted compiler at {} (sha256 {binary_identity})", + binary_path.display() + ); + Ok(EmittedPreparation { + binary_path, + binary_identity, + closure_identity, + seed_identity, + }) +} + +/// THE OLD-ROUTE WITHDRAWAL, AS A GUARD SO A REFUSAL PATH CANNOT SKIP THE RESTORE. The seed's +/// `gunbc` binary — the old compiler/interpreter route's CLI — is renamed out of the way before +/// the first native spawn and restored when the guard drops, on every exit path. The receipt +/// records the withdrawn executable's path; the window is what admission's control clause names. +struct OldRouteWithdrawalGuard { + original: PathBuf, + withdrawn: PathBuf, +} + +fn withdraw_old_route(workspace: &Path) -> Result { + let original = workspace.join("target").join("release").join("gunbc"); + let withdrawn = workspace + .join("target") + .join("release") + .join("gunbc.withdrawn-v2-native"); + if !original.is_file() { + return Err(format!( + "V2-NATIVE REFUSAL cause=OldRouteWithdrawalImpossible — {} is not a file; the \ + control cannot withdraw what is absent", + original.display() + )); + } + std::fs::rename(&original, &withdrawn).map_err(|e| { + format!( + "V2-NATIVE REFUSAL cause=OldRouteWithdrawalImpossible — renaming {}: {e}", + original.display() + ) + })?; + eprintln!( + "required-ci: v2-native old route withdrawn ({} moved aside for the native spawns)", + original.display() + ); + Ok(OldRouteWithdrawalGuard { + original, + withdrawn, + }) +} + +impl Drop for OldRouteWithdrawalGuard { + fn drop(&mut self) { + if let Err(e) = std::fs::rename(&self.withdrawn, &self.original) { + eprintln!( + "required-ci: v2-native WARNING — restoring the old route failed ({}): {e}", + self.original.display() + ); + } + } +} + +/// The universe file the emitted binary reads: `moduledeclaration` per line, the derived +/// universe followed by the two live-verdict controls (which the harness names explicitly — +/// they are controls, never universe members, and the receipt's population join would refuse +/// them as foreign rows if they reached it). +fn write_universe_file(path: &Path, universe: &[(String, String)]) -> Result<(), String> { + let mut text = String::new(); + for (module, declaration) in universe { + text.push_str(module); + text.push('\t'); + text.push_str(declaration); + text.push('\n'); + } + for declaration in [FALSE_CONTROL_DECL, TRUE_CONTROL_DECL] { + text.push_str(CONTROL_MODULE); + text.push('\t'); + text.push_str(declaration); + text.push('\n'); + } + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("creating {}: {e}", parent.display()))?; + } + std::fs::write(path, text).map_err(|e| format!("writing {}: {e}", path.display())) +} + +/// One controls-only universe file for the malformed-specimen run: the poisoned source root is +/// the subject. The control rows stay named because the driver refuses an empty universe file; +/// under the control run's fixture-only roots they resolve to nothing, and their prepare-stage +/// refusal observations are not consumed — the run's only consumed output is the poison path's +/// per-file refusal. +fn write_control_universe_file(path: &Path) -> Result<(), String> { + write_universe_file(path, &[]) +} + +/// Build the malformed control's scratch source root: exactly the committed specimen, +/// materialized under its `.dag` name. The root is REMOVED first — a scratch root that +/// accumulates whatever earlier runs left behind would let a stale second file into the +/// control's ingest, and the control's whole value is that its root holds exactly one poisoned +/// file. Returns the workspace-relative root the binary is spawned with. +fn materialize_malformed_specimen(workspace: &Path) -> Result { + let specimen = workspace.join(MALFORMED_SPECIMEN_COMMITTED); + let bytes = std::fs::read(&specimen).map_err(|e| { + format!( + "V2-NATIVE REFUSAL cause=MalformedSpecimenUnreadable — reading {}: {e}", + specimen.display() + ) + })?; + let root = workspace.join(MALFORMED_CONTROL_ROOT); + if root.exists() { + std::fs::remove_dir_all(&root).map_err(|e| format!("clearing {}: {e}", root.display()))?; + } + let materialized = workspace.join(MALFORMED_MATERIALIZED_PATH); + let parent = materialized + .parent() + .expect("the materialized path names a file under the control root"); + std::fs::create_dir_all(parent).map_err(|e| format!("creating {}: {e}", parent.display()))?; + std::fs::write(&materialized, bytes) + .map_err(|e| format!("writing {}: {e}", materialized.display()))?; + Ok(MALFORMED_CONTROL_ROOT.to_string()) +} + +/// The spawned run's decoded observations: one row per printed verdict, the per-file refusals +/// the context fold collected, and the terminal marker's row count. A line that is neither an +/// observation, nor a file refusal, nor the terminal marker refuses the parse — the binary's +/// stdout is a receipt surface, and an unrecognized line on it is a harness defect, not noise. +struct NativeRunOutput { + observations: Vec, + file_refusals: Vec, + terminal_rows: u64, +} + +fn decode_verdict_json(value: &serde_json::Value) -> Result { + let variant = value + .get("_variant") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("verdict carries no _variant tag: {value}"))?; + match variant { + "NativeTestPassed" => Ok(NativeVerdictObserved::Passed), + "NativeTestReturnedFalse" => Ok(NativeVerdictObserved::ReturnedFalse), + "NativeTestReturnedOther" => Ok(NativeVerdictObserved::ReturnedOther), + "NativeTestRefused" => { + let stage = value + .get("stage") + .and_then(|s| s.get("_variant")) + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("refusal verdict carries no stage tag: {value}"))?; + let reason = value + .get("reason") + .and_then(|r| r.as_str()) + .ok_or_else(|| format!("refusal verdict carries no reason: {value}"))?; + Ok(NativeVerdictObserved::Refused { + stage: stage.to_string(), + reason: reason.to_string(), + }) + } + other => Err(format!("unknown verdict variant: {other}")), + } +} + +fn parse_native_run_output(stdout: &str) -> Result { + let mut observations = Vec::new(); + let mut file_refusals = Vec::new(); + let mut terminal_rows = None; + for line in stdout.lines() { + if line.trim().is_empty() { + continue; + } + let value: serde_json::Value = serde_json::from_str(line) + .map_err(|e| format!("unparseable line on the native run's stdout: {e}: {line}"))?; + if let Some(terminal) = value.get("_terminal") { + if terminal.as_str() != Some("complete") { + return Err(format!("terminal marker is not complete: {line}")); + } + terminal_rows = value.get("rows").and_then(|r| r.as_u64()); + continue; + } + if let Some(refusal) = value.get("file_refusal") { + let path = refusal + .get("path") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("file refusal carries no path: {line}"))?; + let head_reason = refusal + .get("head_reason") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("file refusal carries no head_reason: {line}"))?; + let fatal_reason = refusal + .get("fatal_reason") + .and_then(|v| v.as_str()) + .ok_or_else(|| format!("file refusal carries no fatal_reason: {line}"))?; + file_refusals.push(NativeFileRefusalObserved { + path: path.to_string(), + head_reason: head_reason.to_string(), + fatal_reason: fatal_reason.to_string(), + }); + continue; + } + let module: Vec = value + .get("module") + .and_then(|m| serde_json::from_value(m.clone()).ok()) + .ok_or_else(|| format!("observation carries no module segments: {line}"))?; + let declaration = value + .get("declaration") + .and_then(|d| d.as_str()) + .ok_or_else(|| format!("observation carries no declaration: {line}"))?; + let verdict = decode_verdict_json( + value + .get("verdict") + .ok_or_else(|| format!("observation carries no verdict: {line}"))?, + )?; + observations.push(NativeObservation { + module: module.join("."), + declaration: declaration.to_string(), + verdict, + }); + } + let terminal_rows = + terminal_rows.ok_or_else(|| "the native run printed no terminal marker".to_string())?; + Ok(NativeRunOutput { + observations, + file_refusals, + terminal_rows, + }) +} + +/// Spawn the emitted binary once, by explicit path, and decode its stdout. A nonzero exit or an +/// unparseable stdout refuses the lane — the run's output is the receipt's evidence, and a +/// partial or garbled evidence stream is a red, never a truncated green. +fn run_native_binary( + binary: &Path, + universe_file: &Path, + source_roots: &[String], +) -> Result { + let output = Command::new(binary) + .arg(universe_file) + .args(source_roots) + .output() + .map_err(|e| { + format!( + "V2-NATIVE REFUSAL cause=NativeRunSpawnFailed — spawning {}: {e}", + binary.display() + ) + })?; + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + if !output.status.success() { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeRunFailed status={:?} — {}\n{}", + output.status.code(), + binary.display(), + String::from_utf8_lossy(&output.stderr) + .lines() + .rev() + .take(20) + .collect::>() + .into_iter() + .rev() + .collect::>() + .join("\n") + )); + } + parse_native_run_output(&stdout) +} + +/// A CONTEXT-STAGE ROW IS A DERIVED CLASSIFICATION, AND IT IS MINTED ONLY FROM ITS OBSERVATION. +/// A module whose file the context fold refused never reached preparation, so the binary's row +/// for it is a prepare-stage resolver refusal; the harness rewrites exactly those rows to the +/// Context stage carrying the FILE'S fatal reason — the cause that actually refused the file — +/// and admission's context_refusals_backed clause refuses any Context row whose (module, path, +/// reason) triple no observed file refusal backs through the receipt's module-source index. A +/// prepare refusal whose module's file loaded stays a prepare refusal with the resolver's own +/// reason. +/// +/// THE JOIN KEY IS module → path, NEVER path → module: the observation names its module, and +/// the file refusal names its path, so the relation is queried in the module → path direction. +/// (An earlier draft queried the path-keyed map with the module, which type-checks at +/// `HashMap` and matches nothing — the reclassification silently never fired.) +fn reclassify_context_refusals( + observations: &mut [NativeObservation], + file_refusals: &[NativeFileRefusalObserved], + path_for_module: &HashMap, +) { + let refused_by_path: HashMap<&str, &NativeFileRefusalObserved> = file_refusals + .iter() + .map(|fr| (fr.path.as_str(), fr)) + .collect(); + for observation in observations.iter_mut() { + let Some(path) = path_for_module.get(observation.module.as_str()) else { + continue; + }; + let Some(file_refusal) = refused_by_path.get(path.as_str()) else { + continue; + }; + if matches!(observation.verdict, NativeVerdictObserved::Refused { .. }) { + observation.verdict = NativeVerdictObserved::Refused { + stage: "NativeTestStageContext".to_string(), + reason: file_refusal.fatal_reason.clone(), + }; + } + } +} + +// ── the receipt, as interpreter Values ────────────────────────────────────────────────────── +// +// The receipt is built as the `.dag` authority's own types — Symbol fields enter as Str (the +// interpreter's symbol literal value), records and variants name their types, and the Optional +// controls use the Present/Absent encoding every host bridge in this crate shares. + +fn identity_value(ctx: &v1_interpreter::InterpContext, module: &str, declaration: &str) -> Value { + Value::Record { + type_name: ctx.sym("NativeRouteTestIdentity"), + fields: Rc::new(vec![ + (ctx.sym("module"), str_value(module)), + (ctx.sym("declaration"), str_value(declaration)), + ]), + } +} + +fn verdict_value(ctx: &v1_interpreter::InterpContext, verdict: &NativeVerdictObserved) -> Value { + match verdict { + NativeVerdictObserved::Passed => Value::Variant { + type_name: ctx.sym("NativeTestVerdict"), + variant_name: ctx.sym("NativeTestPassed"), + fields: Rc::new(vec![]), + }, + NativeVerdictObserved::ReturnedFalse => Value::Variant { + type_name: ctx.sym("NativeTestVerdict"), + variant_name: ctx.sym("NativeTestReturnedFalse"), + fields: Rc::new(vec![]), + }, + NativeVerdictObserved::ReturnedOther => Value::Variant { + type_name: ctx.sym("NativeTestVerdict"), + variant_name: ctx.sym("NativeTestReturnedOther"), + fields: Rc::new(vec![]), + }, + NativeVerdictObserved::Refused { stage, reason } => Value::Variant { + type_name: ctx.sym("NativeTestVerdict"), + variant_name: ctx.sym("NativeTestRefused"), + fields: Rc::new(vec![ + ( + ctx.sym("stage"), + Value::Variant { + type_name: ctx.sym("NativeTestStage"), + variant_name: ctx.sym(stage), + fields: Rc::new(vec![]), + }, + ), + (ctx.sym("reason"), str_value(reason)), + ]), + }, + } +} + +fn optional_verdict_value( + ctx: &v1_interpreter::InterpContext, + verdict: Option<&NativeVerdictObserved>, +) -> Value { + match verdict { + Some(v) => Value::Variant { + type_name: ctx.sym("Optional"), + variant_name: ctx.sym("Present"), + fields: Rc::new(vec![(ctx.sym("value"), verdict_value(ctx, v))]), + }, + None => Value::Variant { + type_name: ctx.sym("Optional"), + variant_name: ctx.sym("Absent"), + fields: Rc::new(vec![]), + }, + } +} + +#[allow(clippy::too_many_arguments)] +fn receipt_value( + ctx: &v1_interpreter::InterpContext, + tested_tree: &str, + preparation: &EmittedPreparation, + universe: &[(String, String)], + module_source_index: &[(String, String)], + population: &[NativeObservation], + file_refusals: &[NativeFileRefusalObserved], + false_control: Option<&NativeVerdictObserved>, + true_control: Option<&NativeVerdictObserved>, + malformed_control: Value, + withdrawn_executable: &str, +) -> Value { + let universe_values: Vec = universe + .iter() + .map(|(module, declaration)| identity_value(ctx, module, declaration)) + .collect(); + let population_values: Vec = population + .iter() + .map(|row| Value::Record { + type_name: ctx.sym("NativeRouteMemberRow"), + fields: Rc::new(vec![ + ( + ctx.sym("identity"), + identity_value(ctx, &row.module, &row.declaration), + ), + (ctx.sym("verdict"), verdict_value(ctx, &row.verdict)), + ]), + }) + .collect(); + let file_refusal_values: Vec = file_refusals + .iter() + .map(|fr| Value::Record { + type_name: ctx.sym("NativeTestFileRefusal"), + fields: Rc::new(vec![ + (ctx.sym("path"), str_value(&fr.path)), + (ctx.sym("head_reason"), str_value(&fr.head_reason)), + (ctx.sym("fatal_reason"), str_value(&fr.fatal_reason)), + ]), + }) + .collect(); + let module_source_values: Vec = module_source_index + .iter() + .map(|(module, path)| Value::Record { + type_name: ctx.sym("NativeRouteModuleSource"), + fields: Rc::new(vec![ + (ctx.sym("module"), str_value(module)), + (ctx.sym("path"), str_value(path)), + ]), + }) + .collect(); + Value::Record { + type_name: ctx.sym("NativeRouteReceipt"), + fields: Rc::new(vec![ + (ctx.sym("job"), str_value("required-v2-native")), + (ctx.sym("tested_tree"), str_value(tested_tree)), + ( + ctx.sym("preparation_seed_identity"), + str_value(&preparation.seed_identity), + ), + ( + ctx.sym("emitted_closure_identity"), + str_value(&preparation.closure_identity), + ), + ( + ctx.sym("executable_path"), + str_value(preparation.binary_path.display().to_string()), + ), + ( + ctx.sym("executable_identity"), + str_value(&preparation.binary_identity), + ), + ( + ctx.sym("universe"), + super::list_value_from_vec(universe_values), + ), + ( + ctx.sym("module_source_index"), + super::list_value_from_vec(module_source_values), + ), + ( + ctx.sym("population"), + super::list_value_from_vec(population_values), + ), + ( + ctx.sym("file_refusals"), + super::list_value_from_vec(file_refusal_values), + ), + ( + ctx.sym("false_control"), + optional_verdict_value(ctx, false_control), + ), + ( + ctx.sym("true_control"), + optional_verdict_value(ctx, true_control), + ), + (ctx.sym("malformed_control"), malformed_control), + ( + ctx.sym("old_route_control"), + Value::Variant { + type_name: ctx.sym("NativeRouteOldRouteControl"), + variant_name: ctx.sym("OldRouteWithdrawn"), + fields: Rc::new(vec![( + ctx.sym("withdrawn_executable"), + str_value(withdrawn_executable), + )]), + }, + ), + ]), + } +} + +/// The lane's one phase. Green exactly when the admission authority admits the minted receipt; +/// every earlier failure is a located refusal that stops the line. THE VERDICT IS THE +/// AUTHORITY'S OWN, EVALUATED — never re-decided in Rust: the receipt Value is handed to +/// `native_route_admission` in the authority's closure, and the summary the lane prints is +/// `native_route_admission_summary` of that same value, so the terminal line and the admission +/// fold cannot disagree about what was decided. +pub fn run_required_v2_native(source_roots: &[String]) -> Result<(), String> { + let workspace = super::process_workspace_root(); + let tested_tree = std::env::var("GITHUB_SHA").unwrap_or_else(|_| "local".to_string()); + + // 1. THE UNIVERSE, DERIVED. The floor's producer over the full module inventory, filtered + // to the universe prefix, plus both directions of the module/path relation: the entry join + // consumed path → module during derivation, and the context reclassification consumes + // module → path below. + let derivation = derive_native_universe(source_roots)?; + let universe = derivation.universe; + eprintln!( + "required-ci: v2-native universe derived — {} v2.test.* identities", + universe.len() + ); + // THE MODULE-SOURCE INDEX THE RECEIPT CARRIES, restricted to the universe's own modules — + // the domain the context reclassification can touch and the admission join reasons over. + // Built here, where a missing row is a located refusal, rather than inside receipt minting + // where it could only panic or default. + let mut universe_modules: Vec = + universe.iter().map(|(module, _)| module.clone()).collect(); + universe_modules.dedup(); + let mut module_source_index: Vec<(String, String)> = Vec::with_capacity(universe_modules.len()); + for module in &universe_modules { + let Some(path) = derivation.path_for_module.get(module) else { + return Err(format!( + "V2-NATIVE REFUSAL cause=UniverseEntryOutsideSubject module={module} — the \ + derived universe names a module the module index does not hold" + )); + }; + module_source_index.push((module.clone(), path.clone())); + } + // The derivation's interpreter context and the full inventory's source bytes died with the + // call above; return the retained arena before the emission peaks beside it (same release + // discipline as the emission-to-build handoff below). + let derivation_trim_kb = super::trim_retained_heap(); + eprintln!("required-ci: v2-native derivation arena released (trim_reclaimed_kb={derivation_trim_kb:?})"); + + // 2. PREPARATION. The seed emits the compiler closure once, in-process; cargo builds the + // emitted crate; the receipt records all three identities. + let preparation = prepare_emitted_compiler(source_roots)?; + + // 3. THE UNIVERSE FILE — the derived universe plus the two named controls. + let universe_file = workspace + .join("target") + .join("v2-native-lane") + .join("universe.tsv"); + write_universe_file(&universe_file, &universe)?; + + // 4. THE OLD ROUTE IS WITHDRAWN for the whole spawn window (universe run and malformed + // control alike); the guard restores it on every exit path. + let withdrawal = withdraw_old_route(&workspace)?; + let withdrawn_executable = withdrawal.original.display().to_string(); + + // 5. THE NATIVE RUN. The emitted binary, by explicit path, over the derived universe. + eprintln!("required-ci: v2-native running the emitted compiler over the universe"); + let main_output = run_native_binary(&preparation.binary_path, &universe_file, source_roots)?; + eprintln!( + "required-ci: v2-native native run complete — {} verdict rows, {} file refusals", + main_output.terminal_rows, + main_output.file_refusals.len() + ); + + // 6. THE MALFORMED CONTROL. The same binary over the materialized specimen's scratch root + // ALONE. The run's only consumed output is the poison path's per-file refusal, and rooting + // the control at the full corpus paid a second whole-corpus context fold — the lane's + // dominant cost — to produce it. Tokenization is per-file and deterministic, so the + // observed refusal is identical under the restricted roots. The committed specimen is not a + // `.dag` file (its constant carries the reason), so the control's source root is built here + // — exactly the specimen, rebuilt fresh, never whatever a previous run left behind. + let control_universe_file = workspace + .join("target") + .join("v2-native-lane") + .join("controls-universe.tsv"); + write_control_universe_file(&control_universe_file)?; + let control_root = materialize_malformed_specimen(&workspace)?; + let control_roots = vec![control_root]; + let control_output = run_native_binary( + &preparation.binary_path, + &control_universe_file, + &control_roots, + )?; + drop(withdrawal); + eprintln!("required-ci: v2-native old route restored"); + + // 7. THE RECEIPT. Controls are lifted out of the observed population (they are not universe + // members and the exact join would refuse them as foreign), context refusals are reclassified + // against the observed file refusals, and the malformed control records what the poisoned + // run observed. + let mut population: Vec = Vec::new(); + let mut false_control: Option = None; + let mut true_control: Option = None; + for observation in main_output.observations { + if observation.module == CONTROL_MODULE && observation.declaration == FALSE_CONTROL_DECL { + false_control = Some(observation.verdict); + } else if observation.module == CONTROL_MODULE + && observation.declaration == TRUE_CONTROL_DECL + { + true_control = Some(observation.verdict); + } else { + population.push(observation); + } + } + reclassify_context_refusals( + &mut population, + &main_output.file_refusals, + &derivation.path_for_module, + ); + let malformed_control = match control_output + .file_refusals + .iter() + .find(|fr| fr.path.contains(MALFORMED_MATERIALIZED_PATH)) + { + Some(fr) => { + let reason = fr.fatal_reason.clone(); + let path = fr.path.clone(); + // Built late because it needs the admission context's symbols; carried as data here. + (path, reason) + } + None => (String::new(), String::new()), + }; + + let (graph, indices) = super::resolve_workspace_entry( + source_roots, + super::WorkspaceRootRelativeEntry(NATIVE_ROUTE_AUTHORITY_ENTRY), + )?; + let route_ctx = super::make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); + let malformed_value = if malformed_control.0.is_empty() { + Value::Variant { + type_name: route_ctx.sym("NativeRouteMalformedControl"), + variant_name: route_ctx.sym("MalformedSpecimenAccepted"), + fields: Rc::new(vec![]), + } + } else { + Value::Variant { + type_name: route_ctx.sym("NativeRouteMalformedControl"), + variant_name: route_ctx.sym("MalformedSpecimenRefused"), + fields: Rc::new(vec![ + (route_ctx.sym("path"), str_value(&malformed_control.0)), + (route_ctx.sym("reason"), str_value(&malformed_control.1)), + ]), + } + }; + let receipt = receipt_value( + &route_ctx, + &tested_tree, + &preparation, + &universe, + &module_source_index, + &population, + &main_output.file_refusals, + false_control.as_ref(), + true_control.as_ref(), + malformed_value, + &withdrawn_executable, + ); + + // 8. ADMISSION, BY THE AUTHORITY. The lane prints the authority's own summary either way. + let admission = v1_interpreter::run_in_context_with_args( + &route_ctx, + "gunbc.witness_v2_native_route.native_route_admission", + &[(Some("receipt".to_string()), receipt)], + false, + ) + .map_err(|e| format!("V2-NATIVE REFUSAL cause=AdmissionUnevaluable — {e}"))?; + let summary = v1_interpreter::run_in_context_with_args( + &route_ctx, + "gunbc.witness_v2_native_route.native_route_admission_summary", + &[(Some("admission".to_string()), admission.clone())], + false, + ) + .map_err(|e| format!("V2-NATIVE REFUSAL cause=AdmissionUnevaluable — summary: {e}"))?; + let summary_text = match &summary { + Value::Str(s) => s.to_string(), + other => { + return Err(format!( + "V2-NATIVE REFUSAL cause=AdmissionUnevaluable — the summary returned {}, not a \ + String", + super::output_policy_value_shape(other) + )) + } + }; + let admitted = match &admission { + Value::Variant { variant_name, .. } => { + route_ctx.sym_eq(*variant_name, "NativeRouteAdmitted") + } + other => { + return Err(format!( + "V2-NATIVE REFUSAL cause=AdmissionUnevaluable — native_route_admission returned \ + {}, not a NativeRouteAdmission variant", + super::output_policy_value_shape(other) + )) + } + }; + eprintln!("required-ci: v2-native admission {summary_text}"); + if admitted { + Ok(()) + } else { + Err(format!( + "V2-NATIVE REFUSAL cause=AdmissionRefused — {summary_text}" + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn refused_observation( + module: &str, + declaration: &str, + stage: &str, + reason: &str, + ) -> NativeObservation { + NativeObservation { + module: module.to_string(), + declaration: declaration.to_string(), + verdict: NativeVerdictObserved::Refused { + stage: stage.to_string(), + reason: reason.to_string(), + }, + } + } + + fn refusal_shape(observation: &NativeObservation) -> Option<(&str, &str)> { + match &observation.verdict { + NativeVerdictObserved::Refused { stage, reason } => Some((stage, reason)), + _ => None, + } + } + + /// THE PRODUCTION-PATH RECLASSIFICATION, AT ITS EXACT JOIN GRAIN. A test module whose own + /// file the context fold refused (here: an unterminated string at tokenize, the poison + /// class the malformed control carries) surfaces in the binary's rows as a Prepare-stage + /// resolver refusal; the reclassification must rewrite THAT identity — keyed by its module, + /// through the module → path direction of the index relation — to the Context stage + /// carrying the file's fatal reason. A second module whose file loaded keeps its Prepare + /// refusal untouched: the join is per-subject, never "some file anywhere". + #[test] + fn context_reclassification_joins_module_to_its_own_files_refusal() { + let mut observations = vec![ + refused_observation( + "v2.test.poisoned", + "never_ran", + "NativeTestStagePrepare", + "resolve_module_not_found", + ), + refused_observation( + "v2.test.healthy", + "ran_and_refused", + "NativeTestStagePrepare", + "resolve_reason_unbound_symbol", + ), + ]; + let file_refusals = vec![NativeFileRefusalObserved { + path: "src/v2/test/poisoned_test.dag".to_string(), + head_reason: "parse_g0_tokens_remain".to_string(), + fatal_reason: "tokenize_lex_e1_unterminated_string".to_string(), + }]; + let path_for_module: HashMap = [ + ( + "v2.test.poisoned".to_string(), + "src/v2/test/poisoned_test.dag".to_string(), + ), + ( + "v2.test.healthy".to_string(), + "src/v2/test/healthy_test.dag".to_string(), + ), + ] + .into_iter() + .collect(); + + reclassify_context_refusals(&mut observations, &file_refusals, &path_for_module); + + assert_eq!( + refusal_shape(&observations[0]), + Some(( + "NativeTestStageContext", + "tokenize_lex_e1_unterminated_string" + )), + "the poisoned module's exact identity must receive its own file's Context-stage \ + fatal reason" + ); + assert_eq!( + refusal_shape(&observations[1]), + Some(("NativeTestStagePrepare", "resolve_reason_unbound_symbol")), + "a module whose file loaded keeps the resolver's own Prepare-stage reason" + ); + } + + /// THE DIRECTION IS LOAD-BEARING. Handed the relation in its REVERSED shape — paths as + /// keys, the exact pre-repair defect — the lookup by module must miss and leave the row a + /// Prepare refusal: no module → path row means no reclassification, never a guessed join. + #[test] + fn context_reclassification_misses_when_queried_off_key() { + let mut observations = vec![refused_observation( + "v2.test.poisoned", + "never_ran", + "NativeTestStagePrepare", + "resolve_module_not_found", + )]; + let file_refusals = vec![NativeFileRefusalObserved { + path: "src/v2/test/poisoned_test.dag".to_string(), + head_reason: "parse_g0_tokens_remain".to_string(), + fatal_reason: "tokenize_lex_e1_unterminated_string".to_string(), + }]; + let reversed_relation: HashMap = [( + "src/v2/test/poisoned_test.dag".to_string(), + "v2.test.poisoned".to_string(), + )] + .into_iter() + .collect(); + + reclassify_context_refusals(&mut observations, &file_refusals, &reversed_relation); + + assert_eq!( + refusal_shape(&observations[0]), + Some(("NativeTestStagePrepare", "resolve_module_not_found")), + "a path-keyed map cannot answer the module-keyed question" + ); + } +} diff --git a/src/v1/stage0/src/cli_run/required_lane_roster.rs b/src/v1/stage0/src/cli_run/required_lane_roster.rs new file mode 100644 index 00000000000..8365dde86df --- /dev/null +++ b/src/v1/stage0/src/cli_run/required_lane_roster.rs @@ -0,0 +1,79 @@ +//! THE (PHASE, LANE) PAIR JOIN — the lane-ownership half of the phase-roster wall. +//! +//! The variant-set join (`phase_roster_findings` in the claim_executor bin) reads variant names +//! off the declaration index, so it can join phase EXISTENCE but not lane OWNERSHIP: a match arm +//! is not a declaration. Lane ownership leaves the authority only by evaluation, through +//! `gunbc.required_ci_phase_roster` `required_ci_lane_phase_rows`. +//! +//! Why this half exists: the required workflow is three independently selected lanes, and the +//! v2-native lane's isolation is load-bearing. A host edit that mapped `V2NativePhase` to another +//! lane while the authority still rosters it in `v2-native` would leave the native job selecting +//! zero phases — `phases_run=0`, no phase failure, a green required job that executed no native +//! test. Joining (phase, lane) pairs in both directions makes that edit red at run start in +//! every lane; the empty-expectation refusal and the exact ran-set check (both in the bin, over +//! the rows this module decodes) make a zero-phase selected lane refuse rather than report +//! success over nothing. + +use crate::v1_interpreter::{self, Value}; + +/// The roster authority's entry file — the same module the variant-set join reads off the +/// declaration index, here consumed by evaluation instead. +const LANE_ROSTER_AUTHORITY_ENTRY: super::WorkspaceRootRelativeEntry = + super::WorkspaceRootRelativeEntry("dag/gunbc/required_ci_phase_roster.dag"); + +/// One authority-rostered (lane, phase) pair, both names in the CLI spelling the host matches on. +pub struct LanePhaseRow { + pub lane: String, + pub phase: String, +} + +fn lane_phase_row_from_value( + ctx: &v1_interpreter::InterpContext, + val: &Value, +) -> Result { + let Value::Record { fields, .. } = val else { + return Err(format!( + "expected RequiredCiLanePhaseRow record, got `{}`", + ctx.format_value(val) + )); + }; + let lane = match ctx.field(fields, "lane") { + Some(Value::Str(s)) => s.to_string(), + _ => return Err("RequiredCiLanePhaseRow missing `lane`".to_string()), + }; + let phase = match ctx.field(fields, "phase") { + Some(Value::Str(s)) => s.to_string(), + _ => return Err("RequiredCiLanePhaseRow missing `phase`".to_string()), + }; + Ok(LanePhaseRow { lane, phase }) +} + +/// Evaluate the authority's `required_ci_lane_phase_rows` and decode every row. Any failure — +/// the module absent, the function unevaluable, a row of the wrong shape — is a refusal, never +/// an empty roster: an unreadable authority is the state in which nothing is checking lane +/// ownership, not permission to proceed. +pub fn authority_lane_phase_rows(source_roots: &[String]) -> Result, String> { + let (graph, indices) = + super::resolve_workspace_entry(source_roots, LANE_ROSTER_AUTHORITY_ENTRY)?; + let ctx = super::make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Hermetic); + let value = v1_interpreter::run_in_context_with_args( + &ctx, + "gunbc.required_ci_phase_roster.required_ci_lane_phase_rows", + &[], + false, + ) + .map_err(|e| format!("lane-roster authority evaluation refused: {e}"))?; + match &value { + Value::List(items) => { + let mut out = Vec::with_capacity(items.len()); + for item in items.iter() { + out.push(lane_phase_row_from_value(&ctx, item)?); + } + Ok(out) + } + other => Err(format!( + "required_ci_lane_phase_rows returned `{}`, expected a List", + ctx.format_value(other) + )), + } +} diff --git a/src/v1/stage0/src/emitted_closure_compile_host.rs b/src/v1/stage0/src/emitted_closure_compile_host.rs index 4134685e412..610d7d611d2 100644 --- a/src/v1/stage0/src/emitted_closure_compile_host.rs +++ b/src/v1/stage0/src/emitted_closure_compile_host.rs @@ -88,7 +88,9 @@ const PROBE_ROOT_DIR_NAME: &str = "gunbc-emit-compile"; /// /// Deriving the name per entry makes each probe crate its own package, so fingerprints cannot /// alias; dependencies are separate packages and stay warm. -fn probe_package_name(entry: &str) -> String { +/// `pub(crate)` so the v2-native lane's harness names the built binary by the same package name +/// the manifest writer gave the crate — one derivation, never a second slug beside it. +pub(crate) fn probe_package_name(entry: &str) -> String { let slug: String = entry .chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) @@ -167,11 +169,32 @@ pub fn cargo_verdict_compiled(verdict: &CargoVerdict) -> bool { matches!(verdict, CargoVerdict::Completed { status: 0, .. }) } +/// THE SUMMARY IS THE REFUSAL'S DIAGNOSTIC SURFACE. Callers that cannot compile print this string +/// and stop (`EmittedCompilerBuildFailed`, restore-failed mutation, a red baseline). Rendering +/// only `status` swallows the rustc header and stderr the verdict already holds — a completed +/// non-zero cargo then looks like a bare exit code, which is how a native-lane build refusal +/// reached the log with no error class. A zero-status run stays compact: there is nothing to +/// attribute. A non-zero run names the attributed diagnostic and line when the scan found them, +/// and always carries `stderr_tail` so an unattributed refusal (the native lane's dummy +/// attribution symbol, a kill-adjacent cargo, a fault in another module) still prints WHAT rustc +/// said. pub fn cargo_verdict_summary(verdict: &CargoVerdict) -> String { match verdict { CargoVerdict::NotAttempted { reason } => format!("NotAttempted reason={reason}"), CargoVerdict::DidNotComplete { detail } => format!("DidNotComplete detail={detail}"), - CargoVerdict::Completed { status, .. } => format!("Completed status={status}"), + CargoVerdict::Completed { status, .. } if *status == 0 => { + format!("Completed status={status}") + } + CargoVerdict::Completed { + status, + stderr_tail, + probe_line, + probe_diagnostic, + } => format!( + "Completed status={status} diagnostic={} line={} stderr_tail={stderr_tail}", + probe_diagnostic.as_deref().unwrap_or("unattributed"), + probe_line.as_deref().unwrap_or("unattributed"), + ), } } @@ -483,8 +506,18 @@ pub fn emit_compile_outcome_summary(outcome: &EmitCompileOutcome) -> String { /// Package header from `extdeps.rust.version` `render_cargo_package_header_prefix`; dependency /// rows from `v1.compiler.stage0_crates` `stage0_foundation_runtime_dependencies` (the seed's /// runtime dependency set, which emitted code links against), each rendered by that module's -/// `render_stage0_crate_dep`. No `[lib]` section: `src/lib.rs` is cargo's own default, so naming -/// it would be a second spelling. +/// `render_stage0_crate_dep`. +/// +/// THE `[lib]` NAME IS THE EMITTER'S CONTRACT, NOT A SPELLING OF THE PATH. `src/lib.rs` stays +/// cargo's default path; what must be named is the LIB TARGET's crate name, because the emitted +/// `main.rs` reaches the closure through it: `v1.compiler.emit_rust` `emit_rust_selected` binds +/// the self-emitted crate's name (`v1_compiled` for every non-retained-host pipeline entry), and +/// the SourceRootEvalDriver and DirectIngestDriver mains both `use v1_compiled::…`. The package +/// name is per-entry (one slug per probe, sharing one target dir), so without this section the +/// lib takes the package's name and the driver main's self-references fail E0433 — measured on +/// the required-v2-native lane's first preparation. Pipeline-free probe entries never named +/// their crate in a `use`, so the gap was unreachable until a pipeline entry became a probe +/// subject. /// /// The corpus's hand-authored TOML string (`tools.self_host_curated_seed_linked_harness` /// `cssl_v1_compiled_probe_lib_cargo_toml`) is deliberately not used: it is marked scaffold debt @@ -528,8 +561,11 @@ fn probe_manifest(workspace: &Path, entry: &str) -> String { let features = render_stage0_crate_features_section(stage0_features_for_crate_kind( GeneratedPartitionCrateKind::GeneratedFoundationCrate, )); + // `v1_compiled` is the emitter's own literal (`emit_rust_selected`), mirrored here the way + // every seed mirror in this file is: the host cannot reach into the emission for it, and the + // emitted main.rs's self-references fail to link under any other lib name. format!( - "{}\nedition = \"2021\"\n{features}\n[dependencies]\n{rendered}", + "{}\nedition = \"2021\"\n\n[lib]\nname = \"v1_compiled\"\n{features}\n[dependencies]\n{rendered}", render_cargo_package_header_prefix(probe_package_name(entry)) ) } @@ -538,6 +574,13 @@ fn probe_manifest(workspace: &Path, entry: &str) -> String { /// root is inferred into the workspace and would need its own `[workspace]` to escape — a manifest /// fact invented to work around its location. /// +/// The per-job temp a CI executor declares, when it declares one — the ONE read of that +/// declaration, so the required phase's refusal and the lane's selection cannot drift into two +/// spellings of the same fact. +fn declared_runner_temp() -> Option { + std::env::var_os("RUNNER_TEMP").filter(|value| !value.is_empty()) +} + /// RUNNER-SCOPED, NOT HOST-SHARED, AND THIS WAS MEASURED THE HARD WAY. A fixed path in the host's /// `/tmp` is shared by every tenant of a SELF-HOSTED runner and persists across runs, slots and /// jobs. On the first required run the directory existed owned by another uid, so creating the @@ -568,13 +611,34 @@ fn required_ci_probe_root_from_runner_temp( } pub fn required_ci_emit_compile_probe_root() -> Result { - required_ci_probe_root_from_runner_temp(std::env::var_os("RUNNER_TEMP").as_deref()) + required_ci_probe_root_from_runner_temp(declared_runner_temp().as_deref()) } pub fn local_emit_compile_probe_root() -> PathBuf { std::env::temp_dir().join(PROBE_ROOT_DIR_NAME) } +fn lane_probe_root_from_runner_temp(runner_temp: Option<&std::ffi::OsStr>) -> PathBuf { + match runner_temp.filter(|value| !value.is_empty()) { + Some(base) => PathBuf::from(base).join(PROBE_ROOT_DIR_NAME), + None => local_emit_compile_probe_root(), + } +} + +/// THE V2-NATIVE LANE'S PROBE ROOT FOLLOWS THE DECLARED EXECUTION ENVIRONMENT, SELECTED ONCE. +/// +/// The lane runs in two environments and each has its own root authority. In required CI the +/// executor declares a per-job temp: the self-hosted fleet's host-shared temp persists across +/// jobs, runs and euids, and a stale or concurrent `gunbc-emit-compile` there is an EACCES at +/// best and two runs writing one crate dir at worst (receipt: run 34471447387, +/// `EmittedCrateNotWritten — … Permission denied`). Locally no runner temp exists and the host +/// temp is the local route's authority. This is environment SELECTION, not a failure arm: both +/// roots are declared, nothing is widened, and the required phase's own stricter policy +/// (refuse without the declaration) is untouched beside it. +pub fn lane_emit_compile_probe_root() -> PathBuf { + lane_probe_root_from_runner_temp(declared_runner_temp().as_deref()) +} + fn probe_crate_dir(probe_root: &Path, entry: &str) -> PathBuf { let slug: String = entry .chars() @@ -584,7 +648,11 @@ fn probe_crate_dir(probe_root: &Path, entry: &str) -> PathBuf { } /// Write the emitted Rust files plus a manifest, and return the crate directory. -fn write_probe_crate( +/// +/// `pub(crate)` for the required-v2-native lane's harness (cli_run::native_lane_runner), which +/// prepares the emitted-native compiler through this same writer rather than growing a second +/// one beside it (DESIGN §2 — the note on `write_probe_crate_files` is the argument). +pub(crate) fn write_probe_crate( run: &CompileRun, probe_root: &Path, entry: &str, @@ -697,7 +765,13 @@ fn attributed_diagnostic( /// /// Phases within one required run are sequential in one process, so nothing else holds cargo's /// lock on that directory. -fn run_cargo(crate_dir: &Path, workspace: &Path, attribution_symbol: &str) -> CargoVerdict { +/// `pub(crate)` for the same consumer as `write_probe_crate`: the v2-native lane builds the +/// emitted compiler crate through this same cargo invocation. +pub(crate) fn run_cargo( + crate_dir: &Path, + workspace: &Path, + attribution_symbol: &str, +) -> CargoVerdict { let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); let mut command = std::process::Command::new(&cargo); command @@ -1917,6 +1991,47 @@ pub fn run_required_emit_compile( mod tests { use super::*; + /// A NON-ZERO COMPLETED VERDICT MUST NAME WHAT RUSTC SAID. The native lane's refusal + /// interpolates only this summary; a status-only render is how a cargo failure reached CI + /// as `Completed status=101` with the diagnostic sitting unread in the same verdict. + #[test] + fn cargo_verdict_summary_renders_the_diagnostic_a_non_zero_run_already_holds() { + let attributed = CargoVerdict::Completed { + status: 101, + stderr_tail: "error: could not compile `probe`".to_string(), + probe_line: Some("--> src/fixture.rs:1:1".to_string()), + probe_diagnostic: Some("error[E0308]: mismatched types".to_string()), + }; + let summary = cargo_verdict_summary(&attributed); + assert!( + summary.contains("error[E0308]: mismatched types"), + "attributed header is the class the caller is adjudicating: {summary}" + ); + assert!( + summary.contains("--> src/fixture.rs:1:1"), + "attributed location rides with the header: {summary}" + ); + let unattributed = CargoVerdict::Completed { + status: 101, + stderr_tail: "error[E0433]: failed to resolve".to_string(), + probe_line: None, + probe_diagnostic: None, + }; + let summary = cargo_verdict_summary(&unattributed); + assert!( + summary.contains("error[E0433]: failed to resolve"), + "an unattributed refusal still prints stderr_tail, which is the native lane's case: \ + {summary}" + ); + let green = CargoVerdict::Completed { + status: 0, + stderr_tail: String::new(), + probe_line: None, + probe_diagnostic: None, + }; + assert_eq!(cargo_verdict_summary(&green), "Completed status=0"); + } + /// The manifest is DERIVED, so this asserts the derivation reached the modeled rows, not a /// golden string: version-authority package header, the seed's runtime dependency set, and /// the path dependency the emitted closure links against. @@ -1939,8 +2054,12 @@ mod tests { assert!(manifest.contains(name), "missing dependency row {name}"); } assert!(manifest.contains("/repo/src/v1/stage0")); - // `src/lib.rs` is cargo's own default, so restating it would be a second spelling. - assert!(!manifest.contains("[lib]")); + // THE LIB NAME IS THE EMITTER'S SELF-NAME CONTRACT: the emitted driver mains reach the + // closure through `use v1_compiled::…` (`v1.compiler.emit_rust` `emit_rust_selected` + // binds the name), so the probe crate's lib target must carry it even though the package + // name is per-entry. The PATH stays cargo's default and is not restated. + assert!(manifest.contains("[lib]\nname = \"v1_compiled\"\n")); + assert!(!manifest.contains("path = \"src/lib.rs\"")); } /// One emitted crate on disk, authored by the caller, so each test states the shape it means. @@ -2120,6 +2239,31 @@ mod tests { ); } + /// THE LANE SELECTS THE DECLARED PER-JOB ROOT WHEN ONE EXISTS, the host temp otherwise. + /// + /// Both arms are pinned because both are load-bearing: the first keeps a required-CI lane + /// off the self-hosted fleet's host-shared temp (the EACCES of run 34471447387), the second + /// keeps the local route runnable where no executor declares a temp. The expected values + /// name the authorities, never the spelled dir name — the single-spelling test above owns + /// that needle. + #[test] + fn the_lane_probe_root_follows_the_declared_environment() { + assert_eq!( + lane_probe_root_from_runner_temp(Some(std::ffi::OsStr::new("/runner/job"))), + required_ci_probe_root_from_runner_temp(Some(std::ffi::OsStr::new("/runner/job"))) + .expect("the same declared temp"), + "a declared per-job runner temp owns the lane's probe root, exactly as it owns the \ + required phase's" + ); + for absent in [None, Some(std::ffi::OsStr::new(""))] { + assert_eq!( + lane_probe_root_from_runner_temp(absent), + local_emit_compile_probe_root(), + "with no declared runner temp the lane takes the local route's root" + ); + } + } + /// A FAILED RESTORE MUST WIN OVER EVERY NON-TERMINAL FAULT VERDICT. /// /// The two conditions are independent and co-occur: the fault arm can be green, incomplete diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 58b0700561b..c88e689330e 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -4401,6 +4401,9 @@ fn run_built_seed_regen( })?; } let on_disk = current_exe_on_disk()?; + // The admitted side carries the executable-digest spelling (`current_exe_digest`, + // `next_pass_executable_digest`); the tagged file digest (`path_digest`) spells the same + // bytes differently, and this gate compared them verbatim -- unpassable since #9771. let observed_executable_digest = current_exe_digest()?; if observed_executable_digest != admitted_executable_digest { return Err(format!( diff --git a/src/v1/stage0/src/std_compiler_entry.rs b/src/v1/stage0/src/std_compiler_entry.rs index b3b96ec2c84..b22a21ba75a 100644 --- a/src/v1/stage0/src/std_compiler_entry.rs +++ b/src/v1/stage0/src/std_compiler_entry.rs @@ -16,9 +16,12 @@ use std::rc::Rc; pub enum CompilerEntryDriver { RetainedHostCliKernel, DirectIngestDriver, + SourceRootEvalDriver, } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RetainedHostCliKernel; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct DirectIngestDriver; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SourceRootEvalDriver; diff --git a/src/v1/stage0/src/std_occurrence_binding_candidates.rs b/src/v1/stage0/src/std_occurrence_binding_candidates.rs index a65a2ec673c..5cbb4886280 100644 --- a/src/v1/stage0/src/std_occurrence_binding_candidates.rs +++ b/src/v1/stage0/src/std_occurrence_binding_candidates.rs @@ -313,8 +313,7 @@ pub fn occurrence_containment_parent_scope( std::option::Option::None } else { let parent_terminal = (*__fm)[0].clone(); - let parent_ancestors_reversed: Rc> = - Rc::new((*__fm).iter().skip(1).cloned().collect()); + let parent_ancestors_reversed: Rc> = Rc::new(__fm.skip(1)); Some(Rc::new(OccurrenceContainmentPath { ancestors: v1_rt::reverse(parent_ancestors_reversed.clone()), terminal: parent_terminal.clone(), diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 503740cf544..a7b82d31ee6 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -817,10 +817,17 @@ pub fn rust_type_is_rc_wrapped(type_name: String) -> bool { } pub fn rust_shared_wrap_ctor(inner_expr: String) -> String { - crate::v1_compiler_languages::sharing_wrap_ctor_for_target( - RenderTarget::Rust, - inner_expr.clone(), - ) + if inner_expr.starts_with("panic!(") + || inner_expr.starts_with("compile_error!(") + || inner_expr.starts_with("unreachable!(") + { + inner_expr + } else { + crate::v1_compiler_languages::sharing_wrap_ctor_for_target( + RenderTarget::Rust, + inner_expr.clone(), + ) + } } pub fn rust_shared_wrap_ctor_prefix() -> String { @@ -17515,7 +17522,18 @@ if ((child.children.clone().len() as i64) == 0) { if variant_is_synthetic_positional_payload(child.children.clone(), env.source_indices.clone()) { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" ".to_string(), variant_path.clone()), "(_) => panic!(\"no ".to_string()), fname.clone()), " on positional-payload variant\"),".to_string()) } else { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" ".to_string(), variant_path.clone()), " { ".to_string()), crate::v1_compiler_emit::emit_ident(fname.clone(), RenderTarget::Rust)), ": __val, .. } => ".to_string()), crate::v1_compiler_emit_core_support::apply_type_template1(sharing.clone_value.clone(), "__val".to_string())), ",".to_string()) + { + let arm_field_boxed = match Rc::new({ let mut __result = Vec::new(); for f in child.children.clone().iter().cloned() { if (crate::v1_compiler_infer_env::authored_name(env.clone(), f.clone()) == fname.clone()) { __result.push(f); } } __result }).first().cloned() { + Some(f) => needs_box_wrapping(crate::v1_compiler_infer_types::resolved_type(f.clone()), recursive_types.clone(), shared_types.clone(), env.source_indices.clone()), + std::option::Option::None => false, +}; +let clone_expr = if arm_field_boxed.clone() { + crate::v1_compiler_emit_core_support::apply_type_template1(sharing.deref_clone.clone(), "*__val".to_string()) + } else { + crate::v1_compiler_emit_core_support::apply_type_template1(sharing.clone_value.clone(), "__val".to_string()) + }; +v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" ".to_string(), variant_path.clone()), " { ".to_string()), crate::v1_compiler_emit::emit_ident(fname.clone(), RenderTarget::Rust)), ": __val, .. } => ".to_string()), clone_expr.clone()), ",".to_string()) +} } } }); } __result }); @@ -21700,6 +21718,40 @@ Rc::new(vec![v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::conc }) } +pub fn rust_as_ref_let_is_irrefutable( + pattern: Rc, + emit_info: Rc, +) -> bool { + if crate::v1_std_core::match_pattern_is_irrefutable(pattern.clone()) { + true + } else { + match (*pattern.clone()).clone() { + MatchPattern::VariantPattern { + name: n, + parent_enum: parent, + .. + } => { + let resolved = pattern_parent_enum( + crate::v1_std_core::qualified_last_segment(n.clone()), + parent.clone(), + "".to_string(), + emit_info.type_summaries.clone(), + ); + match resolved { + Some(enum_name) => match emit_info.type_summaries.get(&enum_name).cloned() { + Some(summary) => { + v1_rt::sorted_map_keys(summary.variant_name_set.as_ref()).len() <= 1 + } + None => false, + }, + None => false, + } + } + _ => false, + } + } +} + pub fn rc_pattern_preludes( pattern: Rc, rc_analysis: Rc, @@ -21775,7 +21827,12 @@ let inner_analysis = analyze_rc_pattern(fb_pat_here.clone(), "".to_string(), sha if inner_analysis.needs_rc_pattern.clone() { { let aware = emit_pattern_rc_aware(fb_pat_here.clone(), v1_rt::rc_list_push(v1_rt::rc_list_push(Rc::new(vec![]), bare_n.clone()), fb_name.clone()), inner_analysis.clone(), shared_types.clone(), "".to_string(), source_indices.clone(), emit_info.clone()); -let head = v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let ".to_string(), aware.clone()), " = ".to_string()), crate::v1_compiler_emit::emit_ident(fb_name.clone(), RenderTarget::Rust)), ".as_ref() else { unreachable!() };".to_string()); +let tail = if rust_as_ref_let_is_irrefutable(fb_pat_here.clone(), emit_info.clone()) { + ";".to_string() + } else { + " else { unreachable!() };".to_string() + }; +let head = v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let ".to_string(), aware.clone()), " = ".to_string()), crate::v1_compiler_emit::emit_ident(fb_name.clone(), RenderTarget::Rust)), ".as_ref()".to_string()), tail.clone()); let inner_preludes = rc_pattern_preludes(fb_pat_here.clone(), inner_analysis.clone(), shared_types.clone(), source_indices.clone(), emit_info.clone()); if (inner_preludes.clone() == "".to_string()) { Rc::new(vec![head.clone()]) @@ -21786,7 +21843,7 @@ if (inner_preludes.clone() == "".to_string()) { } else { { let bound_str = emit_pattern(fb_pat_here.clone(), v1_rt::rc_list_push(v1_rt::rc_list_push(Rc::new(vec![]), bare_n.clone()), fb_name.clone()), shared_types.clone(), "".to_string(), source_indices.clone(), emit_info.clone()); -let tail = if crate::v1_std_core::match_pattern_is_irrefutable(fb_pat_here.clone()) { +let tail = if rust_as_ref_let_is_irrefutable(fb_pat_here.clone(), emit_info.clone()) { ";".to_string() } else { " else { unreachable!() };".to_string() @@ -28248,7 +28305,7 @@ pub fn freemonoid_tail_let_from_fm(tail_bind: String) -> String { } else { v1_rt::concat( v1_rt::concat("let ".to_string(), tail_bind.clone()), - ": Rc> = Rc::new((*__fm).iter().skip(1).cloned().collect()); ".to_string(), + ": Rc> = Rc::new(__fm.skip(1)); ".to_string(), ) } } @@ -38627,6 +38684,15 @@ pub fn compiler_entry_retained_host_driver() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn compiler_entry_source_root_eval_driver() -> String { + thread_local! { + static CACHED: String = { + "SourceRootEvalDriver".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct CompilerPipelineEntryDecl { pub module_name: String, @@ -38738,6 +38804,13 @@ pub fn compiler_pipeline_entry_is_retained_host(modules: Rc> } } +pub fn compiler_pipeline_entry_is_source_root_eval(modules: Rc>>) -> bool { + match compiler_pipeline_entry_decl(modules.clone()) { + Some(d) => (d.driver.clone() == compiler_entry_source_root_eval_driver()), + std::option::Option::None => false, + } +} + pub fn compiler_pipeline_entry_is_ambiguous(modules: Rc>>) -> bool { ((compiler_pipeline_entry_decls(modules.clone()).len() as i64) > 1) } @@ -38927,7 +39000,18 @@ pub fn emit_main_rs( if (corpus_has_compiler_pipeline(modules.clone()) && (retained_host_pipeline.clone() == false)) { - return emit_direct_ingest_driver_main_rs(crate_name.clone(), pipeline_module.clone()); + { + if compiler_pipeline_entry_is_source_root_eval(modules.clone()) { + return emit_source_root_eval_driver_main_rs( + crate_name.clone(), + pipeline_module.clone(), + ); + } + return emit_direct_ingest_driver_main_rs( + crate_name.clone(), + pipeline_module.clone(), + ); + } } if (((workflow_funcs.clone().len() as i64) == 0) && (retained_host_pipeline.clone() == false)) @@ -39082,6 +39166,20 @@ pub fn emit_direct_ingest_driver_main_rs( } } +pub fn emit_source_root_eval_driver_main_rs( + crate_name: String, + pipeline_module: String, +) -> Rc { + { + let pipeline_mod = + crate::v1_compiler_emit_core_support::module_to_filename(pipeline_module.clone()); + Rc::new(TextFile { + path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), + content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.\n\n".to_string(), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]\n\n".to_string()), "use std::rc::Rc;\n\n".to_string()), "use ".to_string()), crate_name.clone()), "::extdeps_communication_medium::{DecodeFidelity, Medium};\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::{\n".to_string()), " native_test_context_from_ingest, native_test_eval_one, native_test_prepare_module,\n".to_string()), " NativeTestObservation, NativeTestStage, NativeTestVerdict,\n".to_string()), "};\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_source_authority::{source_root_for_storage_path, DagSourceReadWitness};\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_std_artifact::{Artifact, ArtifactKind};\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_std_diagnostic::Outcome;\n".to_string()), "\n".to_string()), "fn collect_dag_paths(dir: &std::path::Path, out: &mut Vec) -> Result<(), String> {\n".to_string()), " let entries = match std::fs::read_dir(dir) {\n".to_string()), " Ok(entries) => entries,\n".to_string()), " Err(cause) => return Err(format!(\"could not walk source root {}: {cause}\", dir.display())),\n".to_string()), " };\n".to_string()), " for entry in entries {\n".to_string()), " let entry = match entry {\n".to_string()), " Ok(entry) => entry,\n".to_string()), " Err(cause) => return Err(format!(\"could not read entry under {}: {cause}\", dir.display())),\n".to_string()), " };\n".to_string()), " let path = entry.path();\n".to_string()), " if path.is_dir() {\n".to_string()), " collect_dag_paths(&path, out)?;\n".to_string()), " } else if path.extension().map(|ext| ext == \"dag\").unwrap_or(false) {\n".to_string()), " out.push(path.to_string_lossy().into_owned());\n".to_string()), " }\n".to_string()), " }\n".to_string()), " Ok(())\n".to_string()), "}\n".to_string()), "\n".to_string()), "fn main() {\n".to_string()), " let mut args = std::env::args().skip(1);\n".to_string()), " let universe_path = match args.next() {\n".to_string()), " Some(path) => path,\n".to_string()), " None => {\n".to_string()), " eprintln!(\"REFUSED: no universe file given -- usage: ".to_string()), crate_name.clone()), " ...\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " };\n".to_string()), " let source_roots: Vec = args.collect();\n".to_string()), " if source_roots.is_empty() {\n".to_string()), " eprintln!(\"REFUSED: no source roots given -- usage: ".to_string()), crate_name.clone()), " ...\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " let universe_text = match std::fs::read_to_string(&universe_path) {\n".to_string()), " Ok(text) => text,\n".to_string()), " Err(cause) => {\n".to_string()), " eprintln!(\"REFUSED: could not read universe file {universe_path}: {cause}\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " };\n".to_string()), " let mut universe: Vec<(Rc>, String)> = Vec::new();\n".to_string()), " let mut seen: std::collections::HashSet = std::collections::HashSet::new();\n".to_string()), " for line in universe_text.lines() {\n".to_string()), " if line.is_empty() || line.starts_with('#') {\n".to_string()), " continue;\n".to_string()), " }\n".to_string()), " match line.split_once('\\t') {\n".to_string()), " Some((module, declaration)) if !module.is_empty() && !declaration.is_empty() => {\n".to_string()), " if !seen.insert(line.to_string()) {\n".to_string()), " eprintln!(\"REFUSED: duplicate universe line: {line}\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " universe.push((\n".to_string()), " Rc::new(module.split('.').map(|segment| segment.to_string()).collect::>()),\n".to_string()), " declaration.to_string(),\n".to_string()), " ));\n".to_string()), " }\n".to_string()), " _ => {\n".to_string()), " eprintln!(\"REFUSED: malformed universe line (expected moduledeclaration): {line}\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " if universe.is_empty() {\n".to_string()), " eprintln!(\"REFUSED: universe file {universe_path} carried no test identities\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " let mut paths: Vec = Vec::new();\n".to_string()), " for root in &source_roots {\n".to_string()), " if let Err(cause) = collect_dag_paths(std::path::Path::new(root), &mut paths) {\n".to_string()), " eprintln!(\"REFUSED: {cause}\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " }\n".to_string()), " paths.sort();\n".to_string()), " let mut reads: Vec> = Vec::new();\n".to_string()), " for path in &paths {\n".to_string()), " let text = match std::fs::read_to_string(path) {\n".to_string()), " Ok(text) => text,\n".to_string()), " Err(cause) => {\n".to_string()), " eprintln!(\"REFUSED: could not read {path}: {cause}\");\n".to_string()), " std::process::exit(2);\n".to_string()), " }\n".to_string()), " };\n".to_string()), " reads.push(Rc::new(DagSourceReadWitness {\n".to_string()), " source: Rc::new(Medium {\n".to_string()), " carried: text,\n".to_string()), " fidelity: DecodeFidelity::Lossless,\n".to_string()), " _phantom: std::marker::PhantomData,\n".to_string()), " }),\n".to_string()), " artifact: Rc::new(Artifact {\n".to_string()), " kind: ArtifactKind::SourceFile,\n".to_string()), " id: path.clone(),\n".to_string()), " file_path: path.clone(),\n".to_string()), " }),\n".to_string()), " compilation_unit: path.clone(),\n".to_string()), " source_root: source_root_for_storage_path(path.clone()),\n".to_string()), " }));\n".to_string()), " }\n".to_string()), " let context = match &*native_test_context_from_ingest(Rc::new(reads.into())) {\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),\n".to_string()), " Outcome::Rejected { diagnostics } => {\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");\n".to_string()), " std::process::exit(1);\n".to_string()), " }\n".to_string()), " };\n".to_string()), " for refusal in context.file_refusals.iter() {\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));\n".to_string()), " }\n".to_string()), " let mut module_order: Vec<(String, Rc>)> = Vec::new();\n".to_string()), " let mut module_decls: std::collections::HashMap> = std::collections::HashMap::new();\n".to_string()), " for (qn, declaration) in &universe {\n".to_string()), " let dotted = qn.iter().cloned().collect::>().join(\".\");\n".to_string()), " if !module_decls.contains_key(&dotted) {\n".to_string()), " module_order.push((dotted.clone(), qn.clone()));\n".to_string()), " module_decls.insert(dotted.clone(), Vec::new());\n".to_string()), " }\n".to_string()), " module_decls.get_mut(&dotted).unwrap().push(declaration.clone());\n".to_string()), " }\n".to_string()), " let mut rows: u64 = 0;\n".to_string()), " for (dotted, qn) in &module_order {\n".to_string()), " let declarations = &module_decls[dotted];\n".to_string()), " match &*native_test_prepare_module(context.clone(), qn.clone()) {\n".to_string()), " Outcome::Accepted { value: prepared, .. } => {\n".to_string()), " for declaration in declarations {\n".to_string()), " let observation = native_test_eval_one(prepared.clone(), declaration.clone());\n".to_string()), " println!(\"{}\", serde_json::to_string(&observation).unwrap());\n".to_string()), " rows += 1;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " Outcome::Rejected { diagnostics } => {\n".to_string()), " let reason = diagnostics.tail.last().map(|d| d.reason.clone()).unwrap_or_else(|| diagnostics.head.reason.clone());\n".to_string()), " for declaration in declarations {\n".to_string()), " let observation = NativeTestObservation {\n".to_string()), " module: qn.clone(),\n".to_string()), " declaration: declaration.clone(),\n".to_string()), " verdict: Rc::new(NativeTestVerdict::NativeTestRefused {\n".to_string()), " stage: NativeTestStage::NativeTestStagePrepare,\n".to_string()), " reason: reason.clone(),\n".to_string()), " }),\n".to_string()), " };\n".to_string()), " println!(\"{}\", serde_json::to_string(&observation).unwrap());\n".to_string()), " rows += 1;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " println!(\"{}\", serde_json::json!({ \"_terminal\": \"complete\", \"rows\": rows, \"file_refusals\": context.file_refusals.len() }));\n".to_string()), "}\n".to_string()), +}) + } +} + pub fn emit_entry_point_absent_main_rs(crate_name: String) -> Rc { Rc::new(TextFile { path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 16b54dd786c..b1905c2bd08 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -10370,7 +10370,7 @@ macro_rules! v1_algebra_method_arms { // String-straddle wall (`string_realization_straddle_detail`'s `Value::List` // exemption). Closed by regrounding `Char`/codepoint-sequence so the realization // is distinguishable (grounding root, sibling #5428). - let s = expect_str(Some(&$receiver), "chars")?; + let s = expect_str_free_monoid(Some(&$receiver), "chars")?; let items: Vec = s.chars().map(|c| Value::Int(c as i64)).collect(); Ok(list_value(items)) }, @@ -19654,6 +19654,107 @@ mod native_len_tests { } } +#[cfg(test)] +mod chars_receiver_tests { + use std::rc::Rc; + + use im::{vector as im_vec, HashMap}; + + use crate::v1_compiler_infer_emit_info::empty_emit_graph_info; + use crate::v1_compiler_infer_items::ResolvedGraph; + + use super::{ + expect_str_free_monoid, list_value, str_value, with_active_context, ExecutionMode, + InterpContext, InterpError, Value, + }; + + fn fresh_ctx() -> InterpContext { + let graph = ResolvedGraph { + modules: Rc::new(im_vec![]), + item_registry: Rc::new(HashMap::new()), + diagnostics: Rc::new(im_vec![]), + emit_graph_info: empty_emit_graph_info(), + }; + InterpContext::new(&graph, Rc::new(HashMap::new()), ExecutionMode::Hermetic) + } + + fn empty_string(ctx: &InterpContext) -> Value { + Value::Variant { + type_name: ctx.sym("String"), + variant_name: ctx.sym("Empty"), + fields: Rc::new(vec![]), + } + } + + fn cons_string(ctx: &InterpContext, text: &str) -> Value { + let mut cur = empty_string(ctx); + for cp in text.chars().rev() { + cur = Value::Variant { + type_name: ctx.sym("String"), + variant_name: ctx.sym("Cons"), + fields: Rc::new(vec![ + (ctx.sym("head"), Value::Int(cp as i64)), + (ctx.sym("tail"), cur), + ]), + }; + } + cur + } + + #[test] + fn chars_receiver_grounds_every_string_realization() { + let ctx = fresh_ctx(); + with_active_context(&ctx, || { + assert_eq!( + expect_str_free_monoid(Some(&str_value("ab")), "chars").unwrap(), + "ab" + ); + assert_eq!( + expect_str_free_monoid(Some(&empty_string(&ctx)), "chars").unwrap(), + "" + ); + assert_eq!( + expect_str_free_monoid(Some(&cons_string(&ctx, "hé")), "chars").unwrap(), + "hé" + ); + }); + } + + #[test] + fn chars_receiver_still_refuses_non_strings() { + let ctx = fresh_ctx(); + // A Cons chain whose head is not a codepoint is not a String. + let non_codepoint_chain = Value::Variant { + type_name: ctx.sym("String"), + variant_name: ctx.sym("Cons"), + fields: Rc::new(vec![ + (ctx.sym("head"), str_value("x")), + (ctx.sym("tail"), empty_string(&ctx)), + ]), + }; + // An unrelated variant is not an Empty/Cons chain at all. + let optional_some = Value::Variant { + type_name: ctx.sym("Optional"), + variant_name: ctx.sym("Some"), + fields: Rc::new(vec![(ctx.sym("value"), Value::Int(65))]), + }; + with_active_context(&ctx, || { + for v in [ + list_value(vec![Value::Int(97)]), + non_codepoint_chain, + optional_some, + Value::Int(97), + ] { + let err = expect_str_free_monoid(Some(&v), "chars").unwrap_err(); + assert!( + matches!(err, InterpError::TypeError { .. }), + "expected TypeError, got {err:?}" + ); + } + }); + } +} + // The well-known free-monoid encoding symbols. Pre-interned at context construction // (`over_scope_indexes`) so a lookup for any of them can never miss -- see // `free_monoid_ctx_syms` for why a miss must not be possible, only detected. @@ -19946,6 +20047,19 @@ fn expect_str(val: Option<&Value>, context: &str) -> InterpResult { } } +/// Like `expect_str`, but grounds a modeled String first: a String reaches a consumer in any +/// of its realizations — native `Str`, `Empty`/`Cons` codepoint chain, or a compacted mix — +/// and `free_monoid_to_string` already decides which values ARE strings (a generic +/// `Value::List` and a non-codepoint chain are not, and keep `expect_str`'s typed refusal). +/// `length`/`fold_list` already read every realization through `free_monoid_to_vec`; this is +/// the same realization-agnosticism for a consumer that needs the codepoints as one string. +fn expect_str_free_monoid(val: Option<&Value>, context: &str) -> InterpResult { + match val.and_then(free_monoid_to_string) { + Some(s) => Ok(s), + None => expect_str(val, context), + } +} + fn expect_byte_vec(val: Option<&Value>, context: &str) -> InterpResult> { match val { Some(Value::List(items)) => { diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index d0bbd203a8b..70abb1a1ad0 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -1,19 +1,31 @@ module v2.compiler.compile -import std.compiler_entry { CompilerEntryDriver, DirectIngestDriver } +import std.compiler_entry { CompilerEntryDriver, SourceRootEvalDriver } import v2.compiler.emit { emit } -import v2.compiler.eval { eval_node, inputs_root_only } +import v2.compiler.eval { eval, eval_node, eval_default_interpretation, inputs_root_only } import v2.compiler.infer { infer, InferredTree } import v2.compiler.name_resolve { Admission, ResolutionSubject, - resolve_with_admission + resolve_with_admission, + resolve_with_admission_grounded +} +import v2.compiler.program_assembly { + assemble_program_from_ingest, + program_assembly_fold_ingest, + program_assembly_read_to_normalized_root_prepared, + ProgramAssemblyFoldState, + ProgramAssemblyFoldOk, + ProgramAssemblyFoldFailed +} +import v2.compiler.source_authority { + DagSourceReadWitness, + SourceRootIngest, + source_root_set_from_ingest } -import v2.compiler.program_assembly { assemble_program_from_ingest } -import v2.compiler.source_authority { SourceRootIngest } import v2.compiler.normalize { normalize } import v2.compiler.normalized_tree { NormalizedTree } -import v2.compiler.parse { parse } +import v2.compiler.parse { parse, prepare_grammar, PreparedGrammar } import v2.compiler.resolve { resolve, ResolvedTree } import v2.compiler.tokenize { tokenize } import v2.std.staging { then_outcome, CacheProbe, Miss } @@ -26,7 +38,12 @@ import v2.std.grammar { ParseTree } import v2.std.language_model { LanguageModel } import extdeps.communication.medium { Lossless, Medium } import v2.std.compilers.target_model { TargetModel } -import v2.extdeps.languages.dag { dag_language_model, dag_target_model } +import v2.extdeps.languages.dag { + dag_language_model, + dag_target_model, + namespace_graft_unwrap_module_working_root, + qualified_name_from_module_node +} import v2.lens.complexity_accumulator_copy.compile_gate { accumulator_copy_compile_gate } import v2.lens.determinism { determinism_compile_gate } import v2.lens.machine_shape { machine_shape_compile_gate } @@ -36,13 +53,15 @@ import v2.lens.fact_density { fact_density_hollow_alias_gate } import v2.lens.unit_modeling { unit_modeling_gate } import v2.lens.lifecycle_carrier { lifecycle_carrier_gate } import std.algebra { Cons, Empty, FreeMonoid } -import v2.std.algebra { list_append, list_reverse } +import v2.std.algebra { fold_list, list_append, list_reverse, list_snoc_item } import v2.std.collection { List } import v2.std.diagnostic { Accepted, Diagnostic, + Diagnostics, ExternalContractUnknown, Locus, + NonEmptyDiagnostics, Outcome, Some, invariant_port_locus, @@ -55,7 +74,20 @@ import v2.std.diagnostic { outcome_rejected, outcome_with_diagnostics } -import v2.std.node { Edge, Node, NodeFold, Symbol, fold_node } +import v2.std.node { Arrow, Edge, Named, Node, NodeFold, Positional, Symbol, TypeNode, fold_node } +import v2.std.node_query { find_arrow_body_child, find_named_child } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.qualified_name { QualifiedName } +import v2.std.cross_tree.import_model { SourceRootRef } +import v2.std.cross_tree.resolution { + SourceRootIndex, + source_root_index_empty, + source_root_index_insert +} +import v2.extdeps.runtimes.v2_evaluator { + v2_eval_runtime_bool_is_false, + v2_eval_runtime_bool_is_true +} import v2.std.text { String } import v2.std.witness { Holds, Violates, Witness, witness_violates } @@ -64,13 +96,17 @@ import v2.std.witness { Holds, Violates, Witness, witness_violates } // literal this module could never satisfy, so a crate emitted from this corpus was rendered // with the no-entry-point refusal main no matter what it contained. // -// DirectIngestDriver rather than RetainedHostCliKernel, and the difference is not a preference: -// a crate emitted from this corpus does not contain v1_compiler.cli_run, so the retained-host -// driver would name a module that is not there. The driver reaches compile_ingest_with_admission -// below -- source text and a target in, emitted text out -- which is the entry a command line -// can be wired to. compile() is not that entry: it takes an already-parsed CoreNode. - -data compiler_pipeline_entry: CompilerEntryDriver = DirectIngestDriver +// SourceRootEvalDriver, and the difference from both siblings is a fact about what the emitted +// binary is FOR. RetainedHostCliKernel is unrenderable here (a crate emitted from this corpus +// does not contain v1_compiler.cli_run). DirectIngestDriver renders a one-source translate +// probe -- a census instrument for the standalone front door, which cannot resolve a module +// against its real peers and so cannot execute a repository test. SourceRootEvalDriver renders +// the repository test-execution route: the host reads the declared source roots and a universe +// of qualified test identities, and the pipeline assembles the ingest once, prepares each +// module (resolve + infer), and Evals each named test body, one typed verdict row per member. +// The driver functions below are the contract the rendered main calls. + +data compiler_pipeline_entry: CompilerEntryDriver = SourceRootEvalDriver type CoreNode = Node @@ -600,3 +636,354 @@ fn compile_ingest_staging(source: Medium, target: TargetModel) -> Outcom ) pipeline(source) } + +// ═══════════════════════════════════════════════════════════════════════════════════════════ +// THE SOURCE-ROOT EVAL DRIVER — the terminal route the required-v2-native CI lane executes. +// ═══════════════════════════════════════════════════════════════════════════════════════════ +// +// THE TERMINAL SUBJECT IS A TEST VERDICT, NOT A TRANSLATION. DirectIngestDriver answers "the +// native compiler accepted this standalone source for translation"; this driver answers "this +// exact test identity, resolved against its real source roots, reached a native Eval verdict". +// The route mirrors the floor's execution mode — locate the named declaration, evaluate its +// body, require a Boolean — with the v2 pipeline's own evaluator standing in for the seed +// interpreter. +// +// THREE FUNCTIONS, ONE PER COST GRAIN. native_test_context_from_ingest pays the whole-ingest +// fold (tokenize/parse/normalize every source) ONCE per lane; it rejects only on a LANE-LEVEL +// defect (a grammar that fails to prepare), never on corpus data. native_test_prepare_module +// pays resolve + infer ONCE per module. native_test_eval_one pays entry resolution + +// evaluation per declaration and NEVER rejects: every failure it can reach is a typed +// NativeTestRefused verdict carrying the stage and the diagnostic's head reason, so a census +// over observations has no silent remainder. +// +// THE PER-FILE FRONT-END REFUSAL IS CENSUS DATA, NOT A WHOLE-CONTEXT REFUSAL. The program +// assembly fold (v2.compiler.program_assembly) is wholesale fail-closed — one unparseable +// source fails the program — which is the right door for COMPILATION and the wrong shape for +// this census: the v2 front-end has a live corpus frontier (v2.workflow.compile_door_ledger), +// and a wholesale fold would let one frontier file deny verdict rows for every other member — +// the absorbing fallback in ingest form (DESIGN 5: the deficit's frequency reads zero because +// the whole lane is red). So the context fold below collects each source's tokenize/parse/ +// normalize refusal as a NativeTestFileRefusal row and keeps folding. Nothing is thereby +// WIDENED: a refused file contributes no root, so its module is absent from the index, every +// test identity living in it surfaces as a Context-stage refusal row minted by the lane host +// from exactly these rows, and a module that PARSED but imports a refused file fails at +// Prepare with the resolver's own diagnostic. The lane's admission still refuses over any +// unattributed row — collection changes the grain of the refusal, never its force. +// +// NO LENS GATES ON THIS ROUTE, deliberately. validate_then_compile's required lenses are the +// COMPILE door's corpus invariants; the floor's seed-interpreter route does not run them per +// test either, and running them here would conflate the modeling door with the execution route +// and multiply their whole-tree cost by the test population. + +type NativeTestStage + = NativeTestStageContext + | NativeTestStagePrepare + | NativeTestStageEntry + | NativeTestStageEval + +type NativeTestVerdict + = NativeTestPassed + | NativeTestReturnedFalse + | NativeTestReturnedOther + | NativeTestRefused { stage: NativeTestStage, reason: Symbol } + +type NativeTestObservation { + module: QualifiedName + declaration: Symbol + verdict: NativeTestVerdict +} + +// ONE SOURCE'S FRONT-END REFUSAL, AT THE TWO GRAINS THE CLASSIFICATION NEEDS. The rejection's +// diagnostic list prepends non-fatal pending advisories (rejected_with_pending), so the head +// reason and the fatal (last) reason are different observations of one refusal — the door +// ledger's cause-ownership table is keyed by exactly this grain distinction, and the lane's +// classifier asks both questions of every row. +type NativeTestFileRefusal { + path: Symbol + head_reason: Symbol + fatal_reason: Symbol +} + +type NativeTestContext { + roots: FreeMonoid + index: SourceRootIndex + active_roots: FreeMonoid + file_refusals: FreeMonoid +} + +type NativeTestPreparedModule { + module: QualifiedName + inferred: InferredTree +} + +type NativeTestContextFoldState { + roots: FreeMonoid + index: SourceRootIndex + file_refusals: FreeMonoid +} + +// The LAST diagnostic is the cause that actually refused the file; the head is the advisory +// that rode along. Both are carried because ownership is keyed by grain. +fn native_test_fatal_reason(d: NonEmptyDiagnostics) -> Symbol { + fold_list(xs: d.tail, empty: d.head.reason, cons: fn(_acc, diag) { diag.reason }) +} + +fn native_test_file_refusal(path: Symbol, d: NonEmptyDiagnostics) -> NativeTestFileRefusal { + NativeTestFileRefusal { + path: path, + head_reason: d.head.reason, + fatal_reason: native_test_fatal_reason(d: d) + } +} + +fn native_test_context_fold_step( + state: NativeTestContextFoldState, + read: DagSourceReadWitness, + lm: LanguageModel, + prepared: PreparedGrammar, + residue: Diagnostics +) -> NativeTestContextFoldState { + match program_assembly_read_to_normalized_root_prepared(read: read, lm: lm, prepared: prepared, residue: residue) { + Rejected { diagnostics: d } => + NativeTestContextFoldState { + roots: state.roots, + index: state.index, + file_refusals: list_snoc_item( + xs: state.file_refusals, + item: native_test_file_refusal(path: read.compilation_unit, d: d) + ) + } + Accepted { value: root, diagnostics: _ } => + match qualified_name_from_module_node(root: root.root) { + Rejected { diagnostics: d } => + NativeTestContextFoldState { + roots: state.roots, + index: state.index, + file_refusals: list_snoc_item( + xs: state.file_refusals, + item: native_test_file_refusal(path: read.compilation_unit, d: d) + ) + } + Accepted { value: qn, diagnostics: _ } => + NativeTestContextFoldState { + roots: list_snoc_item(xs: state.roots, item: root), + index: source_root_index_insert(index: state.index, module: qn, root: read.source_root), + file_refusals: state.file_refusals + } + } + } +} + +// One language model for the whole fold: dag_language_model() rebuilds the grammar, lex rules +// and canonical-symbol set on every call, and calling it inside the cons closure paid that +// once PER FILE — measured at ~0.3s/file of the context fold's wall time (2026-09-09, +// required-v2-native lane work), a pure recompute with no per-file dependency. +fn native_test_context_from_ingest(ingest: SourceRootIngest) -> Outcome { + let lm = dag_language_model() + match prepare_grammar(grammar: lm.grammar) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: prepared, diagnostics: residue } => { + let folded = fold_list( + xs: ingest, + empty: NativeTestContextFoldState { + roots: Empty, + index: source_root_index_empty(), + file_refusals: Empty + }, + cons: fn(state, read) { + native_test_context_fold_step( + state: state, + read: read, + lm: lm, + prepared: prepared, + residue: residue + ) + } + ) + outcome_accepted(value: NativeTestContext { + roots: folded.roots, + index: folded.index, + active_roots: source_root_set_from_ingest(ingest: ingest), + file_refusals: folded.file_refusals + }) + } + } +} + +fn native_test_prepare_module( + context: NativeTestContext, + module: QualifiedName +) -> Outcome { + bind_outcome( + o: resolve_with_admission_grounded( + lm: dag_language_model(), + roots: context.roots, + admission: Admission { + subject: ResolutionSubject { name: module }, + imports: Empty + }, + index: context.index, + active_roots: context.active_roots + ), + f: fn(resolved) { + bind_outcome( + o: infer(tree: resolved), + f: fn(inferred) { + outcome_accepted(value: NativeTestPreparedModule { + module: module, + inferred: inferred + }) + } + ) + } + ) +} + +// THE DECLARATION SEARCH IS EXACT: a test identity names a top-level declaration of one prepared +// module, so the search first resolves that module's scope node — unwrapping the module +// production wrapper through the namespace-graft authority, then walking one Named spine edge per +// qualified-name segment — and then matches DIRECT CHILDREN of the scope only. Descending into +// nested Named edges could select a same-named graft segment or an edge nested inside a +// declaration body and evaluate it under the requested identity. A scope walk that fails is a +// graft-shape surprise and surfaces as its own entry reason, deliberately absent from +// native_route_entry_limit_reasons so the surprise reds the lane instead of excluding tests. + +fn native_test_module_scope_go(at: Node, segments: QualifiedName) -> Outcome { + match segments { + Empty => outcome_accepted(value: at) + Cons { head: segment, tail: rest } => + bind_outcome( + o: find_named_child(root: at, name: segment), + f: fn(child) { native_test_module_scope_go(at: child, segments: rest) } + ) + } +} + +fn native_test_module_scope_node(root: Node, module: QualifiedName) -> Outcome { + native_test_module_scope_go( + at: namespace_graft_unwrap_module_working_root(root: root), + segments: module + ) +} + +fn native_test_decl_in_children(children: List, name: Symbol) -> Optional { + fold(children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: v } => optional_present(value: v) + Absent => + match e.label { + Named { name: edge_name } => + if edge_name == name { + optional_present(value: e.target) + } else { + optional_absent() + } + Positional => optional_absent() + } + } + }) +} + +fn native_test_observation_refused( + module: QualifiedName, + declaration: Symbol, + stage: NativeTestStage, + reason: Symbol +) -> NativeTestObservation { + NativeTestObservation { + module: module, + declaration: declaration, + verdict: NativeTestRefused { stage: stage, reason: reason } + } +} + +fn native_test_eval_body( + prepared: NativeTestPreparedModule, + declaration: Symbol, + body: Node +) -> NativeTestObservation { + match eval( + tree: prepared.inferred, + interpretation: eval_default_interpretation(), + inputs: inputs_root_only(root: body) + ) { + Rejected { diagnostics: d } => + native_test_observation_refused( + module: prepared.module, + declaration: declaration, + stage: NativeTestStageEval, + reason: native_test_fatal_reason(d: d) + ) + Accepted { value: rv, diagnostics: _ } => + if v2_eval_runtime_bool_is_true(value: rv) { + NativeTestObservation { + module: prepared.module, + declaration: declaration, + verdict: NativeTestPassed + } + } else if v2_eval_runtime_bool_is_false(value: rv) { + NativeTestObservation { + module: prepared.module, + declaration: declaration, + verdict: NativeTestReturnedFalse + } + } else { + NativeTestObservation { + module: prepared.module, + declaration: declaration, + verdict: NativeTestReturnedOther + } + } + } +} + +fn native_test_eval_one( + prepared: NativeTestPreparedModule, + declaration: Symbol +) -> NativeTestObservation { + match native_test_module_scope_node(root: prepared.inferred.root, module: prepared.module) { + Rejected { diagnostics: _ } => + native_test_observation_refused( + module: prepared.module, + declaration: declaration, + stage: NativeTestStageEntry, + reason: ^native_test_entry_module_scope_unresolved + ) + Accepted { value: scope, diagnostics: _ } => + match native_test_decl_in_children(children: scope.children, name: declaration) { + Absent => + native_test_observation_refused( + module: prepared.module, + declaration: declaration, + stage: NativeTestStageEntry, + reason: ^native_test_entry_declaration_not_found + ) + Present { value: target } => + match target.kind { + TypeNode { connective: Arrow } => + match find_arrow_body_child(root: target) { + Rejected { diagnostics: d } => + native_test_observation_refused( + module: prepared.module, + declaration: declaration, + stage: NativeTestStageEntry, + reason: native_test_fatal_reason(d: d) + ) + Accepted { value: body, diagnostics: _ } => + native_test_eval_body( + prepared: prepared, + declaration: declaration, + body: body + ) + } + _ => + native_test_observation_refused( + module: prepared.module, + declaration: declaration, + stage: NativeTestStageEntry, + reason: ^native_test_entry_declaration_not_arrow + ) + } + } + } +} diff --git a/src/v2/compiler/01_tokenize.dag b/src/v2/compiler/01_tokenize.dag index 84bab6dee0a..ad131673d6a 100644 --- a/src/v2/compiler/01_tokenize.dag +++ b/src/v2/compiler/01_tokenize.dag @@ -121,6 +121,15 @@ fn lex_match_prefix(prefix: v2.std.text.String, source: v2.std.text.String) -> L } } +// Lexeme growth must push items (amortized O(log n) on the persistent carrier, per +// list_snoc_item): list_append right-folds over its LEFT side, which is the accumulator +// at these call sites, so per-character accumulation paid O(n) a character - quadratic +// in literal length, measured at 18+ minutes on the 132KB string literal in +// gunbc.live_deploy.srv1_residue_rehearsal (2026-09-09, required-v2-native lane). +fn lexeme_grow(acc: v2.std.text.String, chunk: v2.std.text.String) -> v2.std.text.String { + fold_list(xs: chunk, empty: acc, cons: fn(grown, c) { list_snoc_item(xs: grown, item: c) }) +} + type RepeatState { remaining: v2.std.text.String lexeme: v2.std.text.String @@ -178,7 +187,7 @@ fn lex_repeat_step(elem_r: LexMatchThunk, state: RepeatState) -> RepeatState { elem_r: elem_r, state: RepeatState { remaining: rest, - lexeme: list_append(left: state.lexeme, right: consumed), + lexeme: lexeme_grow(acc: state.lexeme, chunk: consumed), done: false } ) @@ -199,7 +208,7 @@ fn lex_delimited_step(close_r: LexMatchThunk, body_r: LexMatchThunk, state: Deli LexMatchAccepted { remaining: after_close, lexeme: close_lex } => DelimitedState { remaining: after_close, - lexeme: list_append(left: state.lexeme, right: close_lex), + lexeme: lexeme_grow(acc: state.lexeme, chunk: close_lex), done: true, failed: false } @@ -217,7 +226,7 @@ fn lex_delimited_step(close_r: LexMatchThunk, body_r: LexMatchThunk, state: Deli body_r: body_r, state: DelimitedState { remaining: after_body, - lexeme: list_append(left: state.lexeme, right: body_lex), + lexeme: lexeme_grow(acc: state.lexeme, chunk: body_lex), done: false, failed: false } diff --git a/src/v2/compiler/02_parse.dag b/src/v2/compiler/02_parse.dag index 722901f7db4..202ce29f2f8 100644 --- a/src/v2/compiler/02_parse.dag +++ b/src/v2/compiler/02_parse.dag @@ -99,7 +99,6 @@ import v2.std.optional { } import std.algebra { Cons, Empty, FreeSemigroup } import v2.std.algebra { HeadAbsent, HeadFound, TailAbsent, TailFound, any, contains, filter, fold_list, fold_list_right, is_empty, length, list_append, list_head, list_reverse, list_snoc_item, list_tail, unique } -import v2.std.text { Char, CharAbsent, CharFound, string_head } import v2.std.witness { Holds, Violates, Witness, witness_from_optional } import std.cache_identity { ArtifactKindId, CacheInterfaceId } import std.realization { Materialization } @@ -275,6 +274,7 @@ type ParseTableRealization { nullable_set: List first_rows: Map> productions_by_name: Map + prepared_exprs: Map } type ParseTable = ParseTableRealization @@ -535,7 +535,8 @@ fn parse_table_carry_analysis(table: ParseTable, entries: Map Hash { ) } +// Route through chars(s:): the emitted realization needs the boundary conversion — emitted +// fold_list cannot infer T from a host-carrier lexeme (E0282) without it — and the +// interpreted chars builtin grounds every string realization (native compact string and +// Empty/Cons chain alike, via free_monoid_to_string) to the same Int codepoints fold_list +// itself would read, so the digest is realization-stable on both routes. fn parse_lexeme_digest(lexeme: Lexeme) -> Hash { fold_list( - xs: lexeme, + xs: chars(s: lexeme), empty: symbol_identity_digest(sym: ^parse_lexeme_digest_nil), cons: fn(acc, ch) { combine_hash(a: acc, b: byte_limb_hash_peano_digest(limb: ch)) @@ -641,7 +647,8 @@ fn parse_table_for_production_analyzed( memo_lookup_calls: 0, nullable_set: analysis.nullable_set, first_rows: analysis.first_rows, - productions_by_name: grammar_production_index(productions: validated.root.productions) + productions_by_name: grammar_production_index(productions: validated.root.productions), + prepared_exprs: analysis.prepared_exprs } } @@ -669,7 +676,8 @@ fn parse_table_empty() -> ParseTable { memo_lookup_calls: 0, nullable_set: std.algebra.Empty, first_rows: empty_map(), - productions_by_name: empty_map() + productions_by_name: empty_map(), + prepared_exprs: empty_map() } } @@ -756,11 +764,13 @@ fn parse_mint_from_source( // list whose consumers read as "parse stamped this", which is the fabricated answer. Empty is the // answer to a different question than OccurrenceSynthetic's -- nothing was minted at all there -- // and both are correctly empty for this one. -fn parse_minted_id_from_node(n: Node) -> List { +// Snoc-built, never append-per-node: list_append(acc, [id]) re-folds the accumulator per +// element (O(n^2) per captured repeat), list_snoc_item is the primitive push (O(log n)). +fn parse_minted_id_snoc(acc: List, n: Node) -> List { match n.occurrence_id { - OccurrenceMinted { id: id } => Cons { head: id, tail: std.algebra.Empty } - OccurrenceSynthetic => std.algebra.Empty - OccurrenceProjected { id: _, caused_by: _ } => std.algebra.Empty + OccurrenceMinted { id: id } => list_snoc_item(xs: acc, item: id) + OccurrenceSynthetic => acc + OccurrenceProjected { id: _, caused_by: _ } => acc } } @@ -768,9 +778,7 @@ fn parse_minted_id_list(nodes: List) -> List { fold_list( xs: nodes, empty: std.algebra.Empty, - cons: fn(acc, n) { - list_append(left: acc, right: parse_minted_id_from_node(n: n)) - } + cons: fn(acc, n) { parse_minted_id_snoc(acc: acc, n: n) } ) } @@ -938,7 +946,7 @@ fn parse_flatten_diag_batches(batches: List>) -> List Bool { +fn parse_char_is_arm_pattern_lead(c: Int) -> Bool { (c == 95) || ((c >= 65) && (c <= 90)) } @@ -997,27 +1005,25 @@ fn parse_looks_like_match_arm_start(tokens: List) -> Bool { match parse_token_first(tokens: tokens) { Absent => false Present { value: tok0 } => - match string_head(s: tok0.lexeme) { - CharFound { value: c } => - if parse_char_is_arm_pattern_lead(c: c) { - match parse_token_second(tokens: tokens) { - Present { value: tok1 } => - if parse_token_lexeme_is(tok: tok1, lit: "=>") { - true - } else if parse_token_lexeme_is(tok: tok1, lit: "{") { - parse_scan_fat_arrow_after_lbrace( - tokens: parse_tokens_drop(tokens: tokens, count: 2), - depth: 1 - ) - } else { - false - } - Absent => false + if string_length(s: tok0.lexeme) == 0 { + false + } else if parse_char_is_arm_pattern_lead(c: code_point(c: char_at(s: tok0.lexeme, pos: 0))) { + match parse_token_second(tokens: tokens) { + Present { value: tok1 } => + if parse_token_lexeme_is(tok: tok1, lit: "=>") { + true + } else if parse_token_lexeme_is(tok: tok1, lit: "{") { + parse_scan_fat_arrow_after_lbrace( + tokens: parse_tokens_drop(tokens: tokens, count: 2), + depth: 1 + ) + } else { + false } - } else { - false - } - CharAbsent => false + Absent => false + } + } else { + false } } } @@ -1173,8 +1179,8 @@ fn parse_nonterminal_memoized_core( } Violates { diagnostic: _ } => let table_miss = parse_table_record_miss(table: table_at_lookup) - let inner = parse_expr( - expr: production.expression, + let inner = parse_expr_with_first( + expr: parse_table_prepared_expr(table: table_miss, production: production), tokens: tokens, all_tokens: all_tokens, root: root, @@ -1308,6 +1314,7 @@ type GrammarExprFirstFold { type GrammarFirstAnalysis { nullable_set: List first_rows: Map> + prepared_exprs: Map } fn grammar_empty_terminal_list() -> List { @@ -1506,6 +1513,100 @@ fn choice_branches_ambiguous(left: GrammarExprFirstFold, right: GrammarExprFirst } } +// A prepared grammar expression is the parse-time carrier for FIRST knowledge: the grammar is +// fixed for a whole parse, so each Choice node's branch folds (and its ambiguity verdict) are +// pure functions of the grammar and are computed ONCE here, at preparation, bottom-up — every +// node's fold combines its children's already-computed folds in O(union) instead of re-walking +// the subtree. The pre-hoisting dispatch recomputed expr_first_fold on both branches at every +// Choice node at every token position: right-nested choice chains made that O(k^2) per position +// with O(t^2) union constants, measured as the dominant self-hosted parse cost (2026-09-09, +// required-v2-native lane work: 209s -> 33s on a 315KB table module from the algebra carrier +// fixes, then 33s -> 14s from this hoist, forecast by a pointer-keyed memo probe). +type PreparedGrammarExpr + = PreparedTerminal { token_class: Symbol, stamp: TerminalStampMode } + | PreparedNonterminal { production: Symbol } + | PreparedSequence { left: PreparedGrammarExpr, right: PreparedGrammarExpr } + | PreparedChoice { + left: PreparedGrammarExpr, + right: PreparedGrammarExpr, + left_first: GrammarExprFirstFold, + right_first: GrammarExprFirstFold, + ambiguous: Bool + } + | PreparedOptional { element: PreparedGrammarExpr } + | PreparedRepeat { element: PreparedGrammarExpr } + +type PreparedExprFold { + tree: PreparedGrammarExpr + first: GrammarExprFirstFold +} + +fn prepare_grammar_expr( + expr: GrammarExpr, + nullable_set: List, + first_rows: Map> +) -> PreparedExprFold { + match expr { + Terminal { token_class: token_class, stamp: stamp } => + PreparedExprFold { + tree: PreparedTerminal { token_class: token_class, stamp: stamp }, + first: expr_first_fold_terminal(token_class: token_class) + } + Nonterminal { production: production } => + PreparedExprFold { + tree: PreparedNonterminal { production: production }, + first: expr_first_fold_nonterminal(name: production, nullable_set: nullable_set, first_rows: first_rows) + } + Sequence { left: left, right: right } => + let left_prepared = prepare_grammar_expr(expr: left, nullable_set: nullable_set, first_rows: first_rows) + let right_prepared = prepare_grammar_expr(expr: right, nullable_set: nullable_set, first_rows: first_rows) + PreparedExprFold { + tree: PreparedSequence { left: left_prepared.tree, right: right_prepared.tree }, + first: expr_first_fold_sequence(left: left_prepared.first, right: right_prepared.first) + } + Choice { left: left, right: right } => + let left_prepared = prepare_grammar_expr(expr: left, nullable_set: nullable_set, first_rows: first_rows) + let right_prepared = prepare_grammar_expr(expr: right, nullable_set: nullable_set, first_rows: first_rows) + PreparedExprFold { + tree: PreparedChoice { + left: left_prepared.tree, + right: right_prepared.tree, + left_first: left_prepared.first, + right_first: right_prepared.first, + ambiguous: choice_branches_ambiguous(left: left_prepared.first, right: right_prepared.first) + }, + first: expr_first_fold_choice(left: left_prepared.first, right: right_prepared.first) + } + Optional { element: element } => + let element_prepared = prepare_grammar_expr(expr: element, nullable_set: nullable_set, first_rows: first_rows) + PreparedExprFold { + tree: PreparedOptional { element: element_prepared.tree }, + first: expr_first_fold_optional(element: element_prepared.first) + } + Repeat { element: element } => + let element_prepared = prepare_grammar_expr(expr: element, nullable_set: nullable_set, first_rows: first_rows) + PreparedExprFold { + tree: PreparedRepeat { element: element_prepared.tree }, + first: expr_first_fold_repeat(element: element_prepared.first) + } + } +} + +// The on-the-fly arm for tables whose analysis did not carry prepared trees (parse_table_empty +// and test-built tables): identical folds, computed at call time. Tables built by +// parse_table_for_production_analyzed always resolve Present here — both maps are folded from +// the same root.productions, so Absent-after-Holds is unreachable by construction, and this arm +// computes the same value rather than widening anything. +fn parse_table_prepared_expr(table: ParseTable, production: GrammarProduction) -> PreparedGrammarExpr { + match v2.std.collection.map_get(m: table.prepared_exprs, key: production.name) { + Accepted { value: Present { value: prepared }, diagnostics: _ } => prepared + Accepted { value: Absent, diagnostics: _ } => + prepare_grammar_expr(expr: production.expression, nullable_set: table.nullable_set, first_rows: table.first_rows).tree + Rejected { diagnostics: _ } => + prepare_grammar_expr(expr: production.expression, nullable_set: table.nullable_set, first_rows: table.first_rows).tree + } +} + fn append_choice_ambiguity_row( acc: List, production: Symbol, @@ -1598,9 +1699,21 @@ fn compute_grammar_first_analysis_given( root: GrammarRoot, nullable_set: List ) -> GrammarFirstAnalysis { + let first_rows = compute_production_first_rows(root: root, nullable_set: nullable_set) GrammarFirstAnalysis { nullable_set: nullable_set, - first_rows: compute_production_first_rows(root: root, nullable_set: nullable_set) + first_rows: first_rows, + prepared_exprs: fold_list( + xs: root.productions, + empty: empty_map(), + cons: fn(acc, p) { + map_insert( + m: acc, + key: p.name, + value: prepare_grammar_expr(expr: p.expression, nullable_set: nullable_set, first_rows: first_rows).tree + ) + } + ) } } @@ -2144,8 +2257,8 @@ fn parse_expr_terminal( } fn parse_expr_sequence( - left: GrammarExpr, - right: GrammarExpr, + left: PreparedGrammarExpr, + right: PreparedGrammarExpr, tokens: List, all_tokens: List, root: GrammarRoot, @@ -2187,8 +2300,8 @@ fn parse_expr_sequence( } fn parse_choice_residue_backtrack( - left: GrammarExpr, - right: GrammarExpr, + left: PreparedGrammarExpr, + right: PreparedGrammarExpr, tokens: List, all_tokens: List, root: GrammarRoot, @@ -2218,8 +2331,8 @@ fn parse_choice_residue_backtrack( } fn parse_choice_on_present_token( - left: GrammarExpr, - right: GrammarExpr, + left: PreparedGrammarExpr, + right: PreparedGrammarExpr, tok: Token, tokens: List, all_tokens: List, @@ -2253,17 +2366,18 @@ fn parse_choice_on_present_token( } fn parse_choice_first_dispatch( - left: GrammarExpr, - right: GrammarExpr, + left: PreparedGrammarExpr, + right: PreparedGrammarExpr, + left_first: GrammarExprFirstFold, + right_first: GrammarExprFirstFold, + ambiguous: Bool, tokens: List, all_tokens: List, root: GrammarRoot, table: ParseTable, prov: ParseProvenanceState ) -> ParseExprResult { - let left_first = expr_first_fold(expr: left, nullable_set: table.nullable_set, first_rows: table.first_rows) - let right_first = expr_first_fold(expr: right, nullable_set: table.nullable_set, first_rows: table.first_rows) - if choice_branches_ambiguous(left: left_first, right: right_first) { + if ambiguous { parse_choice_residue_backtrack( left: left, right: right, @@ -2301,7 +2415,7 @@ fn parse_choice_first_dispatch( } fn parse_expr_optional( - element: GrammarExpr, + element: PreparedGrammarExpr, tokens: List, all_tokens: List, root: GrammarRoot, @@ -2330,7 +2444,7 @@ fn parse_expr_optional( } fn parse_expr_repeat_step( - element: GrammarExpr, + element: PreparedGrammarExpr, all_tokens: List, root: GrammarRoot, acc: ParseRepeatAcc @@ -2386,7 +2500,7 @@ fn parse_expr_repeat_step( } fn parse_expr_repeat( - element: GrammarExpr, + element: PreparedGrammarExpr, tokens: List, all_tokens: List, root: GrammarRoot, @@ -2413,7 +2527,7 @@ fn parse_expr_repeat( } fn parse_expr_with_first( - expr: GrammarExpr, + expr: PreparedGrammarExpr, tokens: List, all_tokens: List, root: GrammarRoot, @@ -2421,7 +2535,7 @@ fn parse_expr_with_first( prov: ParseProvenanceState ) -> ParseExprResult { match expr { - Terminal { token_class: token_class, stamp: stamp } => + PreparedTerminal { token_class: token_class, stamp: stamp } => parse_expr_terminal( token_class: token_class, stamp: stamp, @@ -2429,7 +2543,7 @@ fn parse_expr_with_first( table: table, prov: prov ) - Nonterminal { production: prod_name } => + PreparedNonterminal { production: prod_name } => parse_nonterminal_memoized( all_tokens: all_tokens, tokens: tokens, @@ -2438,7 +2552,7 @@ fn parse_expr_with_first( prod_name: prod_name, prov: prov ) - Sequence { left: left, right: right } => + PreparedSequence { left: left, right: right } => parse_expr_sequence( left: left, right: right, @@ -2448,17 +2562,20 @@ fn parse_expr_with_first( table: table, prov: prov ) - Choice { left: left, right: right } => + PreparedChoice { left: left, right: right, left_first: left_first, right_first: right_first, ambiguous: ambiguous } => parse_choice_first_dispatch( left: left, right: right, + left_first: left_first, + right_first: right_first, + ambiguous: ambiguous, tokens: tokens, all_tokens: all_tokens, root: root, table: table, prov: prov ) - Optional { element: element } => + PreparedOptional { element: element } => parse_expr_optional( element: element, tokens: tokens, @@ -2467,7 +2584,7 @@ fn parse_expr_with_first( table: table, prov: prov ) - Repeat { element: element } => + PreparedRepeat { element: element } => parse_expr_repeat( element: element, tokens: tokens, @@ -2479,6 +2596,9 @@ fn parse_expr_with_first( } } +// Compat entry for callers holding a raw GrammarExpr (grammar tests, stall probes): prepares +// the expression on the fly from the table's carried analysis — the same folds the production +// path reads from its prepared map — then dispatches on the prepared tree. fn parse_expr( expr: GrammarExpr, tokens: List, @@ -2488,7 +2608,7 @@ fn parse_expr( prov: ParseProvenanceState ) -> ParseExprResult { parse_expr_with_first( - expr: expr, + expr: prepare_grammar_expr(expr: expr, nullable_set: table.nullable_set, first_rows: table.first_rows).tree, tokens: tokens, all_tokens: all_tokens, root: root, diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index e1b650f4508..dd6a8fd3c68 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -1472,6 +1472,8 @@ fn body_lower_reduce_emitted(folded: Node, emitted: Symbol) -> Outcome { body_lower_reduce_passthrough(folded: folded) } else if emitted == ^dag_surface_fn_decl { body_lower_reduce_or_keep(o: body_lower_fn_decl_to_arrow(shell: folded), folded: folded) + } else if emitted == ^dag_surface_test_fn_decl { + body_lower_reduce_or_keep(o: body_lower_test_fn_decl_to_member(shell: folded), folded: folded) } else { outcome_accepted(value: folded) } @@ -1801,6 +1803,40 @@ fn body_lower_fn_decl_named_member_wrap(arrow: Node, fn_name: Symbol) -> Node { ) } +// A `test fn` declaration lowers to the SAME named-member arrow a plain `fn` lowers to: the +// production is the contextual `sequence(ident("test"), fn_decl)` (see dag_grammar_test_fn_decl_expr +// for why `test` is not a reserved keyword), the fold has already lowered the nested fn_decl child +// by the time this shell reduces, and this arm lifts the member out of the sequence's right +// projection after checking the marker lexeme really is "test". A non-"test" marker is a typed +// refusal, never a retained wrapper. The TEST-NESS marker is deliberately not reified into the +// member node: the native route's universe derivation is the floor's own source-scan authority +// (v2.workflow.floor_discovery discover_floor_rows_for_source), which reads the surface text, and +// resolve/infer see an ordinary function — a stated divergence, not a silent one. +fn body_lower_test_fn_decl_to_member(shell: Node) -> Outcome { + match body_lower_unwrap_captured(shell: shell) { + Absent => body_lower_wrapper_retained_shell(shell: shell) + Present { value: captured } => + match body_lower_seq_child(node: captured, name: ^grammar_sequence_left_node_projection) { + Absent => body_lower_wrapper_retained_shell(shell: shell) + Present { value: marker_node } => + match dag_binding_branded_atom_identity(node: marker_node) { + Present { value: marker } => + if marker == ^test { + match body_lower_seq_child(node: captured, name: ^grammar_sequence_right_node_projection) { + Absent => body_lower_wrapper_retained_shell(shell: shell) + Present { value: member } => outcome_accepted(value: member) + } + } else { + outcome_rejected( + d: body_lower_diagnostic(reason: ^body_lowering_reason_test_marker_not_test, n: marker_node) + ) + } + Absent => body_lower_wrapper_retained_shell(shell: shell) + } + } + } +} + fn body_lower_fn_decl_to_arrow(shell: Node) -> Outcome { match body_lower_unwrap_captured(shell: shell) { Absent => body_lower_wrapper_retained_shell(shell: shell) @@ -4304,6 +4340,8 @@ fn body_lower_fn_body_shell(shell: Node) -> Outcome { fn body_lower_forward_row_apply(shell: Node, emitted: Symbol) -> Outcome { if emitted == ^dag_surface_fn_decl { body_lower_fn_decl_to_arrow(shell: shell) + } else if emitted == ^dag_surface_test_fn_decl { + body_lower_test_fn_decl_to_member(shell: shell) } else if emitted == ^dag_surface_fn_body { body_lower_fn_body_shell(shell: shell) } else if emitted == ^dag_surface_match_expr { diff --git a/src/v2/compiler/body_producer_forward.dag b/src/v2/compiler/body_producer_forward.dag index 96597a88c99..1273adca916 100644 --- a/src/v2/compiler/body_producer_forward.dag +++ b/src/v2/compiler/body_producer_forward.dag @@ -10,6 +10,10 @@ data body_producer_forward_row_fn_decl: BodyProducerForwardRow = BodyProducerFor surface_identity: ^dag_surface_fn_decl } +data body_producer_forward_row_test_fn_decl: BodyProducerForwardRow = BodyProducerForwardRow { + surface_identity: ^dag_surface_test_fn_decl +} + data body_producer_forward_row_fn_body: BodyProducerForwardRow = BodyProducerForwardRow { surface_identity: ^dag_surface_fn_body } @@ -31,25 +35,15 @@ data body_producer_forward_row_loop_expr: BodyProducerForwardRow = BodyProducerF } fn body_producer_forward_rows() -> List { - Cons { - head: body_producer_forward_row_fn_decl, - tail: Cons { - head: body_producer_forward_row_fn_body, - tail: Cons { - head: body_producer_forward_row_match_expr, - tail: Cons { - head: body_producer_forward_row_if_expr, - tail: Cons { - head: body_producer_forward_row_let_expr, - tail: Cons { - head: body_producer_forward_row_loop_expr, - tail: std.algebra.Empty - } - } - } - } - } - } + [ + body_producer_forward_row_fn_decl, + body_producer_forward_row_test_fn_decl, + body_producer_forward_row_fn_body, + body_producer_forward_row_match_expr, + body_producer_forward_row_if_expr, + body_producer_forward_row_let_expr, + body_producer_forward_row_loop_expr + ] } fn body_producer_forward_row_count() -> Int { diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 7a6a37a4c0f..0d75c0bc222 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -2930,6 +2930,20 @@ fn dag_grammar_fn_decl_expr() -> GrammarExpr { ) } +// `test fn` IS A CONTEXTUAL PRODUCTION, NOT A KEYWORD. `test` stays an ordinary identifier +// everywhere else — dag/extdeps/languages/typescript/program.dag models the TypeScript +// compiler's own `Cond { test: ... }` field under §3's real-upstream-names rule, and a reserved +// kw_test would break that module's parse. So the production matches an ident lexeme followed by +// a fn_decl, and the body-lowering arm refuses any marker lexeme other than "test" with a typed +// diagnostic. No other top-level item starts with an ident, so the choice arm has no FIRST +// overlap with the keyword-led alternatives. +fn dag_grammar_test_fn_decl_expr() -> GrammarExpr { + dag_grammar_sequence( + left: dag_grammar_terminal_lexeme(token_class: ^dag_token_ident), + right: dag_grammar_nonterminal(production: ^dag_production_fn_decl) + ) +} + fn dag_grammar_top_level_item_expr() -> GrammarExpr { dag_grammar_choice( left: dag_grammar_nonterminal(production: ^dag_production_import_decl), @@ -2939,7 +2953,10 @@ fn dag_grammar_top_level_item_expr() -> GrammarExpr { left: dag_grammar_nonterminal(production: ^dag_production_type_decl), right: dag_grammar_choice( left: dag_grammar_nonterminal(production: ^dag_production_data_decl), - right: dag_grammar_nonterminal(production: ^dag_production_fn_decl) + right: dag_grammar_choice( + left: dag_grammar_nonterminal(production: ^dag_production_fn_decl), + right: dag_grammar_nonterminal(production: ^dag_production_test_fn_decl) + ) ) ) ) @@ -3187,6 +3204,11 @@ fn dag_grammar_root() -> GrammarRoot { expression: dag_grammar_fn_decl_expr(), emitted: ^dag_surface_fn_decl ) + let test_fn_decl = dag_grammar_production( + name: ^dag_production_test_fn_decl, + expression: dag_grammar_test_fn_decl_expr(), + emitted: ^dag_surface_test_fn_decl + ) let qualified_name = dag_grammar_production( name: ^dag_production_qualified_name, expression: dag_grammar_qualified_name_expr(), @@ -3348,6 +3370,7 @@ fn dag_grammar_root() -> GrammarRoot { type_decl, data_decl, fn_decl, + test_fn_decl, qualified_name, generic_params, param_list, diff --git a/src/v2/native_lane_fixture/control.dag b/src/v2/native_lane_fixture/control.dag new file mode 100644 index 00000000000..65827fd7463 --- /dev/null +++ b/src/v2/native_lane_fixture/control.dag @@ -0,0 +1,29 @@ +module v2.native_lane_fixture.control + +import v2.std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +// THE required-v2-native LANE'S LIVE-VERDICT CONTROL PAIR (authority: +// gunbc.witness_v2_native_route). These two declarations exist so the lane can prove its +// verdict path is LIVE rather than green-by-construction: a route that cannot distinguish +// `false` from `true` must never report a pass. The lane's host harness names both identities +// explicitly — they are controls, never universe members: the module sits outside the +// `v2.test.` prefix the universe derivation filters on, and the declarations are plain `fn` +// so floor discovery enrolls no rows for them (the floor's required gate would — correctly — +// redden on a discovered test that returns false). +// +// THE FALSE CONTROL IS THE LOAD-BEARING ONE. The lane's admission refuses unless the emitted +// native binary observes `native_lane_false_control` returning FALSE (NativeTestReturnedFalse): +// a native route that reports it Passed has a dead or diverted verdict arm, and every pass +// verdict in the population is then worthless. The true control is the positive half of the +// same probe: the route must also observe TRUE where true is declared, so a route that +// refuses or falsifies everything cannot green the control either. +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +fn native_lane_false_control() -> Bool { + false +} + +fn native_lane_true_control() -> Bool { + true +} diff --git a/src/v2/std/algebra.dag b/src/v2/std/algebra.dag index d5e0ea80066..4e039dfe9e7 100644 --- a/src/v2/std/algebra.dag +++ b/src/v2/std/algebra.dag @@ -623,8 +623,12 @@ fn non_empty(xs: FreeMonoid) -> Bool { } } +// `count` is the primitive authority for carrier size (O(1) on the persistent-vector +// realization); the fold this replaced was O(n) per call, and the parse repeat loop calls +// length on the remaining-token list twice per element — an O(elements x tokens) quadratic +// measured at 40% of self-hosted parse self-time (2026-09-09, required-v2-native lane work). fn length(xs: FreeMonoid) -> Int { - fold_list(xs: xs, empty: 0, cons: fn(count, _) { count + 1 }) + xs |> count } fn for_all(xs: FreeMonoid, predicate: fn(T) -> Bool) -> Bool { @@ -690,8 +694,12 @@ fn list_append(left: FreeMonoid, right: FreeMonoid) -> FreeMonoid { }) } +// `list_push` is the primitive single-item append (amortized O(log n) on the persistent +// carrier); routing through list_append paid a full O(n) right-fold per snoc, which made +// every build-by-appending accumulator quadratic — measured on the first-set union fold in +// the self-hosted parser (2026-09-09, required-v2-native lane work). fn list_snoc_item(xs: FreeMonoid, item: T) -> FreeMonoid { - list_append(left: xs, right: Cons { head: item, tail: Empty }) + xs |> list_push(item) } type ListPrefixState { diff --git a/src/v2/test/claim/compile_door_ledger_ownership_test.dag b/src/v2/test/claim/compile_door_ledger_ownership_test.dag index 35b3fce85a8..85aad1ddf77 100644 --- a/src/v2/test/claim/compile_door_ledger_ownership_test.dag +++ b/src/v2/test/claim/compile_door_ledger_ownership_test.dag @@ -1,5 +1,11 @@ module v2.test.compile_door_ledger_ownership +import v2.workflow.compile_door_ledger { + cause_ownership_lookup, HeadGrain, ThisLaneCalibration, SharedSelfHostCriticalPath, MigrationOwned, + affected_ledger_from_diff, AffectedDiffRefused, AffectedLedgered, ledger_refused_count +} +import v2.workflow.floor_diff_observe { NameStatusDiffFail, NameStatusDiffOk } + // Fast pure receipt (no door, no live tree) for the flip-trigger table: every frontier cause the // histogram receipts pin is attributed to its owning lane, and an unknown cause is fail-closed // UNATTRIBUTED (not silently bucketed). This is the DESIGN 5 tell that the burn-down table stays @@ -8,7 +14,7 @@ module v2.test.compile_door_ledger_ownership // witness in v2.test.long.compile_door_ledger, which pins the counts; this pins the ATTRIBUTION. fn lane_ok_this_lane(cause: Symbol) -> Bool { - match cause_ownership_lookup(cause: cause) { + match cause_ownership_lookup(cause: cause, grain: HeadGrain) { Present { value: row } => match row.lane { ThisLaneCalibration => true @@ -20,7 +26,7 @@ fn lane_ok_this_lane(cause: Symbol) -> Bool { } fn lane_ok_shared(cause: Symbol) -> Bool { - match cause_ownership_lookup(cause: cause) { + match cause_ownership_lookup(cause: cause, grain: HeadGrain) { Present { value: row } => match row.lane { ThisLaneCalibration => false @@ -32,7 +38,7 @@ fn lane_ok_shared(cause: Symbol) -> Bool { } fn lane_ok_migration(cause: Symbol) -> Bool { - match cause_ownership_lookup(cause: cause) { + match cause_ownership_lookup(cause: cause, grain: HeadGrain) { Present { value: row } => match row.lane { ThisLaneCalibration => false diff --git a/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag b/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag index 030da0b4291..cfe912717a8 100644 --- a/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag +++ b/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag @@ -25,7 +25,8 @@ fn parse_table_recompute_fixture() -> ParseTable { memo_lookup_calls: base.memo_lookup_calls, nullable_set: base.nullable_set, first_rows: base.first_rows, - productions_by_name: base.productions_by_name + productions_by_name: base.productions_by_name, + prepared_exprs: base.prepared_exprs } } @@ -43,7 +44,8 @@ fn parse_table_memoize_fixture() -> ParseTable { memo_lookup_calls: base.memo_lookup_calls, nullable_set: base.nullable_set, first_rows: base.first_rows, - productions_by_name: base.productions_by_name + productions_by_name: base.productions_by_name, + prepared_exprs: base.prepared_exprs } } @@ -119,7 +121,8 @@ fn witness_bypass_store_reds_non_memoize_carrier() -> Bool { memo_lookup_calls: base.memo_lookup_calls, nullable_set: base.nullable_set, first_rows: base.first_rows, - productions_by_name: base.productions_by_name + productions_by_name: base.productions_by_name, + prepared_exprs: base.prepared_exprs } !parse_table_memo_store_well_formed_at(table: bypass, key: key) } diff --git a/src/v2/test/claim/parse/parse_table_content_key_test.dag b/src/v2/test/claim/parse/parse_table_content_key_test.dag index c1da1af71ba..533779f20ca 100644 --- a/src/v2/test/claim/parse/parse_table_content_key_test.dag +++ b/src/v2/test/claim/parse/parse_table_content_key_test.dag @@ -16,7 +16,8 @@ fn mk_table(grammar: Hash, tokens: Hash) -> ParseTable { memo_lookup_calls: base.memo_lookup_calls, nullable_set: base.nullable_set, first_rows: base.first_rows, - productions_by_name: base.productions_by_name + productions_by_name: base.productions_by_name, + prepared_exprs: base.prepared_exprs } } diff --git a/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag b/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag index 3ce289703a9..431927c5e14 100644 --- a/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag +++ b/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag @@ -29,7 +29,7 @@ fn wave1_gate1_a1_domain_construction_frontier_holds() -> Bool { } fn wave1_gate1_a1_forward_behavior_row_count_holds() -> Bool { - body_lowering_forward_behavior_row_count() == 6 + body_lowering_forward_behavior_row_count() == 7 } fn wave1_gate1_a1_forward_fold_surface_identities_agree_holds() -> Bool { diff --git a/src/v2/test/native_decl_selection_test.dag b/src/v2/test/native_decl_selection_test.dag new file mode 100644 index 00000000000..084ebd52f5b --- /dev/null +++ b/src/v2/test/native_decl_selection_test.dag @@ -0,0 +1,120 @@ +module v2.test.native_decl_selection + +import v2.compiler.compile { + native_test_context_from_ingest, + native_test_eval_one, + native_test_prepare_module, + NativeTestObservation, + NativeTestPassed, + NativeTestPreparedModule, + NativeTestRefused, + NativeTestStageEntry +} +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import extdeps.communication.medium { Lossless, Medium } + +// Selection controls for the native driver's exact scope walk: a test identity names a top-level +// declaration of the prepared module, so a same-named NESTED edge — the graft spine segment +// v2.test.collision itself — must never be selected and evaluated under the requested identity. +// The synthetic module declares no top-level collision; its only collision-named edge is the +// spine segment (plus a record field, which infer erases). + +data collision_module_source: String = "module v2.test.collision\n\nimport v2.std.logic { Bool }\n\nimport v2.std.integer { Int }\n\ntest fn real_test() -> Bool { true }\n\ntype Probe { collision: Int }\n" + +data collision_sibling_source: String = "module v2.test.collision_sibling\n\nimport v2.std.logic { Bool }\n\ntest fn shared_name() -> Bool { true }\n" + +fn collision_read() -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: collision_module_source, fidelity: Lossless }, + artifact: Artifact { + kind: SourceFile, + id: ^native_decl_selection_collision_read, + file_path: "src/v2/test/fixture/native_decl_selection/collision_test.dag" + }, + compilation_unit: ^native_decl_selection_collision_cu, + source_root: DagTree + } +} + +fn collision_sibling_read() -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: collision_sibling_source, fidelity: Lossless }, + artifact: Artifact { + kind: SourceFile, + id: ^native_decl_selection_sibling_read, + file_path: "src/v2/test/fixture/native_decl_selection/collision_sibling_test.dag" + }, + compilation_unit: ^native_decl_selection_sibling_cu, + source_root: DagTree + } +} + +fn collision_prepared() -> Optional { + match native_test_context_from_ingest(ingest: [collision_read(), collision_sibling_read()]) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: context, diagnostics: _ } => + match native_test_prepare_module(context: context, module: [^v2, ^test, ^collision]) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: prepared, diagnostics: _ } => optional_present(value: prepared) + } + } +} + +fn observation_is_entry_not_found(observation: NativeTestObservation) -> Bool { + match observation.verdict { + NativeTestRefused { stage: stage, reason: reason } => + match stage { + NativeTestStageEntry => reason == ^native_test_entry_declaration_not_found + _ => false + } + _ => false + } +} + +test fn the_requested_declaration_is_selected_and_evaluated() -> Bool { + match collision_prepared() { + Absent => false + Present { value: prepared } => + let observation = native_test_eval_one(prepared: prepared, declaration: ^real_test) + match observation.verdict { + NativeTestPassed => true + _ => false + } + } +} + +test fn a_nested_same_named_graft_edge_is_not_selected() -> Bool { + match collision_prepared() { + Absent => false + Present { value: prepared } => + observation_is_entry_not_found( + observation: native_test_eval_one(prepared: prepared, declaration: ^collision) + ) + } +} + +test fn an_absent_declaration_is_not_found() -> Bool { + match collision_prepared() { + Absent => false + Present { value: prepared } => + observation_is_entry_not_found( + observation: native_test_eval_one(prepared: prepared, declaration: ^missing) + ) + } +} + +test fn a_sibling_modules_declaration_is_not_selected() -> Bool { + match collision_prepared() { + Absent => false + Present { value: prepared } => + observation_is_entry_not_found( + observation: native_test_eval_one(prepared: prepared, declaration: ^shared_name) + ) + } +} diff --git a/src/v2/test/native_lane_smoke_test.dag b/src/v2/test/native_lane_smoke_test.dag new file mode 100644 index 00000000000..7c72ec98e1d --- /dev/null +++ b/src/v2/test/native_lane_smoke_test.dag @@ -0,0 +1,23 @@ +module v2.test.native_lane_smoke + +import v2.std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +// THE required-v2-native LANE'S POSITIVE-POPULATION FLOOR MEMBER (authority: +// gunbc.witness_v2_native_route, clause native_route_positive_population_present). A universe +// in which nothing passes proves the route dead, and exclusion attribution alone cannot see +// that — a route that refuses every member can still be fully owned. So the census requires +// one member to reach NativeTestPassed where the floor expects pass. The body is deliberately +// the grounded fragment only: a Bool literal, the shape the lane's live-verdict control pair +// (v2.native_lane_fixture.control) already proves reaches a verdict — every richer body today +// meets the node grounding frontier (v2.compiler.infer node_grounding_frontier_note) at the +// eval gate, measured 2026-09-10 at 261 members across 110 modules. This member is the +// route's canary, not a stand-in for the frontier: the census's typed exclusion counts carry +// the frontier's size, and as grounding derivation covers more node kinds the positive +// population grows into real tests. It does not retire — at every frontier depth, a route +// that stops reaching verdicts reddens here first. +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +test fn native_route_positive_population_smoke() -> Bool { + true +} diff --git a/src/v2/test/v2_native_route_test.dag b/src/v2/test/v2_native_route_test.dag new file mode 100644 index 00000000000..d4b5cbe7bdd --- /dev/null +++ b/src/v2/test/v2_native_route_test.dag @@ -0,0 +1,1087 @@ +module v2.test.v2_native_route + +import gunbc.witness_v2_native_route { + NativeRouteTestIdentity, native_route_identity_qualified, native_route_identities_unique, + native_route_identity_member, + native_route_universe_member, + NativeRouteReference, FloorExpectedPass, FloorExpectedRed, FloorRouteGapRefusal, + native_route_reference_for, + NativeRouteMemberRow, + NativeRouteModuleSource, + native_route_required_pass_identities, + NativeRouteExclusionCause, CompilerFrontierAttributed, NativeEntryResolutionLimit, + NativeEvalBoundary, UnattributedRefusal, classify_native_refusal, + NativeRouteDisposition, NativeAgreementPass, NativeAgreementExpectedRed, + NativeAgreementRouteGap, NativeExclusion, NativeDivergence, + NativeRouteDivergenceCause, NativePassedExpectedRed, NativeFalseExpectedPass, + NativeReturnedOtherVerdict, NativeReachedRouteGap, + native_route_disposition, + NativeRouteCensus, native_route_census, + NativeRouteMalformedControl, MalformedSpecimenRefused, MalformedSpecimenAccepted, + NativeRouteOldRouteControl, OldRouteWithdrawn, OldRouteFalsifierArmed, OldRouteControlAbsent, + NativeRouteReceipt, + NativeRouteAdmission, NativeRouteAdmitted, NativeRouteRefused, + required_v2_native_job_name, + native_route_admission, native_route_admitted, native_route_admission_refused_with, + native_route_admission_summary +} +import v2.compiler.compile { + NativeTestVerdict, NativeTestPassed, NativeTestReturnedFalse, NativeTestReturnedOther, + NativeTestRefused, + NativeTestStage, NativeTestStageContext, NativeTestStagePrepare, NativeTestStageEntry, + NativeTestStageEval, + NativeTestFileRefusal +} +import v2.std.optional { Absent, Optional, Present } +import v2.std.collection { List } +import v2.std.algebra { length } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.integer { Int } + +// ── synthetic receipt constructors ───────────────────────────────────────────────────────── +// +// The admission fold reads the REAL floor rosters (floor_expected_red_roster, +// floor_route_gap_roster) from inside native_route_admission, so the synthetic identities here +// are chosen OFF every roster: "v2.test.*" names no real module carries. The disposition, +// reference and census functions take their rosters as arguments, so their tests are fully +// synthetic. + +fn test_identity(module: String, declaration: String) -> NativeRouteTestIdentity { + NativeRouteTestIdentity { module: module, declaration: declaration } +} + +fn passed_row(module: String, declaration: String) -> NativeRouteMemberRow { + NativeRouteMemberRow { + identity: test_identity(module: module, declaration: declaration), + verdict: NativeTestPassed + } +} + +fn false_row(module: String, declaration: String) -> NativeRouteMemberRow { + NativeRouteMemberRow { + identity: test_identity(module: module, declaration: declaration), + verdict: NativeTestReturnedFalse + } +} + +fn refused_row( + module: String, + declaration: String, + stage: NativeTestStage, + reason: Symbol +) -> NativeRouteMemberRow { + NativeRouteMemberRow { + identity: test_identity(module: module, declaration: declaration), + verdict: NativeTestRefused { stage: stage, reason: reason } + } +} + +fn file_refusal_row(path: Symbol, head: Symbol, fatal: Symbol) -> NativeTestFileRefusal { + NativeTestFileRefusal { path: path, head_reason: head, fatal_reason: fatal } +} + +// A receipt with every well-formedness field recorded and both live controls observed; the +// universe, module-source index, population and file-refusal populations are the arguments +// under test. +fn receipt_over( + universe: List, + module_source_index: List, + population: List, + file_refusals: List +) -> NativeRouteReceipt { + NativeRouteReceipt { + job: required_v2_native_job_name, + tested_tree: "tree-sha", + preparation_seed_identity: "seed-sha", + emitted_closure_identity: "closure-sha", + executable_path: "/emitted/compiler", + executable_identity: "binary-sha", + universe: universe, + module_source_index: module_source_index, + population: population, + file_refusals: file_refusals, + false_control: Present { value: NativeTestReturnedFalse }, + true_control: Present { value: NativeTestPassed }, + malformed_control: MalformedSpecimenRefused { + path: "target/v2-native-lane/malformed-control-root/poison.dag", + reason: ^tokenize_lex_e1_unterminated_string + }, + old_route_control: OldRouteWithdrawn { withdrawn_executable: "/seed/gunbc" } + } +} + +// THE RATCHET'S ONE ROSTERED IDENTITY, AS A SYNTHETIC ROW. The required-pass roster is real +// authority data, so every synthetic receipt that aims at admission must carry the smoke +// member passing — the clause reads the roster, not the test's intent. +fn smoke_identity() -> NativeRouteTestIdentity { + test_identity( + module: "v2.test.native_lane_smoke", + declaration: "native_route_positive_population_smoke" + ) +} + +fn smoke_source_row() -> NativeRouteModuleSource { + NativeRouteModuleSource { + module: "v2.test.native_lane_smoke", + path: ^src_v2_test_native_lane_smoke_test_dag + } +} + +// The smallest green receipt: one passing member beside the rostered smoke member, universe +// and population equal, and the module-source index covering both universe modules. +fn green_receipt() -> NativeRouteReceipt { + receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + smoke_identity() + ], + module_source_index: [ + NativeRouteModuleSource { module: "v2.test.alpha", path: ^alpha_path }, + smoke_source_row() + ], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + passed_row( + module: "v2.test.native_lane_smoke", + declaration: "native_route_positive_population_smoke" + ) + ], + file_refusals: [] + ) +} + +// ── the universe is a prefix ─────────────────────────────────────────────────────────────── + +test fn the_universe_is_the_prefix_and_nothing_else() -> Bool { + native_route_universe_member(module_qn: "v2.test.v2_native_route") + && native_route_universe_member(module_qn: "v2.test.") + && !native_route_universe_member(module_qn: "v2.compiler.compile") + && !native_route_universe_member(module_qn: "v2.test") + && !native_route_universe_member(module_qn: "dag.test.claim") +} + +test fn identity_qualification_derives_the_dotted_spelling() -> Bool { + native_route_identity_qualified( + identity: test_identity(module: "v2.test.alpha", declaration: "passes") + ) == "v2.test.alpha.passes" +} + +test fn duplicate_identities_are_detected_at_identity_grain() -> Bool { + let dup = test_identity(module: "v2.test.alpha", declaration: "passes") + !native_route_identities_unique(xs: [ + dup, + test_identity(module: "v2.test.beta", declaration: "passes"), + dup + ]) + && native_route_identities_unique(xs: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.alpha", declaration: "fails"), + test_identity(module: "v2.test.beta", declaration: "passes") + ]) +} + +// ── the reference is the floor's rosters joined at the identity ──────────────────────────── + +test fn the_reference_is_the_floor_rosters_joined_at_the_identity() -> Bool { + match native_route_reference_for(qualified: "v2.test.alpha.red", expected_red: ["v2.test.alpha.red"], route_gap: []) { + FloorExpectedRed => true + FloorExpectedPass => false + FloorRouteGapRefusal => false + } +} + +test fn the_reference_defaults_to_the_floors_standing_expectation() -> Bool { + match native_route_reference_for(qualified: "v2.test.alpha.green", expected_red: [], route_gap: []) { + FloorExpectedPass => true + FloorExpectedRed => false + FloorRouteGapRefusal => false + } +} + +test fn route_gap_membership_wins_over_expected_red() -> Bool { + match native_route_reference_for( + qualified: "v2.test.alpha.gapped", + expected_red: ["v2.test.alpha.gapped"], + route_gap: ["v2.test.alpha.gapped"] + ) { + FloorRouteGapRefusal => true + FloorExpectedPass => false + FloorExpectedRed => false + } +} + +// ── classification is stage- and provenance-keyed ────────────────────────────────────────── + +test fn an_entry_stage_refusal_is_a_driver_limit_only_at_a_known_reason() -> Bool { + match classify_native_refusal(stage: NativeTestStageEntry, reason: ^native_test_entry_declaration_not_arrow) { + NativeEntryResolutionLimit { reason: r } => r == ^native_test_entry_declaration_not_arrow + CompilerFrontierAttributed { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: _ } => false + } + && match classify_native_refusal(stage: NativeTestStageEntry, reason: ^parse_g0_tokens_remain) { + NativeEntryResolutionLimit { reason: _ } => false + CompilerFrontierAttributed { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: r } => r == ^parse_g0_tokens_remain + } +} + +test fn an_eval_stage_refusal_is_the_boundary_only_at_a_boundary_reason() -> Bool { + match classify_native_refusal(stage: NativeTestStageEval, reason: ^eval_rejected_effect_io_unbound) { + NativeEvalBoundary { reason: r } => r == ^eval_rejected_effect_io_unbound + CompilerFrontierAttributed { reason: _ } => false + NativeEntryResolutionLimit { reason: _ } => false + UnattributedRefusal { reason: _ } => false + } + && match classify_native_refusal(stage: NativeTestStageEval, reason: ^eval_some_unowned_reason) { + NativeEvalBoundary { reason: _ } => false + CompilerFrontierAttributed { reason: _ } => false + NativeEntryResolutionLimit { reason: _ } => false + UnattributedRefusal { reason: r } => r == ^eval_some_unowned_reason + } +} + +test fn a_context_stage_refusal_is_frontier_attributed_only_when_the_ledger_owns_it() -> Bool { + match classify_native_refusal(stage: NativeTestStageContext, reason: ^parse_g0_tokens_remain) { + CompilerFrontierAttributed { reason: r } => r == ^parse_g0_tokens_remain + NativeEntryResolutionLimit { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: _ } => false + } + && match classify_native_refusal(stage: NativeTestStageContext, reason: ^some_unowned_cause) { + CompilerFrontierAttributed { reason: _ } => false + NativeEntryResolutionLimit { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: r } => r == ^some_unowned_cause + } +} + +test fn a_prepare_stage_refusal_is_frontier_attributed_only_when_the_ledger_owns_it() -> Bool { + match classify_native_refusal(stage: NativeTestStagePrepare, reason: ^parse_g0_tokens_remain) { + CompilerFrontierAttributed { reason: r } => r == ^parse_g0_tokens_remain + NativeEntryResolutionLimit { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: _ } => false + } + && match classify_native_refusal(stage: NativeTestStagePrepare, reason: ^some_unowned_cause) { + CompilerFrontierAttributed { reason: _ } => false + NativeEntryResolutionLimit { reason: _ } => false + NativeEvalBoundary { reason: _ } => false + UnattributedRefusal { reason: r } => r == ^some_unowned_cause + } +} + +// ── the disposition matrix ───────────────────────────────────────────────────────────────── + +test fn a_pass_agrees_with_the_standing_expectation() -> Bool { + match native_route_disposition(verdict: NativeTestPassed, reference: FloorExpectedPass) { + NativeAgreementPass => true + NativeAgreementExpectedRed => false + NativeAgreementRouteGap => false + NativeExclusion { cause: _ } => false + NativeDivergence { cause: _ } => false + } +} + +test fn a_pass_against_an_expected_red_identity_is_a_divergence() -> Bool { + match native_route_disposition(verdict: NativeTestPassed, reference: FloorExpectedRed) { + NativeDivergence { cause: c } => + match c { + NativePassedExpectedRed => true + NativeFalseExpectedPass => false + NativeReturnedOtherVerdict => false + NativeReachedRouteGap => false + } + NativeAgreementPass => false + NativeAgreementExpectedRed => false + NativeAgreementRouteGap => false + NativeExclusion { cause: _ } => false + } +} + +test fn a_false_against_an_expected_red_identity_is_the_expected_red_observed() -> Bool { + match native_route_disposition(verdict: NativeTestReturnedFalse, reference: FloorExpectedRed) { + NativeAgreementExpectedRed => true + NativeAgreementPass => false + NativeAgreementRouteGap => false + NativeExclusion { cause: _ } => false + NativeDivergence { cause: _ } => false + } +} + +test fn a_false_against_the_standing_expectation_is_a_divergence() -> Bool { + match native_route_disposition(verdict: NativeTestReturnedFalse, reference: FloorExpectedPass) { + NativeDivergence { cause: c } => + match c { + NativeFalseExpectedPass => true + NativePassedExpectedRed => false + NativeReturnedOtherVerdict => false + NativeReachedRouteGap => false + } + NativeAgreementPass => false + NativeAgreementExpectedRed => false + NativeAgreementRouteGap => false + NativeExclusion { cause: _ } => false + } +} + +test fn a_non_boolean_return_is_a_divergence_on_every_reference() -> Bool { + match native_route_disposition(verdict: NativeTestReturnedOther, reference: FloorExpectedPass) { + NativeDivergence { cause: NativeReturnedOtherVerdict } => true + _ => false + } + && match native_route_disposition(verdict: NativeTestReturnedOther, reference: FloorExpectedRed) { + NativeDivergence { cause: NativeReturnedOtherVerdict } => true + _ => false + } + && match native_route_disposition(verdict: NativeTestReturnedOther, reference: FloorRouteGapRefusal) { + NativeDivergence { cause: NativeReturnedOtherVerdict } => true + _ => false + } +} + +test fn a_reached_verdict_on_a_route_gap_identity_is_a_divergence() -> Bool { + match native_route_disposition(verdict: NativeTestPassed, reference: FloorRouteGapRefusal) { + NativeDivergence { cause: NativeReachedRouteGap } => true + _ => false + } + && match native_route_disposition(verdict: NativeTestReturnedFalse, reference: FloorRouteGapRefusal) { + NativeDivergence { cause: NativeReachedRouteGap } => true + _ => false + } +} + +test fn the_route_gap_agrees_only_with_the_hermetic_boundary_refusal() -> Bool { + match native_route_disposition( + verdict: NativeTestRefused { stage: NativeTestStageEval, reason: ^eval_rejected_effect_io_unbound }, + reference: FloorRouteGapRefusal + ) { + NativeAgreementRouteGap => true + _ => false + } +} + +test fn a_route_gap_refusal_off_the_boundary_is_an_exclusion() -> Bool { + match native_route_disposition( + verdict: NativeTestRefused { stage: NativeTestStageEval, reason: ^some_unowned_cause }, + reference: FloorRouteGapRefusal + ) { + NativeExclusion { cause: UnattributedRefusal { reason: r } } => r == ^some_unowned_cause + _ => false + } + && match native_route_disposition( + verdict: NativeTestRefused { stage: NativeTestStagePrepare, reason: ^parse_g0_tokens_remain }, + reference: FloorRouteGapRefusal + ) { + NativeExclusion { cause: CompilerFrontierAttributed { reason: r } } => r == ^parse_g0_tokens_remain + _ => false + } +} + +// ── the census ───────────────────────────────────────────────────────────────────────────── + +test fn the_census_counts_every_disposition() -> Bool { + let census = native_route_census( + rows: [ + passed_row(module: "v2.test.a", declaration: "one"), + false_row(module: "v2.test.b", declaration: "two"), + refused_row(module: "v2.test.c", declaration: "three", stage: NativeTestStageEval, reason: ^eval_rejected_effect_io_unbound), + refused_row(module: "v2.test.d", declaration: "four", stage: NativeTestStagePrepare, reason: ^parse_g0_tokens_remain), + refused_row(module: "v2.test.e", declaration: "five", stage: NativeTestStageEntry, reason: ^native_test_entry_declaration_not_arrow), + refused_row(module: "v2.test.f", declaration: "six", stage: NativeTestStageEval, reason: ^eval_rejected_effect_io_unbound), + refused_row(module: "v2.test.g", declaration: "seven", stage: NativeTestStageContext, reason: ^some_unowned_cause), + passed_row(module: "v2.test.h", declaration: "eight") + ], + expected_red: ["v2.test.b.two", "v2.test.h.eight"], + route_gap: ["v2.test.c.three"] + ) + census.agreement_pass == 1 + && census.agreement_expected_red == 1 + && census.agreement_route_gap == 1 + && census.excluded_compiler_frontier == 1 + && census.excluded_entry_limit == 1 + && census.excluded_eval_boundary == 1 + && census.excluded_unattributed == 1 + && census.divergences == 1 +} + +// ── admission: the green shape ───────────────────────────────────────────────────────────── + +test fn a_green_receipt_is_admitted() -> Bool { + native_route_admitted(admission: native_route_admission(receipt: green_receipt())) +} + +// ── admission: well-formedness clauses ───────────────────────────────────────────────────── + +test fn the_wrong_job_name_is_refused() -> Bool { + let receipt = NativeRouteReceipt { + job: "required-v2-native-but-wrong", + tested_tree: green_receipt().tested_tree, + preparation_seed_identity: green_receipt().preparation_seed_identity, + emitted_closure_identity: green_receipt().emitted_closure_identity, + executable_path: green_receipt().executable_path, + executable_identity: green_receipt().executable_identity, + universe: green_receipt().universe, + module_source_index: green_receipt().module_source_index, + population: green_receipt().population, + file_refusals: green_receipt().file_refusals, + false_control: green_receipt().false_control, + true_control: green_receipt().true_control, + malformed_control: green_receipt().malformed_control, + old_route_control: green_receipt().old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "job_name_mismatch" + ) +} + +test fn an_unrecorded_tested_tree_is_refused() -> Bool { + let receipt = NativeRouteReceipt { + job: green_receipt().job, + tested_tree: "", + preparation_seed_identity: green_receipt().preparation_seed_identity, + emitted_closure_identity: green_receipt().emitted_closure_identity, + executable_path: green_receipt().executable_path, + executable_identity: green_receipt().executable_identity, + universe: green_receipt().universe, + module_source_index: green_receipt().module_source_index, + population: green_receipt().population, + file_refusals: green_receipt().file_refusals, + false_control: green_receipt().false_control, + true_control: green_receipt().true_control, + malformed_control: green_receipt().malformed_control, + old_route_control: green_receipt().old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "tested_tree_unrecorded" + ) +} + +test fn an_unrecorded_preparation_seed_is_refused() -> Bool { + let receipt = NativeRouteReceipt { + job: green_receipt().job, + tested_tree: green_receipt().tested_tree, + preparation_seed_identity: "", + emitted_closure_identity: green_receipt().emitted_closure_identity, + executable_path: green_receipt().executable_path, + executable_identity: green_receipt().executable_identity, + universe: green_receipt().universe, + module_source_index: green_receipt().module_source_index, + population: green_receipt().population, + file_refusals: green_receipt().file_refusals, + false_control: green_receipt().false_control, + true_control: green_receipt().true_control, + malformed_control: green_receipt().malformed_control, + old_route_control: green_receipt().old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "preparation_seed_unrecorded" + ) +} + +test fn an_unrecorded_emitted_closure_is_refused() -> Bool { + let receipt = NativeRouteReceipt { + job: green_receipt().job, + tested_tree: green_receipt().tested_tree, + preparation_seed_identity: green_receipt().preparation_seed_identity, + emitted_closure_identity: "", + executable_path: green_receipt().executable_path, + executable_identity: green_receipt().executable_identity, + universe: green_receipt().universe, + module_source_index: green_receipt().module_source_index, + population: green_receipt().population, + file_refusals: green_receipt().file_refusals, + false_control: green_receipt().false_control, + true_control: green_receipt().true_control, + malformed_control: green_receipt().malformed_control, + old_route_control: green_receipt().old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "emitted_closure_unrecorded" + ) +} + +test fn an_unrecorded_route_identity_is_refused() -> Bool { + let base = green_receipt() + let no_path = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: "", + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + let no_identity = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: "", + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: no_path), + cause_name: "route_identity_unrecorded" + ) + && native_route_admission_refused_with( + admission: native_route_admission(receipt: no_identity), + cause_name: "route_identity_unrecorded" + ) +} + +// ── admission: the universe shape ────────────────────────────────────────────────────────── + +test fn an_empty_universe_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over(universe: [], module_source_index: [], population: [], file_refusals: [])), + cause_name: "universe_underived" + ) +} + +test fn a_duplicated_universe_is_refused() -> Bool { + let member = test_identity(module: "v2.test.alpha", declaration: "passes") + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [member, member], + module_source_index: [], + population: [passed_row(module: "v2.test.alpha", declaration: "passes")], + file_refusals: [] + )), + cause_name: "universe_duplicates_present" + ) +} + +test fn a_foreign_universe_member_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.compiler.compile", declaration: "not_a_test")], + module_source_index: [], + population: [passed_row(module: "v2.compiler.compile", declaration: "not_a_test")], + file_refusals: [] + )), + cause_name: "universe_prefix_violations_present" + ) +} + +// ── admission: the exact join ────────────────────────────────────────────────────────────── + +test fn a_duplicated_population_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "passes")], + module_source_index: [], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + passed_row(module: "v2.test.alpha", declaration: "passes") + ], + file_refusals: [] + )), + cause_name: "population_duplicates_present" + ) +} + +test fn a_foreign_population_row_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "passes")], + module_source_index: [], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + passed_row(module: "v2.test.beta", declaration: "extra") + ], + file_refusals: [] + )), + cause_name: "population_foreign_rows_present" + ) +} + +test fn an_omitted_population_row_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "missing") + ], + module_source_index: [], + population: [passed_row(module: "v2.test.alpha", declaration: "passes")], + file_refusals: [] + )), + cause_name: "population_omissions_present" + ) +} + +// ── admission: the population must prove the route ───────────────────────────────────────── + +test fn an_all_refused_population_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "blocked")], + module_source_index: [], + population: [refused_row( + module: "v2.test.alpha", + declaration: "blocked", + stage: NativeTestStagePrepare, + reason: ^parse_g0_tokens_remain + )], + file_refusals: [] + )), + cause_name: "positive_population_empty" + ) +} + +// ── admission: the required-native ratchet ──────────────────────────────────────────────── + +// The rostered smoke identity, observed as an ATTRIBUTED exclusion — the exact collapse the +// ratchet exists to block: every clause about classification greens, and coverage still lost +// the identity. +test fn a_regressed_required_native_pass_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [smoke_identity()], + module_source_index: [smoke_source_row()], + population: [refused_row( + module: "v2.test.native_lane_smoke", + declaration: "native_route_positive_population_smoke", + stage: NativeTestStagePrepare, + reason: ^parse_g0_tokens_remain + )], + file_refusals: [] + )), + cause_name: "required_native_pass_regressed" + ) +} + +// The smoke identity is not in the universe at all — coverage shrank silently. The omission +// clause cannot fire (the universe itself no longer names the member), so the ratchet is the +// only wall, and it reads the roster against the OBSERVED population. +test fn an_omitted_required_native_pass_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "passes")], + module_source_index: [], + population: [passed_row(module: "v2.test.alpha", declaration: "passes")], + file_refusals: [] + )), + cause_name: "required_native_pass_regressed" + ) +} + +test fn the_ratchet_roster_is_the_smoke_member_alone_today() -> Bool { + length(xs: native_route_required_pass_identities) == 1 + && native_route_identity_member(target: smoke_identity(), xs: native_route_required_pass_identities) +} + +test fn a_divergence_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "lies")], + module_source_index: [], + population: [false_row(module: "v2.test.alpha", declaration: "lies")], + file_refusals: [] + )), + cause_name: "divergences_present" + ) +} + +test fn an_unattributed_exclusion_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "blocked") + ], + module_source_index: [], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + refused_row( + module: "v2.test.beta", + declaration: "blocked", + stage: NativeTestStagePrepare, + reason: ^some_unowned_cause + ) + ], + file_refusals: [] + )), + cause_name: "unattributed_exclusions_present" + ) +} + +// ── admission: file-refusal ownership at both grains ─────────────────────────────────────── + +test fn an_unattributed_file_refusal_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "passes")], + module_source_index: [], + population: [passed_row(module: "v2.test.alpha", declaration: "passes")], + file_refusals: [file_refusal_row( + path: ^some_path, + head: ^parse_grammar_choice_overlap_residue, + fatal: ^some_unowned_cause + )] + )), + cause_name: "unattributed_file_refusals_present" + ) +} + +test fn an_unattributed_head_advisory_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [test_identity(module: "v2.test.alpha", declaration: "passes")], + module_source_index: [], + population: [passed_row(module: "v2.test.alpha", declaration: "passes")], + file_refusals: [file_refusal_row( + path: ^some_path, + head: ^some_unowned_advisory, + fatal: ^parse_g0_tokens_remain + )] + )), + cause_name: "unattributed_head_advisories_present" + ) +} + +// ── admission: context refusals are bound to their observation ───────────────────────────── + +test fn a_context_refusal_without_its_file_refusal_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "blocked") + ], + module_source_index: [ + NativeRouteModuleSource { module: "v2.test.beta", path: ^beta_path } + ], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + refused_row( + module: "v2.test.beta", + declaration: "blocked", + stage: NativeTestStageContext, + reason: ^parse_g0_tokens_remain + ) + ], + file_refusals: [] + )), + cause_name: "context_refusals_unbacked" + ) +} + +// THE REASON-ONLY JOIN IS THE HOLE THIS CLAUSE CLOSED. The file refusal below carries the +// Context row's exact fatal reason, but at ANOTHER MODULE'S path — under the pre-repair clause +// (any file anywhere with the reason) this receipt was admitted. +test fn a_context_refusal_backed_by_another_modules_file_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "blocked") + ], + module_source_index: [ + NativeRouteModuleSource { module: "v2.test.alpha", path: ^alpha_path }, + NativeRouteModuleSource { module: "v2.test.beta", path: ^beta_path } + ], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + refused_row( + module: "v2.test.beta", + declaration: "blocked", + stage: NativeTestStageContext, + reason: ^parse_g0_tokens_remain + ) + ], + file_refusals: [file_refusal_row( + path: ^alpha_path, + head: ^parse_grammar_choice_overlap_residue, + fatal: ^parse_g0_tokens_remain + )] + )), + cause_name: "context_refusals_unbacked" + ) +} + +// THE PATH WITHOUT THE REASON IS ALSO NOT THE OBSERVATION: the row's module resolves to the +// refusal's path, but the file refused for a different cause than the row carries. +test fn a_context_refusal_at_its_own_path_with_the_wrong_reason_is_refused() -> Bool { + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "blocked") + ], + module_source_index: [ + NativeRouteModuleSource { module: "v2.test.beta", path: ^beta_path } + ], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + refused_row( + module: "v2.test.beta", + declaration: "blocked", + stage: NativeTestStageContext, + reason: ^parse_g0_tokens_remain + ) + ], + file_refusals: [file_refusal_row( + path: ^beta_path, + head: ^parse_grammar_choice_overlap_residue, + fatal: ^resolve_module_not_found + )] + )), + cause_name: "context_refusals_unbacked" + ) +} + +test fn a_context_refusal_backed_by_its_file_refusal_is_not_refused_on_that_clause() -> Bool { + let admission = native_route_admission(receipt: receipt_over( + universe: [ + test_identity(module: "v2.test.alpha", declaration: "passes"), + test_identity(module: "v2.test.beta", declaration: "blocked"), + smoke_identity() + ], + module_source_index: [ + NativeRouteModuleSource { module: "v2.test.alpha", path: ^alpha_path }, + NativeRouteModuleSource { module: "v2.test.beta", path: ^beta_path }, + smoke_source_row() + ], + population: [ + passed_row(module: "v2.test.alpha", declaration: "passes"), + refused_row( + module: "v2.test.beta", + declaration: "blocked", + stage: NativeTestStageContext, + reason: ^parse_g0_tokens_remain + ), + passed_row( + module: "v2.test.native_lane_smoke", + declaration: "native_route_positive_population_smoke" + ) + ], + file_refusals: [file_refusal_row( + path: ^beta_path, + head: ^parse_grammar_choice_overlap_residue, + fatal: ^parse_g0_tokens_remain + )] + )) + !native_route_admission_refused_with(admission: admission, cause_name: "context_refusals_unbacked") + && native_route_admitted(admission: admission) +} + +// ── admission: the controls ──────────────────────────────────────────────────────────────── + +test fn a_missing_false_control_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: Absent, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "false_control_not_observed" + ) +} + +test fn a_passed_false_control_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: Present { value: NativeTestPassed }, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "false_control_not_observed" + ) +} + +test fn a_missing_true_control_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: Absent, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "true_control_not_observed" + ) +} + +test fn a_falsified_true_control_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: Present { value: NativeTestReturnedFalse }, + malformed_control: base.malformed_control, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "true_control_not_observed" + ) +} + +test fn an_accepted_malformed_specimen_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: base.true_control, + malformed_control: MalformedSpecimenAccepted, + old_route_control: base.old_route_control + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "malformed_control_accepted_by_route" + ) +} + +test fn an_absent_old_route_control_is_refused() -> Bool { + let base = green_receipt() + let receipt = NativeRouteReceipt { + job: base.job, + tested_tree: base.tested_tree, + preparation_seed_identity: base.preparation_seed_identity, + emitted_closure_identity: base.emitted_closure_identity, + executable_path: base.executable_path, + executable_identity: base.executable_identity, + universe: base.universe, + module_source_index: base.module_source_index, + population: base.population, + file_refusals: base.file_refusals, + false_control: base.false_control, + true_control: base.true_control, + malformed_control: base.malformed_control, + old_route_control: OldRouteControlAbsent + } + native_route_admission_refused_with( + admission: native_route_admission(receipt: receipt), + cause_name: "old_route_control_never_installed" + ) +} + +// ── admission: the fold collects every violated clause ───────────────────────────────────── + +test fn one_red_run_names_every_violated_clause() -> Bool { + let admission = native_route_admission(receipt: NativeRouteReceipt { + job: "wrong-job", + tested_tree: "", + preparation_seed_identity: "", + emitted_closure_identity: "", + executable_path: "", + executable_identity: "", + universe: [], + module_source_index: [], + population: [], + file_refusals: [], + false_control: Absent, + true_control: Absent, + malformed_control: MalformedSpecimenAccepted, + old_route_control: OldRouteControlAbsent + }) + native_route_admission_refused_with(admission: admission, cause_name: "job_name_mismatch") + && native_route_admission_refused_with(admission: admission, cause_name: "tested_tree_unrecorded") + && native_route_admission_refused_with(admission: admission, cause_name: "preparation_seed_unrecorded") + && native_route_admission_refused_with(admission: admission, cause_name: "emitted_closure_unrecorded") + && native_route_admission_refused_with(admission: admission, cause_name: "route_identity_unrecorded") + && native_route_admission_refused_with(admission: admission, cause_name: "universe_underived") + && native_route_admission_refused_with(admission: admission, cause_name: "positive_population_empty") + && native_route_admission_refused_with(admission: admission, cause_name: "required_native_pass_regressed") + && native_route_admission_refused_with(admission: admission, cause_name: "false_control_not_observed") + && native_route_admission_refused_with(admission: admission, cause_name: "true_control_not_observed") + && native_route_admission_refused_with(admission: admission, cause_name: "malformed_control_accepted_by_route") + && native_route_admission_refused_with(admission: admission, cause_name: "old_route_control_never_installed") +} + +test fn the_admission_summary_names_every_violated_clause() -> Bool { + native_route_admission_summary(admission: native_route_admission(receipt: NativeRouteReceipt { + job: "wrong-job", + tested_tree: "", + preparation_seed_identity: "", + emitted_closure_identity: "", + executable_path: "", + executable_identity: "", + universe: [], + module_source_index: [], + population: [], + file_refusals: [], + false_control: Absent, + true_control: Absent, + malformed_control: MalformedSpecimenAccepted, + old_route_control: OldRouteControlAbsent + })) == "refused: job_name_mismatch, tested_tree_unrecorded, preparation_seed_unrecorded, emitted_closure_unrecorded, route_identity_unrecorded, universe_underived, positive_population_empty, required_native_pass_regressed, false_control_not_observed, true_control_not_observed, malformed_control_accepted_by_route, old_route_control_never_installed" +} diff --git a/src/v2/workflow/compile_door_ledger.dag b/src/v2/workflow/compile_door_ledger.dag index ef3174e95d8..5771814cd8b 100644 --- a/src/v2/workflow/compile_door_ledger.dag +++ b/src/v2/workflow/compile_door_ledger.dag @@ -322,6 +322,22 @@ fn ledger_all_compiled(rows: List) -> Bool { // the door's fail-closed rejection IS compilation failure and this ledger becomes its stopped-line // audit. +// THE KEY CARRIES THE GRAIN (review on #10882, 2026-09-09: a head-grain authority must not become +// a general bag of reasons). A rejection is a LIST with two observed positions: the HEAD — what +// this ledger's own DoorRefused rows carry, where rejected_with_pending prepends non-fatal +// advisories — and the FATAL — the last diagnostic, the cause that actually refused. The same +// symbol can be owned at both grains (parse_grammar_choice_overlap_residue is the native route's +// head advisory on 876 modules and never its fatal; parse_g0_tokens_remain is the fatal under it +// and never surfaces at head), and ownership at one grain says NOTHING about the other — the +// grains are different observations of the same frontier class. cause_is_attributed keeps its +// head-grain contract (this ledger's rows are head-grain by construction); the fatal-grain lookup +// is the native route's instrument (gunbc.witness_v2_native_route), derived from the SAME table — +// one ownership authority, two grains, neither instrument re-enumerates the other. + +type DiagnosticGrain + = HeadGrain + | FatalGrain + type CauseLane = ThisLaneCalibration | SharedSelfHostCriticalPath @@ -329,30 +345,138 @@ type CauseLane type CauseOwnership { cause: Symbol + grain: DiagnosticGrain lane: CauseLane flip_trigger: String } +// ROW GROUPS, BY GRAIN. The first four rows are this ledger's own head-grain observations. The +// tokenize_lex_e1_unrecognized_char HEAD row is the native route's head-grain observation +// (gunbc.witness_v2_native_route joins this table at head grain): the lexer-coverage cause +// surfaces at HEAD on the native route when it is the first diagnostic, even where a later fatal +// follows — owned here so the native head join has no unowned advisory; the fatal-grain row of +// the same cause is the position that refuses. The FATAL-grain rows were measured 2026-09-09 and +// 2026-09-10 by the gunbc.witness_v2_native_route census as the LAST diagnostic — the cause that +// actually refused — through the emitted native binary, at the per-row scope each flip_trigger +// states. rejected_with_pending prepends the non-fatal overlap residue, so the fatal-grain causes +// never surface at the head grain the door ledger's own rows count; their ownership lives here at +// the grain they are observed at, and the native-route census classifies against exactly those +// rows. + data known_frontier_causes: List = [ CauseOwnership { cause: ^fact_density_hollow_alias_locus, + grain: HeadGrain, lane: ThisLaneCalibration, flip_trigger: "fact_density subtree roster corpus-calibrated (smart-eagle burn-down); flips to Blocking when the count zeroes — calibration must never become relaxing the lens" }, CauseOwnership { cause: ^resolve_reason_unbound_symbol, + grain: HeadGrain, lane: SharedSelfHostCriticalPath, flip_trigger: "resolve grows generic type-param binding (sharp-bee std/logic thread); flips to Blocking when the count zeroes" }, CauseOwnership { cause: ^resolve_ambiguous_export, + grain: HeadGrain, lane: MigrationOwned, flip_trigger: "no per-cause trigger — dissolves by construction at the namespace-only containment flip (admit_named_exports flat-merge replaced by position-distinct containment; nimble-boar-198)" }, CauseOwnership { cause: ^parse_grammar_choice_overlap_residue, + grain: HeadGrain, lane: MigrationOwned, flip_trigger: "no per-cause trigger — dissolves by construction at phase-3 import-grammar deletion / grammar-surgery (nimble-boar-198)" + }, + CauseOwnership { + cause: ^tokenize_lex_e1_unrecognized_char, + grain: HeadGrain, + lane: MigrationOwned, + flip_trigger: "no per-cause trigger — lexer coverage is the same grammar-surgery class (nimble-boar-198). Measured 2026-09-09 at HEAD grain by the gunbc.witness_v2_native_route census (6 modules, the annotation-channel and comment-wall syntax among them)" + }, + CauseOwnership { + cause: ^parse_g0_tokens_remain, + grain: FatalGrain, + lane: MigrationOwned, + flip_trigger: "no per-cause trigger — same grammar-surgery class as parse_grammar_choice_overlap_residue (nimble-boar-198). Measured 2026-09-09 by the gunbc.witness_v2_native_route census as the fatal refusal on 706 real v2.test.* modules through the emitted native binary" + }, + CauseOwnership { + cause: ^tokenize_lex_e1_unrecognized_char, + grain: FatalGrain, + lane: MigrationOwned, + flip_trigger: "no per-cause trigger — lexer coverage is the same grammar-surgery class (nimble-boar-198). Measured 2026-09-09 at fatal grain by the gunbc.witness_v2_native_route census (6 modules)" + }, + CauseOwnership { + cause: ^namespace_graft_body_dissolved_refused, + grain: FatalGrain, + lane: MigrationOwned, + flip_trigger: "no per-cause trigger — the graft is namespace machinery and dissolves with it (nimble-boar-198). Measured 2026-09-09 at fatal grain by the gunbc.witness_v2_native_route census (41 modules)" + }, + CauseOwnership { + cause: ^normalize_reason_post_normalize_not_well_formed, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "normalize stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-09 at fatal grain by the gunbc.witness_v2_native_route census (6 modules)" + }, + CauseOwnership { + cause: ^normalized_tree_reason_wrapper_retention_not_normalized, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "normalize stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-09 at fatal grain by the gunbc.witness_v2_native_route census (2 modules)" + }, + CauseOwnership { + cause: ^body_lowering_reason_match_arm_navigation_refused, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "body-lowering stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-09 at fatal grain by the gunbc.witness_v2_native_route census (2 modules)" + }, + CauseOwnership { + cause: ^body_lowering_reason_statement_precedes_without_binding, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "body-lowering stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (71 files: 40 under v2.test.* homes, 31 in dag/extdeps and dag/gunbc)" + }, + CauseOwnership { + cause: ^body_lowering_reason_if_navigation_refused, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "body-lowering stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (5 files, all under v2.test.* homes)" + }, + CauseOwnership { + cause: ^body_lowering_reason_let_body_not_reachable_from_node, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "body-lowering stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (3 files, all under v2.test.* homes)" + }, + CauseOwnership { + cause: ^body_lowering_reason_statement_spine_headless, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "body-lowering stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (1 file: dag/gunbc/discovery_census.dag)" + }, + CauseOwnership { + cause: ^resolve_reason_unbound_symbol, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "resolve stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (2059 v2.test.* members across 417 modules, dominantly the parse frontier's import cascade: the member's own file parses while a file in its import closure refuses, so the imported names resolve to nothing — zeroing rides the parse-frontier rows beside resolve's own binding gaps, the head-grain row's thread)" + }, + CauseOwnership { + cause: ^resolve_reason_ambiguous_symbol, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "resolve stage frontier on the self-host critical path; flips to Blocking when the count zeroes. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (29 v2.test.* members across 6 modules)" + }, + CauseOwnership { + cause: ^infer_grounding_not_derived, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "the node grounding frontier (v2.compiler.infer node_grounding_frontier_note) observed at prepare, where the carried diagnostic is fatal rather than eval-gated; flips to Blocking when the count zeroes, which the frontier note's dissolve-on conditions own. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (2 v2.test.* members, both in v2.test.manual.recompute_trace_probe)" + }, + CauseOwnership { + cause: ^eval_rejected_grounding_not_derived, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "eval's grounding gate refuses a body whose inferred facts carry no DerivedGrounding — the node grounding frontier (v2.compiler.infer node_grounding_frontier_note); flips to Blocking when the count zeroes, which the frontier note's dissolve-on conditions own. Measured 2026-09-10 at fatal grain by the gunbc.witness_v2_native_route census over the lane's source roots dag + src/v2 (261 v2.test.* members across 110 modules)" } ] @@ -376,12 +500,20 @@ fn list_find_first(xs: FreeMonoid, predicate: fn(T) -> Bool) -> Optional Optional { - list_find_first(xs: known_frontier_causes, predicate: fn(row) { row.cause == cause }) +fn cause_ownership_lookup(cause: Symbol, grain: DiagnosticGrain) -> Optional { + list_find_first( + xs: known_frontier_causes, + predicate: fn(row) { + row.cause == cause && discriminant(v: row.grain) == discriminant(v: grain) + } + ) } +// This ledger's own contract stays HEAD-grain: its DoorRefused rows carry the rejection's head +// reason, so attribution for the door histogram asks the head-grain question. The fatal-grain +// question is the native route's to ask (gunbc.witness_v2_native_route), through the same lookup. fn cause_is_attributed(cause: Symbol) -> Bool { - match cause_ownership_lookup(cause: cause) { + match cause_ownership_lookup(cause: cause, grain: HeadGrain) { Present { value: _ } => true Absent => false }