diff --git a/dag/gunbc/floor_component_receipt.dag b/dag/gunbc/floor_component_receipt.dag index d7775e066c2..d0a17a15abc 100644 --- a/dag/gunbc/floor_component_receipt.dag +++ b/dag/gunbc/floor_component_receipt.dag @@ -55,6 +55,7 @@ import extdeps.languages.json.emit { json_object, json_kv, } +import gunbc.selection_degradation_receipt { selection_degradation_receipt_json } data floor_component_receipt_note: String = "The floor's per-COMPONENT receipt, machine-readable, written on every exit path including a red one. WHY IT EXISTS, priced: .github/workflows/falsifier-alert.yml read only the overall workflow conclusion and hardcoded one causal story for every failure — 'affected-set cold control is dark, per-PR selection UNVERIFIED, Gate-1 receipts do not execute'. On run 30509024051 all three clauses were FALSE: the predict-only affected-set control concluded Done over 5217 witnesses and the expect_red batches produced their receipts; the single failing component was the silent-pick gate, killed at its whole-receipt wall budget. An overall-red composite that erases the green state of its completed children is a state-space conflation (DESIGN section 5), and it misdirects triage every time it fires. This module is the projection that makes the alert read state instead of inferring it. THE RECORD IS NOT NEW: each component is one batch-grain Concluded std.observation.ObservationEvent — the same authority gunbc.witness_row_cost names as THE record ('not a TSV schema') — so Refused stays distinct from Failed and a budget kill is TimedOut with its budget and elapsed, not a flattened red. A bespoke status schema here would have been a second representation of that record (DESIGN section 3)." @@ -389,8 +390,17 @@ fn floor_unsuccessful_component_rows(rows: List) -> List filter(r => !floor_component_row_is_done(row: r)) } -fn floor_component_receipt_json_value(run_id: NonEmptyStr, rows: List) -> JsonValue { - json_object(members: [ +fn floor_component_receipt_json_value( + run_id: NonEmptyStr, + rows: List, + selection_present: Bool, + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> JsonValue { + let base = [ json_kv(key: "schema", value: json_string(s: floor_component_receipt_schema)), json_kv(key: "run_id", value: json_string(s: run_id as String)), json_kv(key: "component_count", value: json_int(n: count(rows))), @@ -401,9 +411,55 @@ fn floor_component_receipt_json_value(run_id: NonEmptyStr, rows: List map(r => floor_component_row_json(row: r)) )) - ]) + ] + if selection_present { + json_object(members: concat(base, [ + json_kv( + key: "selection_degradation", + value: selection_degradation_receipt_json( + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + ) + ])) + } else { + json_object(members: base) + } } fn floor_component_receipt_document(run_id: NonEmptyStr, rows: List) -> String { - serialize_json(v: floor_component_receipt_json_value(run_id: run_id, rows: rows)) + serialize_json(v: floor_component_receipt_json_value( + run_id: run_id, + rows: rows, + selection_present: false, + selection_mode_tag: "off" as NonEmptyStr, + selected: 0, + total: 0, + categorization_unavailable: false, + categorization_reason: "" + )) +} + +fn floor_component_receipt_document_with_selection( + run_id: NonEmptyStr, + rows: List, + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> String { + serialize_json(v: floor_component_receipt_json_value( + run_id: run_id, + rows: rows, + selection_present: true, + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + )) } diff --git a/dag/gunbc/observation_emit_census.dag b/dag/gunbc/observation_emit_census.dag index 4600b29fac8..271beff9cf0 100644 --- a/dag/gunbc/observation_emit_census.dag +++ b/dag/gunbc/observation_emit_census.dag @@ -141,6 +141,15 @@ data witness_row_cost_drift_site: CensusedEmitSite = CensusedEmitSite { } } +data selection_degradation_site: CensusedEmitSite = CensusedEmitSite { + marker: "[selection-degradation]" as NonEmptyStr, + source_file: "src/v1/stage0/src/cli_run.rs" as NonEmptyStr, + disposition: CountedFrontierSite { + reason: "floor prep-tax P2 selection honesty receipt — selection_state, selected/total entry groups, ratio, fallback_reason on every discovery completion" as NonEmptyStr, + dissolve_on: "fold into observation receipt projection once selection degradation speaks the observation vocabulary" as NonEmptyStr + } +} + data observation_emit_roster: List = [ floor_memory_site, typecheck_attribution_site, @@ -155,7 +164,8 @@ data observation_emit_roster: List = [ resolve_split_site, assembly_split_site, witness_row_cost_site, - witness_row_cost_drift_site + witness_row_cost_drift_site, + selection_degradation_site ] data observation_emit_roster_completeness_disposition: Disposition = Scaffold { @@ -167,7 +177,7 @@ data observation_emit_roster_completeness_disposition: Disposition = Scaffold { } } -data observation_emit_roster_completeness_note: String = "Roster is bidirectional: every rostered marker must still exist in the seed, AND every known live tag family born after the initial snapshot must appear on the roster (operator finding 3, run 30142403230). Frontier count = 8 (floor-drain, gate-warm-cost, receipt, file, resolve-split, assembly-split, witness-row-cost, witness-row-cost-drift). Six families MigratedToObservation. The unmarked eprintln sites remain the per-print completeness scaffold." +data observation_emit_roster_completeness_note: String = "Roster is bidirectional: every rostered marker must still exist in the seed, AND every known live tag family born after the initial snapshot must appear on the roster (operator finding 3, run 30142403230). Frontier count = 9 (floor-drain, gate-warm-cost, receipt, file, resolve-split, assembly-split, witness-row-cost, witness-row-cost-drift, selection-degradation). Six families MigratedToObservation. The unmarked eprintln sites remain the per-print completeness scaffold." fn emit_site_is_frontier(site: CensusedEmitSite) -> Bool { match site.disposition { diff --git a/dag/gunbc/selection_degradation_receipt.dag b/dag/gunbc/selection_degradation_receipt.dag new file mode 100644 index 00000000000..3598a68bd72 --- /dev/null +++ b/dag/gunbc/selection_degradation_receipt.dag @@ -0,0 +1,314 @@ +module gunbc.selection_degradation_receipt + +import std.types { String, Bool, NonEmptyStr } +import std.nat { Nat } +import std.measure { BasisPoint, basis_point, basis_point_count } +import std.decimal { zero_pad_left } +import extdeps.languages.json.emit { + JsonValue, + JsonKeyValue, + json_string, + json_int, + json_bool, + json_object, + json_kv, +} + +data selection_degradation_receipt_path: String = "target/floor-selection-degradation-receipt.txt" + +data selection_degradation_receipt_note: String = "P2 of the floor prep-tax program (still-moth-459, 2026-08-03): every floor discovery run publishes selection honesty — state, selected/total entry groups, ratio, and fallback_reason — so an affected run that executes the whole corpus is a COUNTED performance degradation (SelectionSuperset), never quiet success (DESIGN section 5). SelectionDegradationUnavailable is the unknown selection_mode_tag fail-closed arm only; upfront categorization_unavailable augments Applied/Superset fallback_reason, not that state. Authority for classification and receipt shape; the seed transport in cli_run.rs resolves this module and calls selection_degradation_receipt_line with measured counts only." + +data selection_degradation_receipt_marker: NonEmptyStr = "[selection-degradation]" as NonEmptyStr + +type SelectionDegradationState + = SelectionDegradationOff + | SelectionDegradationPredictOnly + | SelectionDegradationApplied + | SelectionDegradationSuperset + | SelectionDegradationUnavailable + +type SelectionDegradationReceipt { + selection_state: SelectionDegradationState + selected_entry_groups: Nat + total_entry_groups: Nat + selection_ratio_bp: BasisPoint + fallback_reason: String +} + +fn selection_degradation_mode_of_tag(tag: NonEmptyStr) -> String { + tag as String +} + +fn selection_degradation_state_tag(state: SelectionDegradationState) -> String { + match state { + SelectionDegradationOff => "SelectionOff" + SelectionDegradationPredictOnly => "SelectionPredictOnly" + SelectionDegradationApplied => "SelectionApplied" + SelectionDegradationSuperset => "SelectionSuperset" + SelectionDegradationUnavailable => "SelectionUnavailable" + } +} + +fn selection_ratio_basis_points(selected: Nat, total: Nat) -> BasisPoint { + if total == 0 { + basis_point(count: 0) + } else { + basis_point(count: (selected * 10000) / total) + } +} + +fn selection_ratio_wire(selected: Nat, total: Nat) -> String { + let bp = basis_point_count(bp: selection_ratio_basis_points(selected: selected, total: total)) + let unit = 10000 + let whole = bp / unit + let frac = bp - (whole * unit) + join( + [to_string(whole), ".", zero_pad_left(digits: to_string(frac), width: 4)], + "" + ) +} + +fn selection_degradation_fallback_reason_off() -> String { + "node_frontier_selection=SelectionOff — full corpus roster executed" +} + +fn selection_degradation_fallback_reason_predict_only() -> String { + "node_frontier_selection=SelectionPredictOnly — all witnesses run cold; divergences counted" +} + +fn selection_degradation_fallback_reason_applied_narrowed(selected: Nat, total: Nat) -> String { + concat( + "SelectionApplied: ", + concat( + concat(to_string(selected), concat(" of ", to_string(total))), + " entry groups executed (import-closure and node-frontier skip reduced the run)" + ) + ) +} + +fn selection_degradation_fallback_reason_superset(total: Nat) -> String { + concat( + "SelectionSuperset: all ", + concat( + to_string(total), + " entry groups executed — affected-set selection did not narrow the floor (counted performance degradation)" + ) + ) +} + +fn selection_degradation_fallback_reason_categorization_unavailable(detail: String) -> String { + concat( + "upfront import-closure categorization failed (", + concat(detail, "); per-shard selection is authoritative at completion") + ) +} + +fn selection_degradation_fallback_reason_unknown_mode_tag(tag: NonEmptyStr) -> String { + concat( + "SelectionUnavailable: unknown selection_mode_tag (", + concat(tag as String, "); expected off|applied|predict_only") + ) +} + +fn selection_degradation_state_of( + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> SelectionDegradationState { + if selection_mode_tag == ("off" as NonEmptyStr) { + SelectionDegradationOff + } else if selection_mode_tag == ("predict_only" as NonEmptyStr) { + SelectionDegradationPredictOnly + } else if selection_mode_tag == ("applied" as NonEmptyStr) { + if selected == total { + SelectionDegradationSuperset + } else { + SelectionDegradationApplied + } + } else { + SelectionDegradationUnavailable + } +} + +fn selection_degradation_fallback_reason_of( + selection_mode_tag: NonEmptyStr, + state: SelectionDegradationState, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> String { + match state { + SelectionDegradationOff => selection_degradation_fallback_reason_off() + SelectionDegradationPredictOnly => selection_degradation_fallback_reason_predict_only() + SelectionDegradationApplied => + if categorization_unavailable { + concat( + selection_degradation_fallback_reason_applied_narrowed(selected: selected, total: total), + concat( + " — ", + selection_degradation_fallback_reason_categorization_unavailable(detail: categorization_reason) + ) + ) + } else { + selection_degradation_fallback_reason_applied_narrowed(selected: selected, total: total) + } + SelectionDegradationSuperset => + if categorization_unavailable { + concat( + selection_degradation_fallback_reason_superset(total: total), + concat( + " — ", + selection_degradation_fallback_reason_categorization_unavailable(detail: categorization_reason) + ) + ) + } else { + selection_degradation_fallback_reason_superset(total: total) + } + SelectionDegradationUnavailable => + selection_degradation_fallback_reason_unknown_mode_tag(tag: selection_mode_tag) + } +} + +fn selection_degradation_receipt_of( + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> SelectionDegradationReceipt { + let state = selection_degradation_state_of( + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + SelectionDegradationReceipt { + selection_state: state, + selected_entry_groups: selected, + total_entry_groups: total, + selection_ratio_bp: selection_ratio_basis_points(selected: selected, total: total), + fallback_reason: selection_degradation_fallback_reason_of( + selection_mode_tag: selection_mode_tag, + state: state, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + } +} + +fn selection_degradation_receipt_line( + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> String { + let r = selection_degradation_receipt_of( + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + concat( + concat( + concat(selection_degradation_receipt_marker as String, " selection_state="), + selection_degradation_state_tag(state: r.selection_state) + ), + concat( + concat( + concat( + concat(" selected_entry_groups=", to_string(r.selected_entry_groups)), + concat(" total_entry_groups=", to_string(r.total_entry_groups)) + ), + concat(" selection_ratio=", selection_ratio_wire(selected: selected, total: total)) + ), + concat(" fallback_reason=", r.fallback_reason) + ) + ) +} + +fn selection_degradation_receipt_body( + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> String { + let r = selection_degradation_receipt_of( + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + concat( + concat( + concat( + concat("selection_state=", selection_degradation_state_tag(state: r.selection_state)), + concat("\nselected_entry_groups=", to_string(r.selected_entry_groups)) + ), + concat("\ntotal_entry_groups=", to_string(r.total_entry_groups)) + ), + concat( + concat("\nselection_ratio=", selection_ratio_wire(selected: selected, total: total)), + concat("\nfallback_reason=", r.fallback_reason) + ) + ) +} + +fn selection_degradation_receipt_json( + selection_mode_tag: NonEmptyStr, + selected: Nat, + total: Nat, + categorization_unavailable: Bool, + categorization_reason: String +) -> JsonValue { + let r = selection_degradation_receipt_of( + selection_mode_tag: selection_mode_tag, + selected: selected, + total: total, + categorization_unavailable: categorization_unavailable, + categorization_reason: categorization_reason + ) + json_object(members: [ + json_kv( + key: "selection_state", + value: json_string(s: selection_degradation_state_tag(state: r.selection_state)) + ), + json_kv(key: "selected_entry_groups", value: json_int(n: r.selected_entry_groups)), + json_kv(key: "total_entry_groups", value: json_int(n: r.total_entry_groups)), + json_kv( + key: "selection_ratio", + value: json_string(s: selection_ratio_wire(selected: selected, total: total)) + ), + json_kv(key: "fallback_reason", value: json_string(s: r.fallback_reason)), + json_kv(key: "categorization_unavailable", value: json_bool(b: categorization_unavailable)) + ]) +} + +fn selection_degradation_is_superset(receipt: SelectionDegradationReceipt) -> Bool { + match receipt.selection_state { + SelectionDegradationSuperset => true + SelectionDegradationOff => false + SelectionDegradationPredictOnly => false + SelectionDegradationApplied => false + SelectionDegradationUnavailable => false + } +} + +fn selection_degradation_is_applied_narrowed(receipt: SelectionDegradationReceipt) -> Bool { + match receipt.selection_state { + SelectionDegradationApplied => true + SelectionDegradationOff => false + SelectionDegradationPredictOnly => false + SelectionDegradationSuperset => false + SelectionDegradationUnavailable => false + } +} diff --git a/dag/test/claim/observation_emit_census_witness_test.dag b/dag/test/claim/observation_emit_census_witness_test.dag index 0cbd2a49500..1f2eece1506 100644 --- a/dag/test/claim/observation_emit_census_witness_test.dag +++ b/dag/test/claim/observation_emit_census_witness_test.dag @@ -26,6 +26,7 @@ import gunbc.observation_emit_census { assembly_split_site, witness_row_cost_site, witness_row_cost_drift_site, + selection_degradation_site, } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree @@ -132,7 +133,9 @@ test fn w_post_snapshot_live_tags_are_rostered_bidirectional() -> Bool { census_marker_present(site: witness_row_cost_site) && emit_site_is_frontier(site: witness_row_cost_site) && census_marker_present(site: witness_row_cost_drift_site) && - emit_site_is_frontier(site: witness_row_cost_drift_site) + emit_site_is_frontier(site: witness_row_cost_drift_site) && + census_marker_present(site: selection_degradation_site) && + emit_site_is_frontier(site: selection_degradation_site) } test fn w_the_raw_byte_dump_shape_is_gone_from_the_seed() -> Bool { diff --git a/dag/test/claim/selection_degradation_receipt_witness_test.dag b/dag/test/claim/selection_degradation_receipt_witness_test.dag new file mode 100644 index 00000000000..5dbb90fbd20 --- /dev/null +++ b/dag/test/claim/selection_degradation_receipt_witness_test.dag @@ -0,0 +1,141 @@ +module test.claim.selection_degradation_receipt_witness_test + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { String, Bool, NonEmptyStr } +import std.nat { Nat } +import gunbc.selection_degradation_receipt { + SelectionDegradationReceipt, + SelectionDegradationApplied, + SelectionDegradationSuperset, + SelectionDegradationOff, + selection_degradation_receipt_of, + selection_degradation_receipt_line, + selection_degradation_receipt_body, + selection_degradation_is_superset, + selection_degradation_is_applied_narrowed, + selection_degradation_state_tag, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data sdr_witness_note: String = "Executing consumer for gunbc.selection_degradation_receipt (floor prep-tax P2). THE PAIR IS THE PROOF: a narrowed affected run must read SelectionApplied with honest counts; a whole-corpus affected run must read SelectionSuperset with an explicit fallback_reason — never quiet success. RED controls mislabel a superset as applied." + +test fn sdr_applied_narrowed_receipt() -> Bool { + let line = selection_degradation_receipt_line( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 602, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + let r = selection_degradation_receipt_of( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 602, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + selection_degradation_is_applied_narrowed(receipt: r) + && string_contains(s: line, pattern: "selection_state=SelectionApplied") + && string_contains(s: line, pattern: "selected_entry_groups=602") + && string_contains(s: line, pattern: "total_entry_groups=842") + && string_contains(s: line, pattern: "selection_ratio=0.7149") + && string_contains(s: line, pattern: "fallback_reason=") +} + +test fn sdr_superset_whole_corpus_is_loud() -> Bool { + let line = selection_degradation_receipt_line( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + let r = selection_degradation_receipt_of( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + selection_degradation_is_superset(receipt: r) + && string_contains(s: line, pattern: "selection_state=SelectionSuperset") + && string_contains(s: line, pattern: "selected_entry_groups=842") + && string_contains(s: line, pattern: "total_entry_groups=842") + && string_contains(s: line, pattern: "selection_ratio=1.0000") + && string_contains(s: line, pattern: "counted performance degradation") +} + +test fn sdr_off_mode_reports_full_run() -> Bool { + let line = selection_degradation_receipt_line( + selection_mode_tag: "off" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + let r = selection_degradation_receipt_of( + selection_mode_tag: "off" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + selection_degradation_state_tag(state: r.selection_state) == "SelectionOff" + && string_contains(s: line, pattern: "selection_state=SelectionOff") +} + +test fn sdr_superset_must_not_read_as_applied_red() -> Bool { + let r = selection_degradation_receipt_of( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + !selection_degradation_is_applied_narrowed(receipt: r) +} + +test fn sdr_categorization_unavailable_superset_still_superset() -> Bool { + let r = selection_degradation_receipt_of( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 842, + total: 842, + categorization_unavailable: true, + categorization_reason: "module graph facts absent" + ) + selection_degradation_is_superset(receipt: r) + && string_contains( + s: r.fallback_reason, + pattern: "upfront import-closure categorization failed" + ) +} + +test fn sdr_receipt_body_carries_all_five_fields() -> Bool { + let body = selection_degradation_receipt_body( + selection_mode_tag: "applied" as NonEmptyStr, + selected: 602, + total: 842, + categorization_unavailable: false, + categorization_reason: "" + ) + string_contains(s: body, pattern: "selection_state=SelectionApplied") + && string_contains(s: body, pattern: "selected_entry_groups=602") + && string_contains(s: body, pattern: "total_entry_groups=842") + && string_contains(s: body, pattern: "selection_ratio=0.7149") + && string_contains(s: body, pattern: "fallback_reason=") +} + +test fn sdr_unknown_mode_tag_is_unavailable_fail_closed() -> Bool { + let r = selection_degradation_receipt_of( + selection_mode_tag: "bogus" as NonEmptyStr, + selected: 1, + total: 2, + categorization_unavailable: false, + categorization_reason: "" + ) + selection_degradation_state_tag(state: r.selection_state) == "SelectionUnavailable" + && string_contains(s: r.fallback_reason, pattern: "unknown selection_mode_tag") + && string_contains(s: r.fallback_reason, pattern: "bogus") + && !string_contains(s: r.fallback_reason, pattern: "categorization failed") +} diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 5bcd8b6ca88..4e17fffa1d3 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -14,13 +14,14 @@ use v1_compiler::cli_run::{ compute_histogram_data, enable_floor_compile_clean_lazy_install, heartbeat_feed_enter_batch, heartbeat_feed_entry_completed, heartbeat_feed_snapshot, install_floor_compile_clean_receipt, make_eval_context, project_witness_cost_receipt, record_resolution_divergence_phase, - reset_resolution_divergence_phase_receipt, resolution_divergence_parent_plan_capture_begin, + render_selection_degradation_receipt_body, reset_resolution_divergence_phase_receipt, + resolution_divergence_parent_plan_capture_begin, resolution_divergence_parent_plan_capture_finish, resolve_entry_graph, resolve_entry_graph_shared, run_claim, run_discovery_corpus_with_options, run_value, set_phase, top_n_slowest_witnesses, BudgetKind, ClaimOutcome, DiscoveryCorpusOptions, DiscoverySummary, DiscoveryWidthPolicy, FloorPhase, HistogramData, NodeFrontierSelectionMode, PhaseProfile, - ResolutionDivergencePhase, ResolutionDivergencePhaseState, TimingPercentiles, - DEFAULT_SLOWEST_WITNESS_ATTRIBUTION_N, + ResolutionDivergencePhase, ResolutionDivergencePhaseState, SelectionDegradationSnapshot, + TimingPercentiles, DEFAULT_SLOWEST_WITNESS_ATTRIBUTION_N, }; use v1_compiler::memory_governor::{ binding_cap_cgroup_dir, binding_high_cgroup_dir, floor_budget_below_minimum_footprint, @@ -1563,6 +1564,8 @@ struct ClaimResult { /// as data on the path that needs it. It is a projection of `ClaimOutcome::TimedOut`, /// not a second authority: nothing sets it except the `TimedOut` arm below. budget_refusal: Option, + /// Discovery batch only: finalized selection-degradation facts for floor receipts. + selection_degradation: Option, } /// The pair explaining a budget kill, kept alongside the flattened `ok`/`detail`. @@ -1774,6 +1777,7 @@ fn claim_result_for_outcome( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }, ClaimOutcome::Fail => ClaimResult { detail: { @@ -1798,6 +1802,7 @@ fn claim_result_for_outcome( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }, ClaimOutcome::NotBool { got } => ClaimResult { function, @@ -1811,6 +1816,7 @@ fn claim_result_for_outcome( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }, ClaimOutcome::RuntimeError { message } => ClaimResult { function, @@ -1824,6 +1830,7 @@ fn claim_result_for_outcome( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }, ClaimOutcome::TimedOut { elapsed_ms, @@ -1855,6 +1862,7 @@ fn claim_result_for_outcome( budget_ms, kind, }), + selection_degradation: None, }, } } @@ -2139,6 +2147,7 @@ fn run_native_bundle_unit( corpus_witnesses: 3, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }; if execution_mode != ExecutionMode::Wet { return fail( @@ -2298,6 +2307,7 @@ fn run_native_bundle_unit( corpus_witnesses: selected as usize, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, } } @@ -2333,6 +2343,7 @@ fn run_batch_unit( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }], BatchUnit::NativeBundle { entry, @@ -2467,6 +2478,7 @@ fn run_shared_entry_claims( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }) .collect(); } @@ -2535,6 +2547,7 @@ fn run_memo_shared_claims( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }) .collect(); } @@ -3081,6 +3094,7 @@ fn discovery_claim_result( function: String, ok: bool, detail: String, + selection: NodeFrontierSelectionMode, summary: &DiscoverySummary, projected: Result, String>, ) -> ClaimResult { @@ -3100,6 +3114,9 @@ fn discovery_claim_result( corpus_witnesses: summary.total, witness_row_costs, budget_refusal: discovery_budget_refusal(summary), + selection_degradation: Some(SelectionDegradationSnapshot::from_summary( + selection, summary, + )), }, Err(msg) => { eprintln!("[witness-row-cost] refused: {msg}"); @@ -3128,6 +3145,9 @@ fn discovery_claim_result( // failed to project its receipt is still a budget kill, and dropping that // here would hand it back to the substring classifier. budget_refusal: discovery_budget_refusal(summary), + selection_degradation: Some(SelectionDegradationSnapshot::from_summary( + selection, summary, + )), } } } @@ -3202,6 +3222,10 @@ fn run_discovery_batch_node( corpus_witnesses: summary.total, witness_row_costs: Vec::new(), budget_refusal: discovery_budget_refusal(&summary), + selection_degradation: Some(SelectionDegradationSnapshot::from_summary( + node_frontier_selection, + &summary, + )), }; } } @@ -3326,6 +3350,7 @@ fn run_discovery_batch_node( "expect_red probe unexpectedly green: {} witness(es) passed — un-quarantine (delete known_red_probe_entries / falsifier_self_host_wet_known_red_entries rows) or restore the discriminating red", summary.total ), + node_frontier_selection, &summary, projected, ) @@ -3334,6 +3359,7 @@ fn run_discovery_batch_node( format!("{label} ({} witnesses)", summary.total), true, String::new(), + node_frontier_selection, &summary, projected, ) @@ -3360,6 +3386,10 @@ fn run_discovery_batch_node( corpus_witnesses: summary.total, witness_row_costs: Vec::new(), budget_refusal: discovery_budget_refusal(&summary), + selection_degradation: Some(SelectionDegradationSnapshot::from_summary( + node_frontier_selection, + &summary, + )), }; } } @@ -3375,6 +3405,7 @@ fn run_discovery_batch_node( format!("{label} (expect_red still-red OK)"), true, String::new(), + node_frontier_selection, &summary, projected, ) @@ -3388,6 +3419,7 @@ fn run_discovery_batch_node( summary.total, summary.failures.join("; ") ), + node_frontier_selection, &summary, projected, ) @@ -3422,6 +3454,7 @@ fn run_discovery_batch_node( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, } } else { ClaimResult { @@ -3439,6 +3472,7 @@ fn run_discovery_batch_node( corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, } } } @@ -4201,29 +4235,77 @@ fn write_floor_component_receipt_at( } } - let doc = match run_in_context_with_args( - &ctx, - "floor_component_receipt_document", - &[ - (Some("run_id".to_string()), Value::Str(run_id.clone())), - (Some("rows".to_string()), Value::List(Rc::new(rows.into()))), - ], - false, - ) { - Ok(Value::Str(s)) => s, - Ok(other) => { - eprintln!( - "claim_executor: floor component receipt REFUSED — \ - floor_component_receipt_document returned {other:?}, not Str" - ); - return false; + let doc = if let Some(snapshot) = selection_degradation_from_batch_records(batch_records) { + match run_in_context_with_args( + &ctx, + "floor_component_receipt_document_with_selection", + &[ + (Some("run_id".to_string()), Value::Str(run_id.clone())), + (Some("rows".to_string()), Value::List(Rc::new(rows.into()))), + ( + Some("selection_mode_tag".to_string()), + Value::Str(snapshot.selection_mode_tag.clone()), + ), + ( + Some("selected".to_string()), + Value::Int(snapshot.selected_entry_groups as i64), + ), + ( + Some("total".to_string()), + Value::Int(snapshot.total_entry_groups as i64), + ), + ( + Some("categorization_unavailable".to_string()), + Value::Bool(snapshot.categorization_unavailable), + ), + ( + Some("categorization_reason".to_string()), + Value::Str(snapshot.categorization_reason.clone()), + ), + ], + false, + ) { + Ok(Value::Str(s)) => s, + Ok(other) => { + eprintln!( + "claim_executor: floor component receipt REFUSED — \ + floor_component_receipt_document_with_selection returned {other:?}, not Str" + ); + return false; + } + Err(e) => { + eprintln!( + "claim_executor: floor component receipt REFUSED — \ + floor_component_receipt_document_with_selection eval: {e}" + ); + return false; + } } - Err(e) => { - eprintln!( - "claim_executor: floor component receipt REFUSED — \ - floor_component_receipt_document eval: {e}" - ); - return false; + } else { + match run_in_context_with_args( + &ctx, + "floor_component_receipt_document", + &[ + (Some("run_id".to_string()), Value::Str(run_id.clone())), + (Some("rows".to_string()), Value::List(Rc::new(rows.into()))), + ], + false, + ) { + Ok(Value::Str(s)) => s, + Ok(other) => { + eprintln!( + "claim_executor: floor component receipt REFUSED — \ + floor_component_receipt_document returned {other:?}, not Str" + ); + return false; + } + Err(e) => { + eprintln!( + "claim_executor: floor component receipt REFUSED — \ + floor_component_receipt_document eval: {e}" + ); + return false; + } } }; @@ -4343,8 +4425,8 @@ fn floor_component_row_value( } } -fn write_resolve_receipt(batch_records: &[BatchRecord]) -> bool { - write_resolve_receipt_at(std::path::Path::new("target"), batch_records) +fn write_resolve_receipt(source_roots: &[String], batch_records: &[BatchRecord]) -> bool { + write_resolve_receipt_at(std::path::Path::new("target"), source_roots, batch_records) } /// Per-batch wall receipt (THE COST WALL, Piece 3 derived clamp): typed rows — one @@ -4992,7 +5074,52 @@ fn write_on_success_receipt( true } -fn write_resolve_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord]) -> bool { +fn selection_degradation_from_batch_records( + batch_records: &[BatchRecord], +) -> Option { + batch_records + .iter() + .flat_map(|rec| &rec.results) + .find_map(|result| result.selection_degradation.clone()) +} + +fn write_selection_degradation_receipt_at( + base: &std::path::Path, + source_roots: &[String], + batch_records: &[BatchRecord], +) -> bool { + let Some(snapshot) = selection_degradation_from_batch_records(batch_records) else { + return true; + }; + let body = match render_selection_degradation_receipt_body(source_roots, &snapshot) { + Ok(body) => body, + Err(msg) => { + eprintln!("claim_executor: {msg}"); + return false; + } + }; + let path = base.join("floor-selection-degradation-receipt.txt"); + if let Err(e) = std::fs::create_dir_all(base).and_then(|_| std::fs::write(&path, &body)) { + eprintln!( + "claim_executor: failed to write selection degradation receipt {}: {e} — walk fails closed", + path.display() + ); + return false; + } + eprintln!( + "[receipt] floor selection degradation: {} entry group(s) selected of {} (receipt: {})", + snapshot.selected_entry_groups, + snapshot.total_entry_groups, + path.display() + ); + true +} + +fn write_resolve_receipt_at( + base: &std::path::Path, + source_roots: &[String], + batch_records: &[BatchRecord], +) -> bool { let mut resolves_total: u64 = 0; let mut resolve_ms_total: u128 = 0; let mut discovery_corpus_resolve_ms: u128 = 0; @@ -5008,9 +5135,22 @@ fn write_resolve_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord } } let discovery_phases = v1_compiler::cli_run::take_discovery_phase_totals_receipt_rows(); - let body = format!( + let mut body = format!( "resolves_total={resolves_total}\nresolve_ms_total={resolve_ms_total}\ndiscovery_corpus_resolve_ms={discovery_corpus_resolve_ms}\ndiscovery_corpus_eval_ms={discovery_corpus_eval_ms}\n{discovery_phases}" ); + if let Some(snapshot) = selection_degradation_from_batch_records(batch_records) { + match render_selection_degradation_receipt_body(source_roots, &snapshot) { + Ok(selection_body) => { + body.push_str("\n[selection_degradation]\n"); + body.push_str(&selection_body); + body.push('\n'); + } + Err(msg) => { + eprintln!("claim_executor: resolve receipt selection degradation refused: {msg}"); + return false; + } + } + } let path = base.join("floor-resolve-receipt.txt"); if let Err(e) = std::fs::create_dir_all(base).and_then(|_| std::fs::write(&path, &body)) { eprintln!( @@ -6195,8 +6335,17 @@ fn run_walk( let total_wall_nanos = walk_start.elapsed().as_nanos(); emit_gantt(&batch_records, total_wall_nanos); trace_floor_phase("resolve-receipt", "started", ""); - let resolve_receipt_ok = !emit_ordinary_floor_receipts || write_resolve_receipt(&batch_records); + let resolve_receipt_ok = + !emit_ordinary_floor_receipts || write_resolve_receipt(source_roots, &batch_records); trace_floor_phase("resolve-receipt", "completed", ""); + trace_floor_phase("selection-degradation-receipt", "started", ""); + let selection_degradation_receipt_ok = !emit_ordinary_floor_receipts + || write_selection_degradation_receipt_at( + std::path::Path::new("target"), + source_roots, + &batch_records, + ); + trace_floor_phase("selection-degradation-receipt", "completed", ""); trace_floor_phase("batch-wall-receipt", "started", ""); let batch_wall_receipt_ok = !emit_ordinary_floor_receipts || write_batch_wall_receipt(&batch_records); @@ -6258,6 +6407,7 @@ fn run_walk( || !gate_warm_cost_receipt_ok || !witness_row_cost_receipt_ok || !witness_row_cost_drift_receipt_ok + || !selection_degradation_receipt_ok || !floor_component_receipt_ok || !materialization_receipt_ok; // Floor finalization laws (the in-executor form of the deleted resolve/ @@ -8341,6 +8491,7 @@ mod tests { corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }) .collect(), } @@ -8563,6 +8714,7 @@ mod tests { corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, }; let (mode, _) = batch_failure_mode_and_detail(&batch_record_for_test(vec![plain])); assert_eq!(mode, "WitnessRed"); @@ -8765,7 +8917,7 @@ mod tests { std::env::temp_dir().join(format!("claim-executor-receipt-red-{}", std::process::id())); let _ = fs::remove_file(&base); fs::write(&base, b"a file where the receipt dir should be").unwrap(); - assert!(!write_resolve_receipt_at(&base, &[])); + assert!(!write_resolve_receipt_at(&base, &[], &[])); assert!(!write_batch_wall_receipt_at(&base, &[])); let _ = fs::remove_file(&base); } @@ -10423,6 +10575,9 @@ mod tests { performance_receipts: Vec::new(), total_measured_nanos: 0, roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, }; let killed = ClaimOutcome::TimedOut { elapsed_ms: 900_001, @@ -10440,6 +10595,7 @@ mod tests { "discovery-corpus".into(), false, killed_detail.to_string(), + NodeFrontierSelectionMode::Applied, &summary_with(killed.clone()), projected, ); @@ -10456,6 +10612,7 @@ mod tests { "discovery-corpus".into(), false, "red".into(), + NodeFrontierSelectionMode::Applied, &summary_with(ClaimOutcome::Fail), Ok(Vec::new()), ); @@ -10493,12 +10650,16 @@ mod tests { performance_receipts: Vec::new(), total_measured_nanos: 0, roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, }; let prior = "1 of 1 discovery witness(es) failed: e.dag::f failed"; let result = discovery_claim_result( "probe".into(), false, prior.to_string(), + NodeFrontierSelectionMode::Applied, &summary, Err("[witness-row-cost] REFUSED: missing measured resolve parent for e.dag".into()), ); @@ -10540,6 +10701,9 @@ mod tests { performance_receipts: Vec::new(), total_measured_nanos: 0, roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, }; let (label, provenance) = scoped_witness_summary_outcome( &summary, @@ -10568,6 +10732,7 @@ mod tests { corpus_witnesses: 0, witness_row_costs: Vec::new(), budget_refusal: None, + selection_degradation: None, } } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index c0640594ddc..c69b1eb6cb3 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -8557,6 +8557,146 @@ fn emit_floor_drain_receipt( ); } +const SELECTION_DEGRADATION_RECEIPT_ENTRY: &str = "dag/gunbc/selection_degradation_receipt.dag"; + +/// Kept so `gunbc.observation_emit_census` roster hygiene cannot go stale after the +/// raw `[selection-degradation]` receipt line dissolves into the observation projection. +#[allow(dead_code)] +pub const SELECTION_DEGRADATION_CENSUS_MARKER: &str = "[selection-degradation]"; + +fn node_frontier_selection_mode_tag(mode: NodeFrontierSelectionMode) -> &'static str { + match mode { + NodeFrontierSelectionMode::Off => "off", + NodeFrontierSelectionMode::Applied => "applied", + NodeFrontierSelectionMode::PredictOnly => "predict_only", + } +} + +/// Measured selection-degradation facts finalized at discovery completion. +#[derive(Debug, Clone)] +pub struct SelectionDegradationSnapshot { + pub selection_mode_tag: String, + pub selected_entry_groups: usize, + pub total_entry_groups: usize, + pub categorization_unavailable: bool, + pub categorization_reason: String, +} + +impl SelectionDegradationSnapshot { + pub fn from_summary(selection: NodeFrontierSelectionMode, summary: &DiscoverySummary) -> Self { + Self { + selection_mode_tag: node_frontier_selection_mode_tag(selection).to_string(), + selected_entry_groups: summary.selected_entry_groups, + total_entry_groups: summary.total_entry_groups, + categorization_unavailable: summary.selection_categorization_reason.is_some(), + categorization_reason: summary + .selection_categorization_reason + .clone() + .unwrap_or_default(), + } + } +} + +fn selection_degradation_interp_ctx( + source_roots: &[String], +) -> Result<(v1_interpreter::InterpContext, String), String> { + let entry = resolve_entry_file_under_roots(source_roots, SELECTION_DEGRADATION_RECEIPT_ENTRY) + .map_err(|_| { + format!( + "selection-degradation receipt REFUSED — {SELECTION_DEGRADATION_RECEIPT_ENTRY} \ + not found under source roots" + ) + })?; + let (graph, indices) = resolve_entry_graph_shared(source_roots, &entry) + .map_err(|e| format!("selection-degradation receipt REFUSED — resolve {entry}: {e}"))?; + Ok(( + make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Hermetic), + entry, + )) +} + +fn selection_degradation_dag_args( + snapshot: &SelectionDegradationSnapshot, +) -> Vec<(Option, v1_interpreter::Value)> { + vec![ + ( + Some("selection_mode_tag".to_string()), + v1_interpreter::Value::Str(snapshot.selection_mode_tag.clone()), + ), + ( + Some("selected".to_string()), + v1_interpreter::Value::Int(snapshot.selected_entry_groups as i64), + ), + ( + Some("total".to_string()), + v1_interpreter::Value::Int(snapshot.total_entry_groups as i64), + ), + ( + Some("categorization_unavailable".to_string()), + v1_interpreter::Value::Bool(snapshot.categorization_unavailable), + ), + ( + Some("categorization_reason".to_string()), + v1_interpreter::Value::Str(snapshot.categorization_reason.clone()), + ), + ] +} + +fn eval_selection_degradation_fn( + source_roots: &[String], + fn_name: &str, + snapshot: &SelectionDegradationSnapshot, +) -> Result { + let (ctx, entry) = selection_degradation_interp_ctx(source_roots)?; + match v1_interpreter::run_in_context_with_args( + &ctx, + fn_name, + &selection_degradation_dag_args(snapshot), + false, + ) { + Ok(v1_interpreter::Value::Str(line)) => Ok(line), + Ok(other) => Err(format!( + "selection-degradation receipt REFUSED — {fn_name} returned `{}`, expected Str \ + (entry {entry})", + ctx.format_value(&other) + )), + Err(e) => Err(format!( + "selection-degradation receipt REFUSED — {fn_name} eval: {e}" + )), + } +} + +/// Bracket-tagged stderr line for discovery completion (also embedded in `[measurement]`). +pub fn render_selection_degradation_receipt_line( + source_roots: &[String], + snapshot: &SelectionDegradationSnapshot, +) -> Result { + eval_selection_degradation_fn(source_roots, "selection_degradation_receipt_line", snapshot) +} + +/// Key=value body written to `target/floor-selection-degradation-receipt.txt` and appended to +/// `floor-resolve-receipt.txt`. +pub fn render_selection_degradation_receipt_body( + source_roots: &[String], + snapshot: &SelectionDegradationSnapshot, +) -> Result { + eval_selection_degradation_fn(source_roots, "selection_degradation_receipt_body", snapshot) +} + +/// P2 floor prep-tax receipt on stderr after every discovery completion. +pub fn emit_selection_degradation_receipt( + source_roots: &[String], + selection: NodeFrontierSelectionMode, + summary: &DiscoverySummary, +) { + let _ = SELECTION_DEGRADATION_CENSUS_MARKER; + let snapshot = SelectionDegradationSnapshot::from_summary(selection, summary); + match render_selection_degradation_receipt_line(source_roots, &snapshot) { + Ok(line) => eprintln!("{line}"), + Err(msg) => eprintln!("{msg}"), + } +} + /// Enforce the host-budget-derived entry cap on the private typed cache. Evictions /// are counted and logged — a typed, located diagnostic, never a silent widen. /// Victim selection is arbitrary (first `HashMap` key), not LRU or load-aware: @@ -14770,6 +14910,13 @@ pub struct DiscoverySummary { /// module names is a property of the source closure: independent of cache warmth, resolve order, /// and — critically for a calibration datum — of the diff under test. pub roster_closure_nodes: usize, + /// Entry-group grain of the discovery roster (`entry_row_groups`); set at completion. + pub total_entry_groups: usize, + /// Distinct entries with at least one executed witness (not selection-skipped only). + pub selected_entry_groups: usize, + /// When Applied mode could not run upfront import-closure categorization; per-shard + /// selection remains authoritative and the completion receipt still publishes counts. + pub selection_categorization_reason: Option, } #[derive(Debug, Clone)] @@ -18905,6 +19052,7 @@ pub fn run_discovery_corpus_with_options( options: DiscoveryCorpusOptions, ) -> Result { let pump_started = std::time::Instant::now(); + let selection = options.node_frontier_selection; let out = run_discovery_corpus_with_options_inner( source_roots, scan_dirs, @@ -18917,6 +19065,9 @@ pub fn run_discovery_corpus_with_options( &discovery_phase_totals::PUMP_WALL_MS, pump_started.elapsed(), ); + if let Ok(summary) = &out { + emit_selection_degradation_receipt(source_roots, selection, summary); + } out } @@ -19200,6 +19351,23 @@ fn run_discovery_corpus_with_options_inner( &index, &diff_edits, ); + let selection_categorization_reason = + if options.node_frontier_selection == NodeFrontierSelectionMode::Applied { + let declared_paths = index.module_graph_facts.declared_repo_paths(); + let touched: Vec = diff_edits.touched_entry_files.iter().cloned().collect(); + match discovery_entry_fast_skip_without_resolve( + &rows, + &index.module_graph_facts, + &declared_paths, + &touched, + &diff_edits, + ) { + Ok(_) => None, + Err(e) => Some(e), + } + } else { + None + }; // Derive every leg for the WHOLE roster here, above the width dispatch, while this // thread's shared index is warm. At width > 1 the pool hands each worker its own chunk // of rows, so priming inside `run_discovery_rows` would build one interpreter context @@ -19266,7 +19434,13 @@ fn run_discovery_corpus_with_options_inner( "[calibration] closure consistency: pre-resolve loader-closure union == post-resolve union == {} node(s)", pre_resolve_closure_nodes ); - Ok(attach_deferred_discovery_rows(summary, deferred_rows)) + Ok(finalize_discovery_summary( + summary, + &rows, + options.node_frontier_selection, + selection_categorization_reason.clone(), + deferred_rows, + )) } DiscoveryWidthPolicy::Adaptive(governor) => { // Adaptive pool: entry-groups drain through governor-admitted workers. Each worker @@ -19365,8 +19539,11 @@ fn run_discovery_corpus_with_options_inner( drain_prev = snap; } emit_floor_drain_receipt(&index, total_groups, &drain_peaks); - return Ok(attach_deferred_discovery_rows( + return Ok(finalize_discovery_summary( merge_discovery_summaries(summaries), + &rows, + options.node_frontier_selection, + selection_categorization_reason.clone(), deferred_rows, )); } @@ -19541,19 +19718,33 @@ fn run_discovery_corpus_with_options_inner( worker error — scheduler invariant violated; refusing a partial corpus" )); } - Ok(attach_deferred_discovery_rows( + Ok(finalize_discovery_summary( merge_discovery_summaries(summaries), + &rows, + options.node_frontier_selection, + selection_categorization_reason, deferred_rows, )) } }; } -fn attach_deferred_discovery_rows( +fn finalize_discovery_summary( mut summary: DiscoverySummary, + rows: &[DiscoveryRow], + _selection: NodeFrontierSelectionMode, + selection_categorization_reason: Option, deferred_rows: Vec, ) -> DiscoverySummary { summary.deferred_rows = deferred_rows; + summary.total_entry_groups = entry_row_groups(rows).len(); + summary.selected_entry_groups = summary + .witness_outcomes + .iter() + .map(|o| o.entry.as_str()) + .collect::>() + .len(); + summary.selection_categorization_reason = selection_categorization_reason; summary } @@ -19597,6 +19788,9 @@ fn merge_discovery_summaries(summaries: Vec) -> DiscoverySumma performance_receipts: Vec::new(), total_measured_nanos: 0, roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, }; for summary in summaries { merged.total += summary.total; @@ -19861,6 +20055,9 @@ fn run_discovery_rows( performance_receipts: Vec::new(), total_measured_nanos: 0, roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, }; let skip_enabled = selection != NodeFrontierSelectionMode::Off; // This shard's SUBJECT union closure, accumulated from the graphs it resolves. An ordinary @@ -24258,11 +24455,13 @@ mod moduleless_entry_skip_tests { #[cfg(test)] mod discovery_summary_merge_tests { use super::{ - compute_histogram_data, merge_discovery_summaries, project_witness_cost_receipt, + compute_histogram_data, finalize_discovery_summary, merge_discovery_summaries, + project_witness_cost_receipt, render_selection_degradation_receipt_line, repo_relative_dag_path, run_discovery_corpus_with_options, top_n_slowest_witnesses, - witness_execution_leg_cache_put, ClaimOutcome, DiscoveryCorpusOptions, DiscoverySummary, - DiscoveryWidthPolicy, DiscoveryWitnessOutcome, EntryResolveReceipt, - NodeFrontierSelectionMode, ResolveStageNanos, + witness_execution_leg_cache_put, ClaimOutcome, DiscoveryCorpusOptions, DiscoveryRow, + DiscoverySummary, DiscoveryWidthPolicy, DiscoveryWitnessOutcome, EntryResolveReceipt, + NodeFrontierSelectionMode, ResolveStageNanos, SelectionDegradationSnapshot, + SelectionSkippedDiscoveryRow, }; use crate::v1_interpreter::{ExecutionMode, PerformanceReceipt}; @@ -24351,6 +24550,9 @@ mod discovery_summary_merge_tests { ], total_measured_nanos: 56_000, roster_closure_nodes: 42, + total_entry_groups: 2, + selected_entry_groups: 2, + selection_categorization_reason: None, } } @@ -24368,6 +24570,93 @@ mod discovery_summary_merge_tests { assert_eq!(merged.roster_closure_nodes, 71); } + /// RED control: skip-before-resolve records `selection_skipped_rows` but does NOT add + /// `witness_outcomes` for those entries. `selected_entry_groups` must count executed + /// entry groups only — otherwise narrowed Applied runs mislabel as Superset. + #[test] + fn finalize_discovery_summary_selected_entry_groups_exclude_skip_before_resolve() { + let rows = vec![ + DiscoveryRow { + label: "w1".into(), + entry: "entry_a.dag".into(), + function: "f1".into(), + reads_live_tree: false, + }, + DiscoveryRow { + label: "w2".into(), + entry: "entry_b.dag".into(), + function: "f2".into(), + reads_live_tree: false, + }, + DiscoveryRow { + label: "w3".into(), + entry: "entry_c.dag".into(), + function: "f3".into(), + reads_live_tree: false, + }, + ]; + let summary = DiscoverySummary { + total: 2, + passed: 2, + skipped: 1, + deferred_rows: Vec::new(), + predicted_unaffected: Vec::new(), + selection_skipped_rows: vec![SelectionSkippedDiscoveryRow { + entry: "entry_c.dag".into(), + function: "f3".into(), + provenance: "skip-before-resolve-fast-path".into(), + }], + divergences: Vec::new(), + failures: Vec::new(), + witness_outcomes: vec![ + DiscoveryWitnessOutcome { + entry: "entry_a.dag".into(), + module_path: "test.a".into(), + function: "f1".into(), + outcome: ClaimOutcome::Pass, + execution_leg: "InterpretedLeg".into(), + }, + DiscoveryWitnessOutcome { + entry: "entry_b.dag".into(), + module_path: "test.b".into(), + function: "f2".into(), + outcome: ClaimOutcome::Pass, + execution_leg: "InterpretedLeg".into(), + }, + ], + entry_resolve_receipts: Vec::new(), + total_resolve_nanos: 0, + total_stage_nanos: ResolveStageNanos::default(), + performance_receipts: Vec::new(), + total_measured_nanos: 0, + roster_closure_nodes: 0, + total_entry_groups: 0, + selected_entry_groups: 0, + selection_categorization_reason: None, + }; + let finalized = finalize_discovery_summary( + summary, + &rows, + NodeFrontierSelectionMode::Applied, + None, + Vec::new(), + ); + assert_eq!(finalized.total_entry_groups, 3); + assert_eq!(finalized.selected_entry_groups, 2); + let snapshot = SelectionDegradationSnapshot::from_summary( + NodeFrontierSelectionMode::Applied, + &finalized, + ); + let line = + render_selection_degradation_receipt_line(&source_roots(), &snapshot).expect("receipt"); + assert!( + line.contains("selection_state=SelectionApplied"), + "skip-before-resolve must not inflate selected to total (would read Superset): {line}" + ); + assert!(line.contains("selected_entry_groups=2")); + assert!(line.contains("total_entry_groups=3")); + } + #[test] fn production_witness_cost_projection_preserves_failed_and_refused() { let mut summary = sample_summary(); diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index df7ae7849ea..fe73507c4b3 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -14091,6 +14091,9 @@ pub fn emit_fn_def( return_is_bare_generic.clone(), ret_name.clone(), body_is_param_ref.clone(), + inferred.clone(), + emit_info.clone_bounded_type_params.clone(), + emit_info.type_decl_items.clone(), si.clone(), ); let needs_clone_bound = ((clone_param_names.clone().len() as i64) > 0);