From 5c593b2e4e1afb36b5db54c88dcaca6594554d74 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 10:14:47 +0000 Subject: [PATCH 1/3] Restore the item-grain Rust census that went to zero undeclared, and make it the changed-item denominator the seed-growth admission join was waiting for The census had one realization, scripts/rust_item_census.py, and gunbc#9132 deleted it with the measurement corpus. Nothing replaced it, so from 2026-08-24 no route in this repository could enumerate Rust items at identity grain -- and no rung drop was declared, because the census's consumers were lanes rather than modules and no dependent could refuse. The scaffold protecting it is a live instance of the rule DESIGN 4b(3) added on 2026-08-26: its trigger read "producer lands and enrolls; Python script deleted", a conjunction whose second half an unrelated cut satisfied while the producer never landed. gunbc.rust_item_scan reads items from rustfmt-normalized source with its item-keyword population taken FROM extdeps.languages.rust.syntax rust_item_forms rather than re-coined -- the deleted script's regex ITEM_PATTERNS were exactly the second roster this reader refuses to author, and the census predicted its own failure from it. It is not a parser and does not claim to be: it refuses on any structural line it cannot classify, which is what makes an incomplete recognizer safe rather than quietly smaller. gunbc.rust_item_host_observation supplies TWO populations from TWO functions, which is the condition gunbc.seed_growth_admission's own forward-freeze row set on enrolment. seed_growth_join_declarations is replaced by the split pair plus seed_growth_admit_change; an unattributed hand-Rust change REFUSES rather than reporting an empty delta; deletions offset nothing; ExistingSeedItemModified carries capability_origin, with no value naming a capability originated in Rust. Rung: mechanically preventable AS TO THE ROUTE and no higher -- the wall executes under witnesses and no required CI phase invokes it yet. Arming it is a separate change. The terminal seed-bridge roster is deliberately not authored here. Hand-Rust delta: zero. Every file is .dag. --- dag/gunbc/rust_item_host_observation.dag | 1004 +++++++++++++++ dag/gunbc/rust_item_identity.dag | 26 +- dag/gunbc/rust_item_scan.dag | 1128 +++++++++++++++++ dag/gunbc/seed_growth_admission.dag | 223 +++- ...ust_item_host_observation_witness_test.dag | 356 ++++++ .../claim/rust_item_identity_witness_test.dag | 64 +- .../claim/rust_item_scan_witness_test.dag | 259 ++++ .../seed_growth_admission_witness_test.dag | 62 +- dag/tools/rust_item_census_instrument.dag | 203 +++ 9 files changed, 3224 insertions(+), 101 deletions(-) create mode 100644 dag/gunbc/rust_item_host_observation.dag create mode 100644 dag/gunbc/rust_item_scan.dag create mode 100644 dag/test/claim/rust_item_host_observation_witness_test.dag create mode 100644 dag/test/claim/rust_item_scan_witness_test.dag create mode 100644 dag/tools/rust_item_census_instrument.dag diff --git a/dag/gunbc/rust_item_host_observation.dag b/dag/gunbc/rust_item_host_observation.dag new file mode 100644 index 00000000000..50b80a75394 --- /dev/null +++ b/dag/gunbc/rust_item_host_observation.dag @@ -0,0 +1,1004 @@ +module gunbc.rust_item_host_observation + +import extdeps.git +import gunbc.output_policy { ExpectSuccess } +import gunbc.repo_identity { gunbc_merge_target_ref } +import gunbc.generated_artifact { committed_generated_artifact_paths } +import gunbc.stage0_rust_host_observation { + EmitPopulationConfirmed, + EmitPopulationEvidence, + EmitPopulationUnobserved, + observe_emit_population_evidence +} +import gunbc.rust_item_identity { + RustImplBlock, + RustImplMethod, + RustItemIdentity, + rust_item_identity_key, + rust_item_identity_qualified_name +} +import gunbc.rust_item_scan { + RustFileScan, + RustFileScanned, + RustFileScanRefused, + RustMacroRegion, + attribute_scanned_line, + AttributedToItem, + AttributedToFileLevel, + AttributedToUnnamedMacroBlock, + AttributionOutOfRange, + scan_rust_source +} +import std.algebra { trim } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.types { Bool, GitRef, Int, List, NonEmptyStr, Set, String } +import v2.std.algebra { HeadAbsent, HeadFound, any, filter, fold_list, length, list_append, list_head, list_map, list_snoc_item } + +// TWO POPULATIONS, TWO FUNCTIONS, AND THEY MAY NOT BE COLLAPSED. +// +// gunbc.seed_growth_admission's own forward-freeze row states this as a condition on enrolment, +// learned the hard way: seed_growth_join_declarations used ONE candidate set both to find +// unjustified additions and to declare roster rows stale, so a changed-item population makes every +// justification for an untouched item look stale, while a live population no longer isolates what +// the change added. The two questions have different denominators -- "what did this change add" +// is over merge-base-to-HEAD, "which roster rows still name something real" is over the current +// surface -- so this module answers them with two observations that never substitute for one +// another. +// +// THE FAIL-CLOSED RULE THIS MODULE EXISTS TO HOLD: a changed hand-written Rust region that cannot +// be attributed to an item REFUSES. It does not report an empty delta. An unattributed change +// rendered as "nothing changed" is the empty-observation narrow DESIGN's failure-mode list names -- +// strictly worse than the widen section 5 forbids, because a widen is merely expensive and a narrow +// is silently uncovered, and it is the one failure mode that would make the admission join read +// green while admitting exactly what it exists to refuse. +data rust_item_host_observation_note: String = "Item-grain Rust observation for seed-growth admission. Supplies TWO populations from two functions: RustItemChangeObservation over merge-base-to-HEAD (added, modified and deleted hand items plus generated-path changes) and LiveHandRustObservation over the current hand-Rust surface. Item extraction is gunbc.rust_item_scan; generated-versus-hand classification is derived from gunbc.stage0_rust_source_lifecycle_scaffold's emitted population and gunbc.generated_artifact, never hand-listed. Every arm that cannot attribute a changed hand-Rust region is a typed, located refusal; none returns an empty delta." + +// THE NAMESPACE MAP IS A DECLARED TABLE BECAUSE THE CITATIONS IT MUST JOIN AGAINST ALREADY EXIST. +// The roster in gunbc.seed_growth_admission cites v1_compiler.cli_run, v1_compiler.tests. +// declaration_index_integrity and v1_compiler_tests.anonymous_record_struct_resolution_test; those +// three spellings are three different repository prefixes, so the mapping is a fact about the +// authored corpus and is written down where it can be read, rather than inferred from a path shape. +// A path matching no row REFUSES: minting a plausible namespace for an unmapped tree would produce +// a DeclarationRef that joins against nothing while looking exactly like one that does. +type RustSourceNamespaceRow { + repo_prefix: String + namespace_prefix: String +} + +data rust_source_namespace_rows: List = [ + RustSourceNamespaceRow { repo_prefix: "src/v1/stage0/src/bin/", namespace_prefix: "v1_compiler.bin." }, + RustSourceNamespaceRow { repo_prefix: "src/v1/stage0/src/", namespace_prefix: "v1_compiler." }, + RustSourceNamespaceRow { repo_prefix: "src/v1/stage0/tests/", namespace_prefix: "v1_compiler.tests." }, + RustSourceNamespaceRow { repo_prefix: "src/v1/tests/src/", namespace_prefix: "v1_compiler_tests." } +] + +type RustModulePathProjection + = RustModulePathProjected { module_path: String } + | RustModulePathRefused { repo_path: String, detail: String } + +fn path_replace_slashes(s: String) -> String { + replace(s: s, pattern: "/", replacement: ".") +} + +fn strip_rs_suffix(s: String) -> String { + let n = string_length(s: s) + if n > 3 && substring(s: s, start: n - 3, end: n) == ".rs" { + substring(s: s, start: 0, end: n - 3) + } else { + s + } +} + +// LONGEST PREFIX WINS, AND THE FOLD IS WHY THE ROWS ABOVE MAY BE AUTHORED IN ANY ORDER. +// src/v1/stage0/src/bin/ is a subtree of src/v1/stage0/src/, so a first-match reader would give +// every binary the library's namespace and the row order would silently be load-bearing. +fn rust_namespace_row_for_path(repo_path: String) -> List { + fold_list( + xs: rust_source_namespace_rows, + empty: [] as List, + cons: fn(best, row) { + if !starts_with(s: repo_path, prefix: row.repo_prefix) { + best + } else { + fold_list( + xs: best, + empty: [row], + cons: fn(_acc, chosen) { + if string_length(s: row.repo_prefix) > string_length(s: chosen.repo_prefix) { + [row] + } else { + [chosen] + } + } + ) + } + } + ) +} + +fn rust_module_path_for_repo_path(repo_path: String) -> RustModulePathProjection { + fold_list( + xs: rust_namespace_row_for_path(repo_path: repo_path), + empty: RustModulePathRefused { + repo_path: repo_path, + detail: "no rust_source_namespace_rows prefix claims this path, so no module path can be derived for it" + }, + cons: fn(_acc, row) { + let relative = substring( + s: repo_path, + start: string_length(s: row.repo_prefix), + end: string_length(s: repo_path) + ) + let stem = path_replace_slashes(s: strip_rs_suffix(s: relative)) + if stem == "" { + RustModulePathRefused { repo_path: repo_path, detail: "path carries no module stem" } + } else { + RustModulePathProjected { module_path: concat(row.namespace_prefix, stem) } + } + } + ) +} + +// AN IMPL METHOD AND AN IMPL BLOCK ARE REAL ITEMS AND ARE NOT CITABLE, so they are projected into +// their own arm rather than dropped or forced into a DeclarationRef they do not fit. std.decl_ref +// DeclField offers WholeDeclaration or NamedField and a trait-impl method is neither -- the +// repository has carried exactly one such item in prose since the Cargo-manifest bridge, and two +// earlier attempts to spell it anyway (an invented symbol, then a NamedField naming a field that +// does not exist) are recorded there as the workaround class. The join reports this population +// separately, as gunbc.seed_growth_admission already does for the prose-only item. +type ItemDeclarationRefProjection + = ItemDeclarationRefProjected { ref: DeclarationRef } + | ItemDeclarationRefUncitable { item_key: String, detail: String } + | ItemDeclarationRefRefused { item_key: String, detail: String } + +fn item_declaration_ref(item: RustItemIdentity) -> ItemDeclarationRefProjection { + let key = rust_item_identity_key(item: item) + if item.kind == RustImplMethod || item.kind == RustImplBlock { + ItemDeclarationRefUncitable { + item_key: key, + detail: "std.decl_ref DeclField cannot name a method on an impl block, nor an unnamed impl block itself" + } + } else { + match rust_module_path_for_repo_path(repo_path: item.repo_path as String) { + RustModulePathRefused { repo_path: _, detail: detail } => + ItemDeclarationRefRefused { item_key: key, detail: detail } + RustModulePathProjected { module_path: module_path } => + if item.enclosing_module == "" { + ItemDeclarationRefProjected { + ref: DeclarationRef { + module_path: module_path as NonEmptyStr, + decl_name: item.name, + field: WholeDeclaration + } + } + } else { + ItemDeclarationRefProjected { + ref: DeclarationRef { + module_path: concat(module_path, concat(".", item.enclosing_module)) as NonEmptyStr, + decl_name: item.name, + field: WholeDeclaration + } + } + } + } + } +} + +type LineRange { + start: Int + count: Int +} + +type RustFileChangedRanges { + repo_path: String + old_ranges: List + new_ranges: List +} + +fn line_range_contains(range: LineRange, line: Int) -> Bool { + range.count > 0 && line >= range.start && line <= range.start + range.count - 1 +} + +fn any_range_contains(ranges: List, line: Int) -> Bool { + any(xs: ranges, predicate: fn(r) { line_range_contains(range: r, line: line) }) +} + +fn ranges_overlap_item(ranges: List, item: RustItemIdentity) -> Bool { + any( + xs: ranges, + predicate: fn(r) { + r.count > 0 + && r.start <= item.start_line + item.loc - 1 + && r.start + r.count - 1 >= item.start_line + } + ) +} + +fn range_line_numbers(range: LineRange, at: Int, acc: List) -> List { + if at >= range.count { + acc + } else { + range_line_numbers(range: range, at: at + 1, acc: list_snoc_item(xs: acc, item: range.start + at)) + } +} + +fn ranges_line_numbers(ranges: List) -> List { + fold_list( + xs: ranges, + empty: [] as List, + cons: fn(acc, r) { list_append(left: acc, right: range_line_numbers(range: r, at: 0, acc: [])) } + ) +} + +// THE HUNK HEADER IS GIT'S OWN FORMAT AND IS READ AS SUCH. `@@ -a,b +c,d @@` with the count elided +// meaning one -- that is the unified-diff format the operation's own argv pins with -U0, cited to +// the transport rather than guessed at. A header this reader cannot decode does NOT become an empty +// range set: it refuses, because an undecoded hunk is precisely a changed region nobody attributed. +type HunkRangeParse + = HunkRangeParsed { old_range: LineRange, new_range: LineRange } + | HunkRangeRefused { header: String, detail: String } + +fn digits_from(s: String, at: Int, acc: String) -> String { + if at >= string_length(s: s) { + acc + } else { + let c = char_at(s: s, index: at) + if c >= "0" && c <= "9" { + digits_from(s: s, at: at + 1, acc: concat(acc, c)) + } else { + acc + } + } +} + +type RangeTokenParse + = RangeTokenParsed { range: LineRange } + | RangeTokenRefused { token: String } + +fn parse_range_token(token: String, sign: String) -> RangeTokenParse { + if !starts_with(s: token, prefix: sign) { + RangeTokenRefused { token: token } + } else { + let body = substring(s: token, start: 1, end: string_length(s: token)) + let start_digits = digits_from(s: body, at: 0, acc: "") + if start_digits == "" { + RangeTokenRefused { token: token } + } else { + let rest = substring(s: body, start: string_length(s: start_digits), end: string_length(s: body)) + let count_digits = if starts_with(s: rest, prefix: ",") { + digits_from(s: rest, at: 1, acc: "") + } else { + "1" + } + if starts_with(s: rest, prefix: ",") && count_digits == "" { + RangeTokenRefused { token: token } + } else { + match parse_int(s: start_digits) { + Absent => RangeTokenRefused { token: token } + Present { value: start } => + match parse_int(s: count_digits) { + Absent => RangeTokenRefused { token: token } + Present { value: count } => RangeTokenParsed { range: LineRange { start: start, count: count } } + } + } + } + } + } +} + +fn first_token_with_prefix(tokens: List, prefix: String) -> String { + match list_head(xs: filter(xs: tokens, predicate: fn(t) { starts_with(s: t, prefix: prefix) })) { + HeadAbsent => "" + HeadFound { value: token } => token + } +} + +fn parse_hunk_header(header: String) -> HunkRangeParse { + let body = trim(s: substring(s: header, start: 2, end: string_length(s: header))) + let tokens = filter(xs: split(s: body, delimiter: " "), predicate: fn(t) { t != "" }) + let old_token = first_token_with_prefix(tokens: tokens, prefix: "-") + let new_token = first_token_with_prefix(tokens: tokens, prefix: "+") + match parse_range_token(token: old_token, sign: "-") { + RangeTokenRefused { token: _ } => + HunkRangeRefused { header: header, detail: "hunk header carries no decodable old range" } + RangeTokenParsed { range: old_range } => + match parse_range_token(token: new_token, sign: "+") { + RangeTokenRefused { token: _ } => + HunkRangeRefused { header: header, detail: "hunk header carries no decodable new range" } + RangeTokenParsed { range: new_range } => + HunkRangeParsed { old_range: old_range, new_range: new_range } + } + } +} + +// THE CURRENT FILE'S RANGES ACCUMULATE IN THE STATE AND ARE FLUSHED ONCE, rather than being merged +// into the files list per hunk. The per-hunk form walked every file already collected on every +// hunk, which is a fold whose cost is files times hunks -- the copied-accumulator shape DESIGN +// section 6 says is always fixed regardless of the realized n, and here n is a real diff. +type DiffRangeFoldState { + files: List + current_path: String + current_old: List + current_new: List + refusal_header: String + refusal_detail: String +} + +fn diff_state_flush(state: DiffRangeFoldState) -> DiffRangeFoldState { + if state.current_path == "" { + state + } else { + DiffRangeFoldState { + files: list_snoc_item( + xs: state.files, + item: RustFileChangedRanges { + repo_path: state.current_path, + old_ranges: state.current_old, + new_ranges: state.current_new + } + ), + current_path: "", + current_old: [], + current_new: [], + refusal_header: state.refusal_header, + refusal_detail: state.refusal_detail + } + } +} + +fn diff_state_refused(state: DiffRangeFoldState) -> Bool { + state.refusal_detail != "" +} + +fn path_is_rust_source(path: String) -> Bool { + let n = string_length(s: path) + n > 3 && substring(s: path, start: n - 3, end: n) == ".rs" +} + +fn diff_git_header_new_path(line: String) -> String { + let tokens = filter(xs: split(s: line, delimiter: " "), predicate: fn(t) { t != "" }) + let last = fold_list(xs: tokens, empty: "", cons: fn(_acc, t) { t }) + if starts_with(s: last, prefix: "b/") { + substring(s: last, start: 2, end: string_length(s: last)) + } else { + last + } +} + +fn diff_state_record_hunk( + state: DiffRangeFoldState, + old_range: LineRange, + new_range: LineRange +) -> DiffRangeFoldState { + DiffRangeFoldState { + files: state.files, + current_path: state.current_path, + current_old: list_snoc_item(xs: state.current_old, item: old_range), + current_new: list_snoc_item(xs: state.current_new, item: new_range), + refusal_header: state.refusal_header, + refusal_detail: state.refusal_detail + } +} + +fn diff_state_enter_path(state: DiffRangeFoldState, path: String) -> DiffRangeFoldState { + let flushed = diff_state_flush(state: state) + DiffRangeFoldState { + files: flushed.files, + current_path: path, + current_old: [], + current_new: [], + refusal_header: flushed.refusal_header, + refusal_detail: flushed.refusal_detail + } +} + +fn diff_range_fold_line(state: DiffRangeFoldState, line: String) -> DiffRangeFoldState { + if diff_state_refused(state: state) { + state + } else if starts_with(s: line, prefix: "diff --git ") { + let path = diff_git_header_new_path(line: line) + if path_is_rust_source(path: path) { + diff_state_enter_path(state: state, path: path) + } else { + diff_state_flush(state: state) + } + } else if starts_with(s: line, prefix: "@@") && state.current_path != "" { + match parse_hunk_header(header: line) { + HunkRangeRefused { header: header, detail: detail } => + DiffRangeFoldState { + files: state.files, + current_path: state.current_path, + current_old: state.current_old, + current_new: state.current_new, + refusal_header: header, + refusal_detail: detail + } + HunkRangeParsed { old_range: old_range, new_range: new_range } => + diff_state_record_hunk(state: state, old_range: old_range, new_range: new_range) + } + } else { + state + } +} + +type RustDiffRangeObservation + = RustDiffRangesObserved { files: List } + | RustDiffRangesRefused { header: String, detail: String } + +fn rust_changed_ranges(diff_text: String) -> RustDiffRangeObservation { + let final = fold_list( + xs: split(s: diff_text, delimiter: "\n"), + empty: DiffRangeFoldState { + files: [], + current_path: "", + current_old: [], + current_new: [], + refusal_header: "", + refusal_detail: "" + }, + cons: fn(acc, line) { diff_range_fold_line(state: acc, line: line) } + ) + if diff_state_refused(state: final) { + RustDiffRangesRefused { header: final.refusal_header, detail: final.refusal_detail } + } else { + RustDiffRangesObserved { files: diff_state_flush(state: final).files } + } +} + +// THE CHANGE OBSERVATION'S PURE CORE TAKES ITS FILES ALREADY READ, so every arm below is reachable +// from a fixture. The wet half is one function at the bottom of this module and does nothing but +// fetch: git decides which paths changed and what their bytes are at each end, and every decision +// about what that MEANS is made here, where a witness can plant it. +type RustFileChangeInput { + repo_path: String + base_content: String + head_content: String + present_at_base: Bool + present_at_head: Bool + old_ranges: List + new_ranges: List +} + +type RustItemChangeObservation + = RustItemChangeObserved { + added_hand_items: List, + modified_hand_items: List, + deleted_hand_items: List, + generated_changes: List + } + | RustItemChangeUnattributed { repo_path: String, line: Int, detail: String } + | RustItemChangeScanRefused { repo_path: String, line: Int, line_text: String, detail: String } + | RustItemChangeHostRefused { detail: String } + +type RustChangeFoldState { + added: List + modified: List + deleted: List + generated: List + refusal: RustItemChangeObservation + refused: Bool +} + +fn change_state_refuse(state: RustChangeFoldState, refusal: RustItemChangeObservation) -> RustChangeFoldState { + if state.refused { + state + } else { + RustChangeFoldState { + added: state.added, + modified: state.modified, + deleted: state.deleted, + generated: state.generated, + refusal: refusal, + refused: true + } + } +} + +// MEMBERSHIP IS A SET LOOKUP AND NOT A LINEAR SCAN PER ITEM. The scan-per-item form is a fold whose +// cost is items squared, and cli_run.rs alone carries items in the thousands; DESIGN section 6's +// standing rule is that a proven cost shape is fixed regardless of the realized n, because n is not +// a time-stable fact. +fn item_key_set(items: List) -> Set { + fold_list( + xs: items, + empty: empty_set() as Set, + cons: fn(acc, i) { set_insert(acc, rust_item_identity_key(item: i)) } + ) +} + +// EVERY CHANGED LINE IS ATTRIBUTED, AND THE ATTRIBUTION IS CHECKED RATHER THAN ASSUMED. Attributing +// to an item is what makes a modification visible; attributing to a file-level line is what keeps a +// `use` edit from being reported as a capability change. The third answer -- a line the scan cannot +// place -- is the refusal this whole carrier exists to produce, and it is asked for every changed +// line rather than inferred from the file's shape. +fn change_lines_unattributed( + items: List, + macro_regions: List, + line_count: Int, + lines: List +) -> List { + filter( + xs: lines, + predicate: fn(line) { + match attribute_scanned_line( + items: items, + macro_regions: macro_regions, + line_count: line_count, + line: line + ) { + AttributionOutOfRange { line: _, line_count: _ } => true + AttributedToUnnamedMacroBlock { macro_name: _, line: _ } => true + AttributedToItem { item_key: _ } => false + AttributedToFileLevel => false + } + } + ) +} + +fn change_state_with_generated(state: RustChangeFoldState, path: String) -> RustChangeFoldState { + RustChangeFoldState { + added: state.added, + modified: state.modified, + deleted: state.deleted, + generated: list_snoc_item(xs: state.generated, item: path), + refusal: state.refusal, + refused: state.refused + } +} + +fn change_state_with_items( + state: RustChangeFoldState, + added: List, + modified: List, + deleted: List +) -> RustChangeFoldState { + RustChangeFoldState { + added: list_append(left: state.added, right: added), + modified: list_append(left: state.modified, right: modified), + deleted: list_append(left: state.deleted, right: deleted), + generated: state.generated, + refusal: state.refusal, + refused: state.refused + } +} + +fn change_fold_hand_file( + state: RustChangeFoldState, + file_change: RustFileChangeInput, + base_scan: RustFileScan, + head_scan: RustFileScan +) -> RustChangeFoldState { + match head_scan { + RustFileScanRefused { repo_path: path, line: line, line_text: text, detail: detail } => + change_state_refuse( + state: state, + refusal: RustItemChangeScanRefused { repo_path: path, line: line, line_text: text, detail: detail } + ) + RustFileScanned { + repo_path: _, + items: head_items, + macro_regions: head_macro_regions, + line_count: head_line_count + } => + match base_scan { + RustFileScanRefused { repo_path: path, line: line, line_text: text, detail: detail } => + change_state_refuse( + state: state, + refusal: RustItemChangeScanRefused { repo_path: path, line: line, line_text: text, detail: detail } + ) + RustFileScanned { + repo_path: _, + items: base_items, + macro_regions: base_macro_regions, + line_count: base_line_count + } => { + let unattributed_new = change_lines_unattributed( + items: head_items, + macro_regions: head_macro_regions, + line_count: head_line_count, + lines: ranges_line_numbers(ranges: file_change.new_ranges) + ) + let unattributed_old = change_lines_unattributed( + items: base_items, + macro_regions: base_macro_regions, + line_count: base_line_count, + lines: ranges_line_numbers(ranges: file_change.old_ranges) + ) + if length(xs: unattributed_new) > 0 { + change_state_refuse( + state: state, + refusal: fold_list( + xs: unattributed_new, + empty: RustItemChangeHostRefused { detail: "unattributed line list was non-empty and then empty" }, + cons: fn(_acc, line) { + RustItemChangeUnattributed { + repo_path: file_change.repo_path, + line: line, + detail: "changed line at HEAD falls outside every scanned item and outside the file's recognized line range" + } + } + ) + ) + } else if length(xs: unattributed_old) > 0 { + change_state_refuse( + state: state, + refusal: fold_list( + xs: unattributed_old, + empty: RustItemChangeHostRefused { detail: "unattributed line list was non-empty and then empty" }, + cons: fn(_acc, line) { + RustItemChangeUnattributed { + repo_path: file_change.repo_path, + line: line, + detail: "changed line at the merge base falls outside every scanned item and outside the file's recognized line range" + } + } + ) + ) + } else { + let base_keys = item_key_set(items: base_items) + let head_keys = item_key_set(items: head_items) + change_state_with_items( + state: state, + added: filter( + xs: head_items, + predicate: fn(i) { !set_contains(base_keys, rust_item_identity_key(item: i)) } + ), + modified: filter( + xs: head_items, + predicate: fn(i) { + set_contains(base_keys, rust_item_identity_key(item: i)) + && ranges_overlap_item(ranges: file_change.new_ranges, item: i) + } + ), + deleted: filter( + xs: base_items, + predicate: fn(i) { !set_contains(head_keys, rust_item_identity_key(item: i)) } + ) + ) + } + } + } + } +} + +fn change_fold_file( + state: RustChangeFoldState, + generated_repo_paths: List, + file_change: RustFileChangeInput +) -> RustChangeFoldState { + if state.refused { + state + } else if any(xs: generated_repo_paths, predicate: fn(p) { p == file_change.repo_path }) { + change_state_with_generated(state: state, path: file_change.repo_path) + } else { + change_fold_hand_file( + state: state, + file_change: file_change, + base_scan: if file_change.present_at_base { + scan_rust_source(repo_path: file_change.repo_path, content: file_change.base_content) + } else { + RustFileScanned { repo_path: file_change.repo_path, items: [], macro_regions: [], line_count: 0 } + }, + head_scan: if file_change.present_at_head { + scan_rust_source(repo_path: file_change.repo_path, content: file_change.head_content) + } else { + RustFileScanned { repo_path: file_change.repo_path, items: [], macro_regions: [], line_count: 0 } + } + ) + } +} + +fn observe_rust_item_change( + generated_repo_paths: List, + inputs: List +) -> RustItemChangeObservation { + let final = fold_list( + xs: inputs, + empty: RustChangeFoldState { + added: [], + modified: [], + deleted: [], + generated: [], + refusal: RustItemChangeHostRefused { detail: "no refusal recorded" }, + refused: false + }, + cons: fn(acc, file_change) { + change_fold_file(state: acc, generated_repo_paths: generated_repo_paths, file_change: file_change) + } + ) + if final.refused { + final.refusal + } else { + RustItemChangeObserved { + added_hand_items: final.added, + modified_hand_items: final.modified, + deleted_hand_items: final.deleted, + generated_changes: final.generated + } + } +} + +// THE LIVE OBSERVATION IS A SECOND POPULATION AND NOT A PROJECTION OF THE FIRST. It answers "which +// hand Rust items exist right now", which is what a staleness question needs and what a change +// population cannot supply. A file it cannot scan is REPORTED, counted, with its refusal -- never +// dropped, because a live surface silently missing the files that refuse is a smaller population +// wearing the shape of a complete one. +type LiveHandRustObservation + = LiveHandRustObserved { + items: List, + unscannable: List + } + | LiveHandRustHostRefused { detail: String } + +type LiveFoldState { + items: List + unscannable: List +} + +fn observe_live_hand_rust(sources: List, generated_repo_paths: List) -> LiveHandRustObservation { + let final = fold_list( + xs: filter( + xs: sources, + predicate: fn(file_change) { + file_change.present_at_head && !any(xs: generated_repo_paths, predicate: fn(p) { p == file_change.repo_path }) + } + ), + empty: LiveFoldState { items: [], unscannable: [] }, + cons: fn(acc, file_change) { + match scan_rust_source(repo_path: file_change.repo_path, content: file_change.head_content) { + RustFileScanned { repo_path: p, items: items, macro_regions: _, line_count: n } => + LiveFoldState { + items: list_append(left: acc.items, right: items), + unscannable: acc.unscannable + } + RustFileScanRefused { repo_path: p, line: line, line_text: text, detail: detail } => + LiveFoldState { + items: acc.items, + unscannable: list_snoc_item( + xs: acc.unscannable, + item: RustFileScanRefused { repo_path: p, line: line, line_text: text, detail: detail } + ) + } + } + } + ) + LiveHandRustObserved { items: final.items, unscannable: final.unscannable } +} + +fn change_observation_added_refs(observation: RustItemChangeObservation) -> List { + match observation { + RustItemChangeObserved { + added_hand_items: added, + modified_hand_items: _, + deleted_hand_items: _, + generated_changes: _ + } => list_map(xs: added, f: fn(item) { item_declaration_ref(item: item) }) + RustItemChangeUnattributed { repo_path: _, line: _, detail: _ } => [] + RustItemChangeScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => [] + RustItemChangeHostRefused { detail: _ } => [] + } +} + +fn projected_declaration_refs(projections: List) -> List { + fold_list( + xs: projections, + empty: [] as List, + cons: fn(acc, p) { + match p { + ItemDeclarationRefProjected { ref: ref } => list_snoc_item(xs: acc, item: ref) + ItemDeclarationRefUncitable { item_key: _, detail: _ } => acc + ItemDeclarationRefRefused { item_key: _, detail: _ } => acc + } + } + ) +} + +fn projected_uncitable_keys(projections: List) -> List { + fold_list( + xs: projections, + empty: [] as List, + cons: fn(acc, p) { + match p { + ItemDeclarationRefUncitable { item_key: key, detail: _ } => list_snoc_item(xs: acc, item: key) + ItemDeclarationRefProjected { ref: _ } => acc + ItemDeclarationRefRefused { item_key: _, detail: _ } => acc + } + } + ) +} + +fn projected_refused_keys(projections: List) -> List { + fold_list( + xs: projections, + empty: [] as List, + cons: fn(acc, p) { + match p { + ItemDeclarationRefRefused { item_key: key, detail: _ } => list_snoc_item(xs: acc, item: key) + ItemDeclarationRefProjected { ref: _ } => acc + ItemDeclarationRefUncitable { item_key: _, detail: _ } => acc + } + } + ) +} + +// THE WET HALF, AND IT IS A FUNCTION RATHER THAN A TOP-LEVEL data FOR THE REASON +// gunbc.stage0_rust_host_observation records at length: a top-level data declaration is EVALUATED +// WHEN A MODULE IMPORTS IT, so a dead import alone would perform Git I/O under the hermetic +// envelope and fail an entire discovery batch. As a function the effect is reachable only by +// calling it, and every type and fold above may be imported and exercised without it. +data rust_item_change_repository_path: String = "." + +// THE GENERATED POPULATION IS THE UNION OF TWO AUTHORITIES AND NEITHER MAY BE ASSUMED. The +// generated-artifact registry owns the drift-gated artifacts; the emitted-population manifest owns +// the stage0 mirrors the compiler writes. A change observation that consulted only the registry +// would classify every regenerated mirror as hand-authored and report the whole emit as unjustified +// additions -- so an UNOBSERVED emit population is a refusal here rather than a smaller generated +// set, exactly as gunbc.stage0_rust_host_observation's own carrier requires of its consumers. +type GeneratedPopulation + = GeneratedPopulationObserved { repo_paths: List } + | GeneratedPopulationUnobserved { detail: String } + +fn generated_population_at(revision: String) -> GeneratedPopulation { + match observe_emit_population_evidence(revision: revision) { + EmitPopulationUnobserved { detail: detail } => + GeneratedPopulationUnobserved { detail: detail } + EmitPopulationConfirmed { emitted_stage0_repo_paths: emitted } => + GeneratedPopulationObserved { + repo_paths: list_append(left: emitted, right: committed_generated_artifact_paths()) + } + } +} + +fn git_refusal_detail(operation: String, stderr: String) -> String { + let detail = trim(s: stderr) + if detail == "" { + concat(operation, " refused without stderr") + } else { + concat(operation, concat(": ", detail)) + } +} + +fn observe_rust_item_change_against(base_ref: String) -> RustItemChangeObservation { + let head = git.Inspect.HeadCommit(expect: ExpectSuccess) + if !head.success || (head.sha as String) == "" { + RustItemChangeHostRefused { detail: "git.Inspect.HeadCommit refused or returned an empty revision" } + } else { + let revision = head.sha as String + let baseline = git.Inspect.MergeBase( + repository_path: rust_item_change_repository_path, + left: revision as GitRef, + right: base_ref as GitRef, + expect: ExpectSuccess + ) + if !baseline.success || (baseline.sha as String) == "" { + RustItemChangeHostRefused { + detail: git_refusal_detail(operation: "git.Inspect.MergeBase", stderr: baseline.stderr) + } + } else { + let diff = git.Core.DiffUnified0( + base: baseline.sha as GitRef, + head: revision as GitRef, + range: TwoDot, + expect: ExpectSuccess + ) + if !diff.success { + RustItemChangeHostRefused { detail: "git.Core.DiffUnified0 refused" } + } else { + match generated_population_at(revision: revision) { + GeneratedPopulationUnobserved { detail: detail } => + RustItemChangeHostRefused { + detail: concat( + "the emitted stage0 population was not observed, so generated Rust cannot be told from hand Rust: ", + detail + ) + } + GeneratedPopulationObserved { repo_paths: generated_repo_paths } => + match rust_changed_ranges(diff_text: diff.diff) { + RustDiffRangesRefused { header: header, detail: detail } => + RustItemChangeHostRefused { + detail: concat(detail, concat(" -- header: ", header)) + } + RustDiffRangesObserved { files: files } => + observe_rust_item_change( + generated_repo_paths: generated_repo_paths, + inputs: list_map( + xs: files, + f: fn(entry) { + let base_show = git.Core.Show( + ref: baseline.sha as GitRef, + path: entry.repo_path, + expect: ExpectSuccess + ) + let head_show = git.Core.Show( + ref: revision as GitRef, + path: entry.repo_path, + expect: ExpectSuccess + ) + RustFileChangeInput { + repo_path: entry.repo_path, + base_content: base_show.content, + head_content: head_show.content, + present_at_base: base_show.success, + present_at_head: head_show.success, + old_ranges: entry.old_ranges, + new_ranges: entry.new_ranges + } + } + ) + ) + } + } + } + } + } +} + +fn current_rust_item_change_observation() -> RustItemChangeObservation { + observe_rust_item_change_against(base_ref: gunbc_merge_target_ref as String) +} + +fn rust_paths_from_nul(raw: String) -> List { + filter( + xs: split(s: raw, delimiter: from_code_point(0)), + predicate: fn(path) { path_is_rust_source(path: path) } + ) +} + +// THE LIVE POPULATION IS SCOPED TO THE TREES THE NAMESPACE MAP CLAIMS, and that is a declared scope +// rather than a silent narrowing. A hand Rust file under a tree no row claims has no DeclarationRef +// at all, so including it in a STALENESS population would answer a question no roster row can ask; +// including it in an ADDITION population is a different matter and is not narrowed -- the change +// observation scans whatever the diff names, and an added item under an unclaimed tree REFUSES at +// the join rather than being skipped here. +fn live_hand_rust_candidate_paths(tracked_paths: List) -> List { + filter( + xs: tracked_paths, + predicate: fn(path) { + length(xs: rust_namespace_row_for_path(repo_path: path)) > 0 + } + ) +} + +fn observe_current_live_hand_rust() -> LiveHandRustObservation { + let head = git.Inspect.HeadCommit(expect: ExpectSuccess) + if !head.success || (head.sha as String) == "" { + LiveHandRustHostRefused { detail: "git.Inspect.HeadCommit refused or returned an empty revision" } + } else { + let revision = head.sha as String + let tree = git.Inspect.ListTreePathsAtRevision(ref: revision as GitRef, expect: ExpectSuccess) + if !tree.success { + LiveHandRustHostRefused { + detail: git_refusal_detail(operation: "git.Inspect.ListTreePathsAtRevision(HEAD)", stderr: tree.stderr) + } + } else { + match generated_population_at(revision: revision) { + GeneratedPopulationUnobserved { detail: detail } => + LiveHandRustHostRefused { + detail: concat( + "the emitted stage0 population was not observed, so generated Rust cannot be told from hand Rust: ", + detail + ) + } + GeneratedPopulationObserved { repo_paths: generated_repo_paths } => + observe_live_hand_rust( + generated_repo_paths: generated_repo_paths, + sources: list_map( + xs: live_hand_rust_candidate_paths(tracked_paths: rust_paths_from_nul(raw: tree.paths_nul)), + f: fn(path) { + let show = git.Core.Show(ref: revision as GitRef, path: path, expect: ExpectSuccess) + RustFileChangeInput { + repo_path: path, + base_content: "", + head_content: show.content, + present_at_base: false, + present_at_head: show.success, + old_ranges: [], + new_ranges: [] + } + } + ) + ) + } + } + } +} diff --git a/dag/gunbc/rust_item_identity.dag b/dag/gunbc/rust_item_identity.dag index a3e6e200472..73371ed08e0 100644 --- a/dag/gunbc/rust_item_identity.dag +++ b/dag/gunbc/rust_item_identity.dag @@ -18,11 +18,20 @@ type RustItemKind | RustImplMethod | RustTestFunction +// enclosing_module CARRIES THE INLINE-MODULE PATH BECAUSE THE CITATION VOCABULARY ALREADY DOES. +// gunbc.declaration_index_seed_growth cites seven declarations under +// v1_compiler.cli_run.bare_reference_scanner_tests -- an inline `mod` inside cli_run.rs whose +// members are cited individually -- so a repo_path plus a bare name cannot address them: two +// same-named items in two inline modules of one file would collide onto one key, and the join +// would silently treat an addition as already justified. It is "" for a file-level item, which is +// the ordinary case rather than an absence: a file-level item is not enclosed by anything, and +// there is no third state for the field to conflate. type RustItemIdentity { repo_path: FilePath kind: RustItemKind name: NonEmptyStr impl_subject: String + enclosing_module: String start_line: Int loc: Int } @@ -41,11 +50,22 @@ type RustItemDeclarationRef { field: RustItemDeclField } +fn rust_item_identity_qualified_name(item: RustItemIdentity) -> String { + if item.enclosing_module == "" { + item.name as String + } else { + concat(item.enclosing_module, concat(".", item.name as String)) + } +} + fn rust_item_identity_key(item: RustItemIdentity) -> String { if item.impl_subject == "" { concat( concat(item.repo_path as String, "::"), - concat(rust_item_kind_label(kind: item.kind), concat("::", item.name as String)) + concat( + rust_item_kind_label(kind: item.kind), + concat("::", rust_item_identity_qualified_name(item: item)) + ) ) } else { concat( @@ -54,7 +74,7 @@ fn rust_item_identity_key(item: RustItemIdentity) -> String { rust_item_kind_label(kind: item.kind), concat( "::", - concat(item.impl_subject, concat("::", item.name as String)) + concat(item.impl_subject, concat("::", rust_item_identity_qualified_name(item: item))) ) ) ) @@ -95,6 +115,6 @@ fn rust_item_declaration_ref_key(ref: RustItemDeclarationRef) -> String { } } -data rust_item_host_observation_scaffold_note: String = "MODELED G0 PRODUCER (not yet implemented). Replaces scripts/rust_item_census.py: merge-base and HEAD populations of RustItemIdentity rows parsed through dag/extdeps/languages/rust grammar authority (or an enrolled witness realization), not regex beside the grammar. Dissolve-on: producer lands and enrolls; Python script deleted; gunbc.seed_growth_admission seed_growth_g0_census_host_scaffold retires." +data rust_item_host_observation_producer_note: String = "THE PRODUCER LANDED, AND THIS ROW REPLACES THE SCAFFOLD NOTE THAT STOOD HERE. What was here declared a MODELED G0 PRODUCER that did not exist, with a dissolution reading \"producer lands and enrolls; Python script deleted\". That is a CONJUNCTION, and its second half fired on 2026-08-24 for an unrelated reason -- gunbc#9132 bankrupted the measurement corpus and took scripts/rust_item_census.py with it -- while the producer never landed. So the trigger was half-satisfied by a cut that restored nothing, which is exactly the failure DESIGN section 4b(3) names when it requires a restoration trigger to state the CAPABILITY rather than an artifact that would contribute to one. The capability itself went to zero and nobody declared the drop, because the census's consumers were lanes rather than modules and no dependent could refuse. THE PRODUCER IS NOW gunbc.rust_item_scan scan_rust_source, reading item identities from rustfmt-normalized Rust source with its item-keyword population taken from extdeps.languages.rust.syntax rust_item_forms rather than from a second roster, and gunbc.rust_item_host_observation, which supplies the two populations the admission join consumes -- the merge-base-to-HEAD change and the live hand-Rust surface. WHAT IS STILL OPEN: no modeled Rust INGEST grammar exists, so the reader recognizes item headers and extents rather than parsing, and refuses on any structural line it cannot classify. That refusal is the wall, not a gap; the next rung is a Rust ingest grammar publishing real item identities, at which point gunbc.rust_item_scan deletes in favour of it." data rust_item_identity_extension_proposal_note: String = "PROPOSED std.decl_ref EXTENSION (report before G1 lands). Add ImplMethod { impl_subject: NonEmptyStr, method_name: NonEmptyStr } to DeclField. Rationale: the seventh Cargo-manifest item is impl std::fmt::Display for Stage0CargoBinManifestParseRefusal — an impl-block method, not a field of the enum. MIGRATION: existing DeclarationRef rows remain valid (WholeDeclaration / NamedField unchanged). New seed-growth justifications cite impl methods via ImplMethod. Dissolve-on: when std.decl_ref adopts ImplMethod, rust_item_identity.RustItemDeclField deletes in favor of the std type." diff --git a/dag/gunbc/rust_item_scan.dag b/dag/gunbc/rust_item_scan.dag new file mode 100644 index 00000000000..d3846e208eb --- /dev/null +++ b/dag/gunbc/rust_item_scan.dag @@ -0,0 +1,1128 @@ +module gunbc.rust_item_scan + +import extdeps.languages.rust.syntax { rust_item_forms } +import gunbc.rust_item_identity { + RustConst, + RustEnum, + RustFunction, + RustImplBlock, + RustImplMethod, + RustItemIdentity, + RustItemKind, + RustModule, + RustStatic, + RustStruct, + RustTrait, + RustTypeAlias, + rust_item_identity_key +} +import std.algebra { trim } +import std.types { Bool, Int, List, NonEmptyStr, String } +import v2.std.algebra { any, filter, fold_list, length, list_map, list_snoc_item } + +// WHY THIS READER EXISTS, AND WHAT CAPABILITY IT RESTORES. +// +// The item-grain census that gunbc.seed_growth_admission's completeness join needs had exactly one +// realization -- scripts/rust_item_census.py -- and it was deleted by gunbc#9132 with the rest of +// the measurement corpus. Nothing replaced it, so from 2026-08-24 there was NO ROUTE in this +// repository to enumerate Rust items at identity grain. THAT DROP WAS NEVER DECLARED, and it could +// not have been caught by the delete-first census DESIGN section 3 relies on: the census's +// consumers were lanes rather than modules, so no dependent could refuse when it went. It is also +// a live instance of DESIGN section 4b(3)'s own rule that a restoration trigger must name the +// CAPABILITY -- rust_item_host_observation_scaffold_note's dissolution read "producer lands and +// enrolls; Python script deleted", a conjunction whose second half an unrelated cut satisfied while +// the producer never landed. +// +// THE DELETED SCRIPT'S DEFECT IS NOT REPRODUCED HERE, and that is the reason this is authored in +// .dag rather than transliterated. Its ITEM_PATTERNS were a SECOND roster of Rust item keywords, +// authored beside -- and independent of -- the grammar authority that already owns them. That is +// the DESIGN section 3 nicknaming violation, and the census predicted its own failure from it: +// gunbc.bare_reference_scanner_admission records the script over-reporting one diff by 100 percent, +// counting nine fixture declarations authored inside Rust raw string literals as Rust items. Here +// the item-keyword population is READ FROM extdeps.languages.rust.syntax rust_item_forms. There is +// no second roster; a keyword the grammar authority does not carry is not an item here, by +// construction rather than by diligence. +// +// WHAT THIS IS NOT: a Rust parser. No ingest grammar for Rust exists in this tree -- the rust +// authority carries item FORMS and lexical spellings for emission, not a parse table -- so this +// reader recognizes item HEADERS and their extents by scope depth over rustfmt-normalized source, +// and REFUSES on any structural line it does not recognize. That refusal is what makes an +// incomplete recognizer safe: incompleteness surfaces as a typed, located line-stop rather than as +// a smaller population, which is the empty-observation narrow DESIGN section 5 forbids. It is also +// what keeps the raw-string defect above from recurring in a new spelling: source inside a raw +// string literal that reaches a scope boundary is not a recognized structural line, so it refuses +// instead of minting a phantom item. +data rust_item_scan_note: String = "Item-grain reader over rustfmt-normalized Rust source. Item keywords are read from extdeps.languages.rust.syntax rust_item_forms and are not re-coined; every structural line the recognizer cannot classify is a typed, located refusal rather than a dropped line. Replaces the deleted scripts/rust_item_census.py, whose regex ITEM_PATTERNS were the second roster this reader refuses to author. THREE CONSTRUCTS ARE RECOGNIZED AS EXTENTS THAT DECLARE NOTHING -- a wrapped use list, a wrapped attribute, and a macro invocation block -- because rustfmt continues each past its first line and a reader that met the continuation at a scope boundary would refuse most of the population it exists to census. A macro block additionally records its REGION: the grammar authority carries no macro item form, so a declaration made by a macro cannot be named here, and a changed line inside one is reported UNATTRIBUTED rather than file-level, because a static added inside thread_local! is hand-Rust growth and file-level is the answer that would admit it silently." + +data rust_item_scan_formatting_precondition_note: String = "THE RECOGNIZER'S PRECONDITION IS A REPOSITORY FACT, NOT AN ASSUMPTION. Extents are decided by scope depth: a scope opened at indent 4*d contains structural lines at indent 4*(d+1), and closes at a line that is exactly a closing brace at its own indent. That is rustfmt's normal form, and `cargo fmt --all --check` is a documented check over this tree, so the precondition is one the repository already holds rather than one this reader hopes for. WHERE IT DOES NOT HOLD THE READER REFUSES: a file whose bodies are not so normalized reaches a line it cannot classify and returns RustFileScanRefused naming the line and its text. It never guesses an extent, because a guessed extent silently mis-attributes a changed line to the wrong item -- the one failure mode that would let the admission join report green over an unenumerated addition." + +data rust_item_scan_module_path_note: String = "INLINE MODULES ARE SCOPES, NOT LEAVES, because the citation vocabulary already treats them that way. gunbc.declaration_index_seed_growth cites seven declarations under v1_compiler.cli_run.bare_reference_scanner_tests -- an inline `mod` inside cli_run.rs whose members are cited individually. A reader that recorded the mod as one opaque item would make those seven citations unjoinable, so the scan descends and each item carries the dotted path of its enclosing inline modules. An impl block inside a module nests the same way; a method's impl_subject comes from the nearest enclosing impl, never from a module." + +// FOUR SCOPES THAT RECORD NOTHING SIT BESIDE THE THREE THAT DO, and each exists because rustfmt +// wraps a construct across lines that this reader would otherwise meet at a scope boundary and +// refuse. A `use` list, a derive attribute and a macro invocation all continue past their first +// line; none of them is an item, so none records one. Measured on the seed's hand-maintained Rust +// before they existed, wrapped `use` lists alone refused 58 files -- a recognizer safe enough to be +// useless, which is the failure mode a fail-closed reader has to be watched for. +type RustScanScope + = ModuleScope { module_name: String, start_line: Int } + | UseScope { start_line: Int } + | AttributeScope { start_line: Int } + | MacroScope { macro_name: String, start_line: Int } + | ImplScope { impl_subject: String, start_line: Int } + | ItemScope { + kind: RustItemKind, + item_name: String, + impl_subject: String, + enclosing_module: String, + start_line: Int, + raw_terminator: String + } + +// A MACRO INVOCATION'S BLOCK IS AN EXTENT WITH NO ITEM, AND A CHANGE INSIDE IT IS UNATTRIBUTED. +// +// `thread_local! { static … }` and `macro_rules! name { … }` declare real things, and this reader +// cannot name them: the grammar authority carries no macro item form, and inventing one here would +// be the second roster this module exists not to author. Recognizing the EXTENT keeps the file +// scannable -- cli_run.rs itself carries a thread_local! block, and refusing the whole file for it +// would put the most important subject of the census permanently out of reach. Reporting a change +// inside it as file-level would be the fail-open: a static added inside thread_local! is hand-Rust +// growth. So the region is recorded, a changed line inside it is UNATTRIBUTED, and the admission +// join stops the line. Named ceiling, not silence. +type RustMacroRegion { + macro_name: String + start_line: Int + loc: Int +} + +type RustLineAttribution + = AttributedToItem { item_key: String } + | AttributedToFileLevel + | AttributedToUnnamedMacroBlock { macro_name: String, line: Int } + | AttributionOutOfRange { line: Int, line_count: Int } + +type RustFileScan + = RustFileScanned { + repo_path: String, + items: List, + macro_regions: List, + line_count: Int + } + | RustFileScanRefused { + repo_path: String, + line: Int, + line_text: String, + detail: String + } + +type RustScanState { + repo_path: String + line_number: Int + items: List + macro_regions: List + scopes: List + attach_start: Int + refusal_line: Int + refusal_text: String + refusal_detail: String +} + +fn rust_item_keywords() -> List { + list_map(xs: rust_item_forms, f: fn(form) { form.keyword }) +} + +fn rust_keyword_is_item_form(word: String) -> Bool { + any(xs: rust_item_keywords(), predicate: fn(k) { k == word }) +} + +// TOTAL OVER THE AUTHORITY'S KEYWORDS AND OVER NOTHING ELSE. std.syntax ItemFormKind collapses +// trait, impl, const and static into OtherForm, so the kind cannot be read off the form's kind +// field alone; the keyword decides. Membership still comes from the authority -- every caller has +// already asked rust_keyword_is_item_form -- so this never widens the population, it only names +// what the authority admitted. rust_item_kind_keyword_mapping_is_total is the executing evidence +// that the two stay in step: a keyword added to the grammar authority and not named here reds it, +// rather than silently acquiring a plausible kind. +fn rust_item_kind_for_keyword(word: String) -> RustItemKind { + if word == "fn" { RustFunction } + else if word == "struct" { RustStruct } + else if word == "enum" { RustEnum } + else if word == "type" { RustTypeAlias } + else if word == "trait" { RustTrait } + else if word == "impl" { RustImplBlock } + else if word == "mod" { RustModule } + else if word == "static" { RustStatic } + else { RustConst } +} + +fn rust_item_kind_mapping_named_keywords() -> List { + ["fn", "struct", "enum", "type", "trait", "impl", "mod", "static", "const"] +} + +fn rust_item_kind_keyword_mapping_is_total() -> Bool { + fold_list( + xs: rust_item_keywords(), + empty: true, + cons: fn(ok, k) { + ok && any(xs: rust_item_kind_mapping_named_keywords(), predicate: fn(n) { n == k }) + } + ) +} + +fn char_is_ident(c: String) -> Bool { + (c >= "a" && c <= "z") + || (c >= "A" && c <= "Z") + || (c >= "0" && c <= "9") + || c == "_" +} + +fn leading_ident_from(s: String, at: Int, acc: String) -> String { + if at >= string_length(s: s) { + acc + } else { + let c = char_at(s: s, index: at) + if char_is_ident(c: c) { + leading_ident_from(s: s, at: at + 1, acc: concat(acc, c)) + } else { + acc + } + } +} + +fn leading_ident(s: String) -> String { + leading_ident_from(s: s, at: 0, acc: "") +} + +fn drop_prefix_length(s: String, n: Int) -> String { + if n >= string_length(s: s) { "" } else { substring(s: s, start: n, end: string_length(s: s)) } +} + +fn indent_width_from(s: String, at: Int) -> Int { + if at >= string_length(s: s) { + at + } else if char_at(s: s, index: at) == " " { + indent_width_from(s: s, at: at + 1) + } else { + at + } +} + +fn indent_width(s: String) -> Int { + indent_width_from(s: s, at: 0) +} + +fn scan_past_char_from(s: String, at: Int, target: String) -> Int { + if at >= string_length(s: s) { + at + } else if char_at(s: s, index: at) == target { + at + 1 + } else { + scan_past_char_from(s: s, at: at + 1, target: target) + } +} + +data rust_item_modifier_words: List = ["pub", "async", "unsafe", "extern", "default"] + +fn word_is_item_modifier(word: String) -> Bool { + any(xs: rust_item_modifier_words, predicate: fn(m) { m == word }) +} + +// `const` IS THE ONE SPELLING THAT IS BOTH A MODIFIER AND AN ITEM KEYWORD. It opens a constant on +// its own and modifies a function when `fn` follows, so the peel consults the NEXT word rather than +// the current one. Getting this wrong in either direction is a silent mis-classification -- `const +// fn foo` recorded as a constant named `fn`, or `const C` dropped entirely -- so the discriminating +// pair is authored as a witness rather than argued for here. +fn peel_item_modifiers(s: String, fuel: Int) -> String { + if fuel <= 0 { + s + } else { + let t = trim(s: s) + if starts_with(s: t, prefix: "pub(") { + peel_item_modifiers( + s: drop_prefix_length(s: t, n: scan_past_char_from(s: t, at: 0, target: ")")), + fuel: fuel - 1 + ) + } else if starts_with(s: t, prefix: "\"") { + peel_item_modifiers( + s: drop_prefix_length(s: t, n: scan_past_char_from(s: t, at: 1, target: "\"")), + fuel: fuel - 1 + ) + } else { + let w = leading_ident(s: t) + if w == "const" { + let rest = trim(s: drop_prefix_length(s: t, n: string_length(s: w))) + if leading_ident(s: rest) == "fn" { peel_item_modifiers(s: rest, fuel: fuel - 1) } else { t } + } else if word_is_item_modifier(word: w) { + peel_item_modifiers(s: drop_prefix_length(s: t, n: string_length(s: w)), fuel: fuel - 1) + } else { + t + } + } + } +} + +fn peeled_item_head(text: String) -> String { + peel_item_modifiers(s: text, fuel: 8) +} + +// THE IMPL SUBJECT IS THE TEXT THE CITATION VOCABULARY ALREADY USES. The uncitable item this +// repository has carried in prose since the Cargo-manifest bridge is spelled +// "std::fmt::Display for Stage0CargoBinManifestParseRefusal", so the subject recovered here is the +// header text after `impl` and its generics with the body brace and any where-clause removed -- +// exactly that string. Recovering some other normal form would fork one identity into two +// spellings that coincide only until someone edits either. +fn strip_impl_generics(s: String) -> String { + let t = trim(s: s) + if starts_with(s: t, prefix: "<") { + trim(s: drop_prefix_length(s: t, n: scan_past_char_from(s: t, at: 0, target: ">"))) + } else { + t + } +} + +fn trim_trailing_marker(s: String, marker: String) -> String { + let t = trim(s: s) + let n = string_length(s: t) + let m = string_length(s: marker) + if n >= m && substring(s: t, start: n - m, end: n) == marker { + trim(s: substring(s: t, start: 0, end: n - m)) + } else { + t + } +} + +fn impl_subject_from_head(rest: String) -> String { + let without_generics = strip_impl_generics(s: rest) + let without_brace = trim_trailing_marker( + s: trim_trailing_marker(s: without_generics, marker: "{"), + marker: ";" + ) + trim_trailing_marker(s: without_brace, marker: "where") +} + +fn line_is_blank(text: String) -> Bool { + trim(s: text) == "" +} + +fn line_is_attribute_or_comment(text: String) -> Bool { + let t = trim(s: text) + starts_with(s: t, prefix: "#[") + || starts_with(s: t, prefix: "#![") + || starts_with(s: t, prefix: "//") +} + +fn line_is_file_level_declaration(text: String) -> Bool { + let w = leading_ident(s: peeled_item_head(text: text)) + w == "use" || w == "crate" +} + +// A HEADER CLOSES ON ITS OWN LINE when it carries no block that outlives it: a `;`-terminated +// declaration, or a whole item written on one line. Otherwise it opens a scope that closes at a +// brace at its own indent. There is no third case, and a line matching neither at a scope boundary +// is exactly the state this reader refuses on. +fn head_closes_on_its_own_line(text: String) -> Bool { + let t = trim(s: text) + let n = string_length(s: t) + if n == 0 { + true + } else { + let last = substring(s: t, start: n - 1, end: n) + last == ";" || (last == "}" && string_contains(s: t, pattern: "{")) + } +} + +fn ends_with(s: String, suffix: String) -> Bool { + let n = string_length(s: s) + let m = string_length(s: suffix) + n >= m && substring(s: s, start: n - m, end: n) == suffix +} + +fn hash_run_from(s: String, at: Int, acc: Int) -> Int { + if at >= string_length(s: s) { + acc + } else if char_at(s: s, index: at) == "#" { + hash_run_from(s: s, at: at + 1, acc: acc + 1) + } else { + acc + } +} + +fn repeat_hashes(n: Int, acc: String) -> String { + if n <= 0 { acc } else { repeat_hashes(n: n - 1, acc: concat(acc, "#")) } +} + +// A RAW STRING IS THE ONE CONSTRUCT THAT PUTS ARBITRARY TEXT AT A SCOPE BOUNDARY, and it is the +// exact construct the deleted census misread: gunbc.bare_reference_scanner_admission records it +// counting nine .dag declarations authored inside Rust raw string literals as Rust items. The +// terminator is recovered from the opening delimiter's own hash count -- which is what makes the +// literal self-describing and the recovery decidable rather than a guess -- and while it is open +// every line is skipped, so no text inside a literal can be read as structure. +// +// WHAT THIS DOES NOT COVER, named rather than left to be found: a raw string opened INSIDE a body +// rather than on the item's own header line. Its content is body text at a deeper indent and is +// skipped like any body line; it only matters if that content reaches the item's own indent AND +// matches the close test below. The close test is deliberately narrow for that reason, and a +// premature close leaves the remaining literal text at a scope boundary, where it is not a +// recognized structural line and the file REFUSES. So the residual failure mode is a refusal, not +// a silent mis-attribution. The next rung is a modeled Rust lexer, at which point this whole +// recognizer deletes in its favour. +fn raw_string_open_terminator_from(s: String, at: Int) -> String { + if at >= string_length(s: s) { + "" + } else if char_at(s: s, index: at) != "r" { + raw_string_open_terminator_from(s: s, at: at + 1) + } else { + let hashes = hash_run_from(s: s, at: at + 1, acc: 0) + if at + 1 + hashes < string_length(s: s) + && char_at(s: s, index: at + 1 + hashes) == "\"" { + concat("\"", repeat_hashes(n: hashes, acc: "")) + } else { + raw_string_open_terminator_from(s: s, at: at + 1) + } + } +} + +fn raw_string_terminator(text: String) -> String { + let opened = raw_string_open_terminator_from(s: text, at: 0) + if opened == "" { + "" + } else { + let after_open = drop_prefix_length( + s: text, + n: scan_past_char_from(s: text, at: 0, target: "\"") + ) + if string_contains(s: after_open, pattern: opened) { "" } else { opened } + } +} + +fn scan_top_raw_terminator(scopes: List) -> String { + fold_list( + xs: list_last_scope(xs: scopes), + empty: "", + cons: fn(_acc, scope) { + match scope { + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: raw + } => raw + ModuleScope { module_name: _, start_line: _ } => "" + ImplScope { impl_subject: _, start_line: _ } => "" + UseScope { start_line: _ } => "" + AttributeScope { start_line: _ } => "" + MacroScope { macro_name: _, start_line: _ } => "" + } + } + ) +} + +fn scan_state_clear_raw(state: RustScanState) -> RustScanState { + fold_list( + xs: list_last_scope(xs: state.scopes), + empty: state, + cons: fn(_acc, scope) { + match scope { + ItemScope { + kind: kind, + item_name: name, + impl_subject: subject, + enclosing_module: enclosing, + start_line: start, + raw_terminator: _ + } => + scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item( + xs: list_drop_last(xs: state.scopes), + item: ItemScope { + kind: kind, + item_name: name, + impl_subject: subject, + enclosing_module: enclosing, + start_line: start, + raw_terminator: "" + } + ), + attach_start: state.attach_start + ) + ModuleScope { module_name: _, start_line: _ } => state + ImplScope { impl_subject: _, start_line: _ } => state + UseScope { start_line: _ } => state + AttributeScope { start_line: _ } => state + MacroScope { macro_name: _, start_line: _ } => state + } + } + ) +} + +// AN ITEM CLOSES AT ITS OWN INDENT ON A LINE THAT ENDS THE DECLARATION, and the test admits exactly +// two shapes because rustfmt produces exactly two. A block item ends on a line beginning with its +// closing brace -- `}` for a struct or a fn, `});` for a static built from a closure, `];` for a +// slice literal, all of which begin with the brace or bracket that opened them; a declaration +// without a surviving block ends on a line terminated by a semicolon. What the test deliberately +// does NOT admit is any other line at the item's indent, which is why a wrapped signature's `) -> +// T {` is body rather than a close, and why arbitrary text is far more likely to refuse than to +// close an item early. +fn line_closes_item(text: String) -> Bool { + let t = trim(s: text) + starts_with(s: t, prefix: "}") || starts_with(s: t, prefix: "]") || ends_with(s: t, suffix: ";") +} + +fn scope_start_line(scope: RustScanScope) -> Int { + match scope { + ModuleScope { module_name: _, start_line: n } => n + UseScope { start_line: n } => n + AttributeScope { start_line: n } => n + MacroScope { macro_name: _, start_line: n } => n + ImplScope { impl_subject: _, start_line: n } => n + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: n, + raw_terminator: _ + } => n + } +} + +fn list_drop_last(xs: List) -> List { + let n = length(xs: xs) + fold_list( + xs: xs, + empty: [] as List, + cons: fn(acc, x) { if length(xs: acc) + 1 >= n { acc } else { list_snoc_item(xs: acc, item: x) } } + ) +} + +fn list_last_scope(xs: List) -> List { + fold_list(xs: xs, empty: [] as List, cons: fn(_acc, x) { [x] }) +} + +fn scan_scope_depth(state: RustScanState) -> Int { + length(xs: state.scopes) +} + +fn scan_expected_indent(state: RustScanState) -> Int { + 4 * scan_scope_depth(state: state) +} + +fn scan_enclosing_module_path(scopes: List) -> String { + fold_list( + xs: scopes, + empty: "", + cons: fn(acc, scope) { + match scope { + ModuleScope { module_name: name, start_line: _ } => + if acc == "" { name } else { concat(acc, concat(".", name)) } + ImplScope { impl_subject: _, start_line: _ } => acc + UseScope { start_line: _ } => acc + AttributeScope { start_line: _ } => acc + MacroScope { macro_name: _, start_line: _ } => acc + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: _ + } => acc + } + } + ) +} + +fn scan_enclosing_impl_subject(scopes: List) -> String { + fold_list( + xs: scopes, + empty: "", + cons: fn(acc, scope) { + match scope { + ImplScope { impl_subject: subject, start_line: _ } => subject + ModuleScope { module_name: _, start_line: _ } => acc + UseScope { start_line: _ } => acc + AttributeScope { start_line: _ } => acc + MacroScope { macro_name: _, start_line: _ } => acc + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: _ + } => acc + } + } + ) +} + +fn scan_state_refused(state: RustScanState) -> Bool { + state.refusal_line > 0 +} + +fn scan_state_of( + state: RustScanState, + items: List, + scopes: List, + attach_start: Int +) -> RustScanState { + RustScanState { + repo_path: state.repo_path, + line_number: state.line_number, + items: items, + macro_regions: state.macro_regions, + scopes: scopes, + attach_start: attach_start, + refusal_line: 0, + refusal_text: "", + refusal_detail: "" + } +} + +fn scan_state_refuse(state: RustScanState, text: String, detail: String) -> RustScanState { + RustScanState { + repo_path: state.repo_path, + line_number: state.line_number, + items: state.items, + macro_regions: state.macro_regions, + scopes: state.scopes, + attach_start: state.attach_start, + refusal_line: state.line_number, + refusal_text: text, + refusal_detail: detail + } +} + +fn scan_state_advance(state: RustScanState) -> RustScanState { + RustScanState { + repo_path: state.repo_path, + line_number: state.line_number + 1, + items: state.items, + macro_regions: state.macro_regions, + scopes: state.scopes, + attach_start: state.attach_start, + refusal_line: state.refusal_line, + refusal_text: state.refusal_text, + refusal_detail: state.refusal_detail + } +} + +fn scan_item_start_line(state: RustScanState) -> Int { + if state.attach_start > 0 { state.attach_start } else { state.line_number } +} + +fn scan_item_of_scope( + state: RustScanState, + scope: RustScanScope, + end_line: Int +) -> List { + match scope { + UseScope { start_line: _ } => state.items + AttributeScope { start_line: _ } => state.items + MacroScope { macro_name: _, start_line: _ } => state.items + ModuleScope { module_name: name, start_line: start } => + list_snoc_item( + xs: state.items, + item: RustItemIdentity { + repo_path: state.repo_path, + kind: RustModule, + name: name as NonEmptyStr, + impl_subject: "", + enclosing_module: scan_enclosing_module_path(scopes: list_drop_last(xs: state.scopes)), + start_line: start, + loc: end_line - start + 1 + } + ) + ImplScope { impl_subject: subject, start_line: start } => + list_snoc_item( + xs: state.items, + item: RustItemIdentity { + repo_path: state.repo_path, + kind: RustImplBlock, + name: subject as NonEmptyStr, + impl_subject: "", + enclosing_module: scan_enclosing_module_path(scopes: list_drop_last(xs: state.scopes)), + start_line: start, + loc: end_line - start + 1 + } + ) + ItemScope { + kind: kind, + item_name: name, + impl_subject: subject, + enclosing_module: enclosing, + start_line: start, + raw_terminator: _ + } => + list_snoc_item( + xs: state.items, + item: RustItemIdentity { + repo_path: state.repo_path, + kind: kind, + name: name as NonEmptyStr, + impl_subject: subject, + enclosing_module: enclosing, + start_line: start, + loc: end_line - start + 1 + } + ) + } +} + +fn scan_state_with_macro_region(state: RustScanState, scope: RustScanScope) -> RustScanState { + match scope { + MacroScope { macro_name: name, start_line: start } => + RustScanState { + repo_path: state.repo_path, + line_number: state.line_number, + items: state.items, + macro_regions: list_snoc_item( + xs: state.macro_regions, + item: RustMacroRegion { + macro_name: name, + start_line: start, + loc: state.line_number - start + 1 + } + ), + scopes: state.scopes, + attach_start: state.attach_start, + refusal_line: state.refusal_line, + refusal_text: state.refusal_text, + refusal_detail: state.refusal_detail + } + UseScope { start_line: _ } => state + AttributeScope { start_line: _ } => state + ModuleScope { module_name: _, start_line: _ } => state + ImplScope { impl_subject: _, start_line: _ } => state + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: _ + } => state + } +} + +fn scan_close_scope(state: RustScanState) -> RustScanState { + fold_list( + xs: list_last_scope(xs: state.scopes), + empty: scan_state_refuse( + state: state, + text: "}", + detail: "closing brace with no open scope" + ), + cons: fn(_acc, scope) { + scan_state_of( + state: scan_state_with_macro_region(state: state, scope: scope), + items: scan_item_of_scope(state: state, scope: scope, end_line: state.line_number), + scopes: list_drop_last(xs: state.scopes), + attach_start: 0 + ) + } + ) +} + +fn scan_open_header(state: RustScanState, text: String) -> RustScanState { + let head = peeled_item_head(text: text) + let keyword = leading_ident(s: head) + let rest = trim(s: drop_prefix_length(s: head, n: string_length(s: keyword))) + let start = scan_item_start_line(state: state) + let enclosing = scan_enclosing_module_path(scopes: state.scopes) + let self_closing = head_closes_on_its_own_line(text: text) + if keyword == "impl" { + let subject = impl_subject_from_head(rest: rest) + if subject == "" { + scan_state_refuse(state: state, text: text, detail: "impl header carries no subject") + } else { + let scope = ImplScope { impl_subject: subject, start_line: start } + if self_closing { + scan_state_of( + state: state, + items: scan_item_of_scope(state: state, scope: scope, end_line: state.line_number), + scopes: state.scopes, + attach_start: 0 + ) + } else { + scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item(xs: state.scopes, item: scope), + attach_start: 0 + ) + } + } + } else { + let name = leading_ident(s: rest) + if name == "" { + scan_state_refuse( + state: state, + text: text, + detail: concat(keyword, " item header carries no name") + ) + } else if keyword == "mod" && !self_closing { + scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item( + xs: state.scopes, + item: ModuleScope { module_name: name, start_line: start } + ), + attach_start: 0 + ) + } else { + let impl_subject = scan_enclosing_impl_subject(scopes: state.scopes) + let kind = + if keyword == "fn" && impl_subject != "" { RustImplMethod } + else { rust_item_kind_for_keyword(word: keyword) } + let scope = ItemScope { + kind: kind, + item_name: name, + impl_subject: if kind == RustImplMethod { impl_subject } else { "" }, + enclosing_module: enclosing, + start_line: start, + raw_terminator: if self_closing { "" } else { raw_string_terminator(text: text) } + } + if self_closing { + scan_state_of( + state: state, + items: scan_item_of_scope(state: state, scope: scope, end_line: state.line_number), + scopes: state.scopes, + attach_start: 0 + ) + } else { + scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item(xs: state.scopes, item: scope), + attach_start: 0 + ) + } + } + } +} + +fn scan_scope_is_item(scopes: List) -> Bool { + fold_list( + xs: list_last_scope(xs: scopes), + empty: false, + cons: fn(_acc, scope) { + match scope { + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: _ + } => true + ModuleScope { module_name: _, start_line: _ } => false + ImplScope { impl_subject: _, start_line: _ } => false + UseScope { start_line: _ } => false + AttributeScope { start_line: _ } => false + MacroScope { macro_name: _, start_line: _ } => false + } + } + ) +} + +// THE STEP ASKS ONE QUESTION AND REFUSES WHEN IT CANNOT ANSWER IT: is this line at the current +// scope boundary. Deeper lines are body and are never inspected -- which is why this reader needs +// neither brace matching nor string-literal tracking, since it never asks a question whose answer +// depends on them. A line SHALLOWER than the boundary, or one at the boundary that is not a +// recognized structural form, is the state that refuses; that is the arm the deleted script did not +// have, and its absence is why the script silently minted phantom items from raw-string fixtures. +// CLOSING A WRAPPED ATTRIBUTE KEEPS ITS ATTACHMENT, which the ordinary scope close would drop. An +// attribute belongs to the declaration BELOW it, so the item's extent must begin at the attribute +// rather than at its own header line; the general close resets that attachment because every other +// scope genuinely ends a region. Losing it here would not change any item's identity, but it would +// leave the attribute's own lines outside every item, so an edit to a derive would be reported as +// file-level -- an under-attribution, which is the quiet direction. +fn scan_close_attribute_scope(state: RustScanState) -> RustScanState { + fold_list( + xs: list_last_scope(xs: state.scopes), + empty: state, + cons: fn(_acc, scope) { + scan_state_of( + state: state, + items: state.items, + scopes: list_drop_last(xs: state.scopes), + attach_start: scope_start_line(scope: scope) + ) + } + ) +} + +fn scan_top_scope_kind(scopes: List) -> String { + fold_list( + xs: list_last_scope(xs: scopes), + empty: "", + cons: fn(_acc, scope) { + match scope { + UseScope { start_line: _ } => "use" + AttributeScope { start_line: _ } => "attribute" + MacroScope { macro_name: _, start_line: _ } => "macro" + ModuleScope { module_name: _, start_line: _ } => "module" + ImplScope { impl_subject: _, start_line: _ } => "impl" + ItemScope { + kind: _, + item_name: _, + impl_subject: _, + enclosing_module: _, + start_line: _, + raw_terminator: _ + } => "item" + } + } + ) +} + +fn line_opens_macro_invocation(text: String) -> Bool { + let t = trim(s: text) + let name = leading_ident(s: t) + name != "" + && starts_with(s: trim(s: drop_prefix_length(s: t, n: string_length(s: name))), prefix: "!") +} + +fn scan_step(state: RustScanState, text: String) -> RustScanState { + if scan_state_refused(state: state) { + scan_state_advance(state: state) + } else if line_is_blank(text: text) { + scan_state_advance(state: scan_state_of(state: state, items: state.items, scopes: state.scopes, attach_start: 0)) + } else if scan_top_scope_kind(scopes: state.scopes) == "use" { + if ends_with(s: trim(s: text), suffix: ";") { + scan_state_advance(state: scan_close_scope(state: state)) + } else { + scan_state_advance(state: state) + } + } else if scan_top_scope_kind(scopes: state.scopes) == "attribute" { + if ends_with(s: trim(s: text), suffix: "]") { + scan_state_advance(state: scan_close_attribute_scope(state: state)) + } else { + scan_state_advance(state: state) + } + } else if scan_top_scope_kind(scopes: state.scopes) == "macro" { + if indent_width(s: text) == scan_expected_indent(state: state) - 4 + && line_closes_item(text: text) { + scan_state_advance(state: scan_close_scope(state: state)) + } else { + scan_state_advance(state: state) + } + } else { + let indent = indent_width(s: text) + let expected = scan_expected_indent(state: state) + let inside_item = scan_scope_is_item(scopes: state.scopes) + if inside_item { + let raw = scan_top_raw_terminator(scopes: state.scopes) + if raw != "" { + if !string_contains(s: text, pattern: raw) { + scan_state_advance(state: state) + } else if indent == expected - 4 && line_closes_item(text: text) { + scan_state_advance(state: scan_close_scope(state: state)) + } else { + scan_state_advance(state: scan_state_clear_raw(state: state)) + } + } else if indent == expected - 4 && line_closes_item(text: text) { + scan_state_advance(state: scan_close_scope(state: state)) + } else { + scan_state_advance(state: state) + } + } else if indent > expected { + scan_state_advance( + state: scan_state_refuse( + state: state, + text: text, + detail: "line is indented past the current scope boundary but no item is open at it" + ) + ) + } else if indent < expected { + if trim(s: text) == "}" && indent == expected - 4 { + scan_state_advance(state: scan_close_scope(state: state)) + } else { + scan_state_advance( + state: scan_state_refuse( + state: state, + text: text, + detail: "line is shallower than the current scope boundary and does not close it" + ) + ) + } + } else if line_is_attribute_or_comment(text: text) { + let attaching = scan_state_of( + state: state, + items: state.items, + scopes: state.scopes, + attach_start: if state.attach_start > 0 { state.attach_start } else { state.line_number } + ) + if starts_with(s: trim(s: text), prefix: "//") || ends_with(s: trim(s: text), suffix: "]") { + scan_state_advance(state: attaching) + } else { + scan_state_advance( + state: scan_state_of( + state: attaching, + items: attaching.items, + scopes: list_snoc_item( + xs: attaching.scopes, + item: AttributeScope { start_line: state.line_number } + ), + attach_start: attaching.attach_start + ) + ) + } + } else if line_is_file_level_declaration(text: text) { + if ends_with(s: trim(s: text), suffix: ";") { + scan_state_advance(state: scan_state_of(state: state, items: state.items, scopes: state.scopes, attach_start: 0)) + } else { + scan_state_advance( + state: scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item(xs: state.scopes, item: UseScope { start_line: state.line_number }), + attach_start: 0 + ) + ) + } + } else if line_opens_macro_invocation(text: text) { + if head_closes_on_its_own_line(text: text) { + scan_state_advance(state: scan_state_of(state: state, items: state.items, scopes: state.scopes, attach_start: 0)) + } else { + scan_state_advance( + state: scan_state_of( + state: state, + items: state.items, + scopes: list_snoc_item( + xs: state.scopes, + item: MacroScope { + macro_name: leading_ident(s: trim(s: text)), + start_line: if state.attach_start > 0 { state.attach_start } else { state.line_number } + } + ), + attach_start: 0 + ) + ) + } + } else if rust_keyword_is_item_form(word: leading_ident(s: peeled_item_head(text: text))) { + scan_state_advance(state: scan_open_header(state: state, text: text)) + } else { + scan_state_advance( + state: scan_state_refuse( + state: state, + text: text, + detail: "structural line is neither an item header the rust grammar authority names, nor a use declaration, attribute or comment" + ) + ) + } + } +} + +fn scan_rust_source(repo_path: String, content: String) -> RustFileScan { + let lines = split(s: content, delimiter: "\n") + let final = fold_list( + xs: lines, + empty: RustScanState { + repo_path: repo_path, + line_number: 1, + items: [], + macro_regions: [], + scopes: [], + attach_start: 0, + refusal_line: 0, + refusal_text: "", + refusal_detail: "" + }, + cons: fn(acc, line) { scan_step(state: acc, text: line) } + ) + if scan_state_refused(state: final) { + RustFileScanRefused { + repo_path: repo_path, + line: final.refusal_line, + line_text: final.refusal_text, + detail: final.refusal_detail + } + } else if length(xs: final.scopes) > 0 { + fold_list( + xs: list_last_scope(xs: final.scopes), + empty: RustFileScanRefused { + repo_path: repo_path, + line: 1, + line_text: "", + detail: "scope stack is non-empty but carries no scope" + }, + cons: fn(_acc, scope) { + RustFileScanRefused { + repo_path: repo_path, + line: scope_start_line(scope: scope), + line_text: "", + detail: "scope is still open at end of file -- its closing brace was never reached at its own indent" + } + } + ) + } else { + RustFileScanned { + repo_path: repo_path, + items: final.items, + macro_regions: final.macro_regions, + line_count: length(xs: lines) + } + } +} + +fn scan_item_keys(items: List) -> List { + list_map(xs: items, f: fn(item) { rust_item_identity_key(item: item) }) +} + +fn item_contains_line(item: RustItemIdentity, line: Int) -> Bool { + line >= item.start_line && line <= item.start_line + item.loc - 1 +} + +// ATTRIBUTION HAS NO UNKNOWN ARM, AND THAT IS A PROPERTY OF THE SCAN RATHER THAN OPTIMISM HERE. +// scan_rust_source refuses on every structural line it cannot classify, so on a RustFileScanned +// every line is either inside an item or is a recognized file-level line. The only residual +// question is whether the line exists at all, which is why the third arm is a range fact and not a +// "could not tell". A caller holding a RustFileScanRefused already has its refusal and never +// reaches this function. +// +// THE INNERMOST CONTAINING ITEM WINS. A method's lines are inside both the method and its impl +// block, and a module's members are inside the module; the containing item with the greatest start +// line is the one the change is about. Reporting the outer one would attribute a method edit to the +// impl block, which is precisely the mis-attribution that would let a new capability land inside an +// enclosing scope with no addition to report. +fn macro_region_contains_line(region: RustMacroRegion, line: Int) -> Bool { + line >= region.start_line && line <= region.start_line + region.loc - 1 +} + +fn attribute_scanned_line( + items: List, + macro_regions: List, + line_count: Int, + line: Int +) -> RustLineAttribution { + if line < 1 || line > line_count { + AttributionOutOfRange { line: line, line_count: line_count } + } else { + let hits = filter(xs: items, predicate: fn(item) { item_contains_line(item: item, line: line) }) + let innermost = fold_list( + xs: hits, + empty: 0, + cons: fn(best, item) { if item.start_line > best { item.start_line } else { best } } + ) + fold_list( + xs: filter(xs: hits, predicate: fn(item) { item.start_line == innermost }), + empty: fold_list( + xs: filter( + xs: macro_regions, + predicate: fn(region) { macro_region_contains_line(region: region, line: line) } + ), + empty: AttributedToFileLevel, + cons: fn(_acc, region) { + AttributedToUnnamedMacroBlock { macro_name: region.macro_name, line: line } + } + ), + cons: fn(_acc, item) { AttributedToItem { item_key: rust_item_identity_key(item: item) } } + ) + } +} diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index 0b78cdd0839..b711c1c5157 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -11,12 +11,28 @@ import gunbc.qualified_pipe_callee_seed_growth { qualified_pipe_callee_seed_grow import gunbc.roadmap_authority { observation_scoped_run_seed_growth_justification } import gunbc.target_invocation_seed_growth { target_invocation_seed_growth_justification } import gunbc.witness_runtime_cause_seed_growth { witness_runtime_cause_seed_growth_justification } +import gunbc.rust_item_host_observation { + ItemDeclarationRefProjection, + LiveHandRustObservation, + LiveHandRustObserved, + LiveHandRustHostRefused, + RustItemChangeObservation, + RustItemChangeObserved, + RustItemChangeUnattributed, + RustItemChangeScanRefused, + RustItemChangeHostRefused, + item_declaration_ref, + projected_declaration_refs, + projected_refused_keys, + projected_uncitable_keys +} +import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } import gunbc.seed_growth { SeedGrowthJustification } import gunbc.stage0_rust_host_observation { stage0_rust_observation_seed_growth_justification } import gunbc.floor_non_verdict_enrollment { floor_non_verdict_seed_growth_justification } import gunbc.whole_corpus_compile_admission { whole_corpus_compile_seed_growth_justification } import std.decl_ref { DeclarationRef, WholeDeclaration } -import std.disposition { Disposition, RealizationDispatch, Scaffold, SingleAuthority } +import std.disposition { Disposition } import std.keyed_roster { path_occurrence_count } import std.roster_frontier { declaration_ref_display_key, @@ -25,11 +41,26 @@ import std.roster_frontier { import std.types { Bool, Int, List, String } import v2.std.algebra { any, filter, fold_list, length, list_append } -data seed_growth_admission_note: String = "ITEM-GRAIN admission authority for hand-authored Rust growth (Lane swift-lynx-164). TARGET: compare every newly authored or modified hand Rust item in a change against SeedGrowthJustification.hand_authored_declarations and refuse on the difference (gunbc.seed_growth.seed_growth_note completeness join). PATH-GRAIN census (sibling proud-owl-773 PR #7894) answers which files lack disposition; THIS MODULE answers which ITEMS within files lack justification. NOT IN SCOPE: deleting existing Rust. NOT IN SCOPE: netting deletions against additions. G0 HOST: GONE. scripts/rust_item_census.py was SCAFFOLD-MARKED (seed_growth_g0_census_host_scaffold) and was DELETED from the tree by gunbc#9132, which bankrupted the measurement corpus and left no .sh or .py anywhere outside .githooks. This row cited it in the present tense until 2026-08-24; a reader planning against that sentence would have looked for a producer that does not exist, which is the positional-citation decay DESIGN 3 names, arriving here as a path rather than a line. TWO FURTHER ROWS IN THIS SAME FILE asserted the script was the current host -- seed_growth_g0_census_host_scaffold_note and seed_growth_admission_g0_population_note -- and they ARE repaired in this change, because a module that answers two ways about one fact is the DESIGN §3 violation this correction exists to remove, and leaving them would have made this file its own counter-example (caught by review 55623; an earlier revision of this sentence listed only the sibling modules and omitted the local pair, which is how the contradiction survived the first pass). THREE SIBLING modules still carry the same dead citation as if it were live (gunbc.bare_reference_scanner_admission, gunbc.rust_item_identity, and dag/test/claim/rust_item_identity_witness_test); they are NOT repaired here, and that is the residue gunbc#9132 declared it was leaving rather than a gap this change opened. IT WAS FIVE UNTIL 2026-08-25, and the two that left are recorded here rather than silently subtracted, because a count that moves without saying why is the transcription this repository keeps paying for: gunbc.cli_run_hand_rust_area_ledger and its witness were repaired together, the ledger replacing the citation with a declared missing-measurement-route row and the witness dropping an assertion that the dead path STILL APPEARED in that prose -- which is why the pair had to move together, the witness having been the thing holding the ledger's dead citation green. Item extraction has no host at all until rust_item_host_observation lands, so the G0 interim path is not merely scaffolded now -- it is absent. ENROLLED TODAY: seed_growth_roster_well_formed_gate_holds — roster well-formedness only (non-empty closed roster, well-formed rows, no duplicate DeclarationRef keys). NOT ENROLLED: seed_growth_join_declarations as admission — diff candidates absent until rust_item_host_observation; seed_growth_admission_join_scaffold marks that boundary." +data seed_growth_admission_note: String = "ITEM-GRAIN admission authority for hand-authored Rust growth (Lane swift-lynx-164). TARGET: compare every newly authored or modified hand Rust item in a change against SeedGrowthJustification.hand_authored_declarations and refuse on the difference (gunbc.seed_growth.seed_growth_note completeness join). PATH-GRAIN census answers which files lack disposition; THIS MODULE answers which ITEMS within files lack justification. NOT IN SCOPE: deleting existing Rust. NOT IN SCOPE: netting deletions against additions -- and that is now EXECUTED rather than merely stated: no arm of seed_growth_admit_change reads deleted_hand_items, and seed_growth_deletions_offset_nothing runs one unjustified addition through two observations whose deleted populations differ and requires one verdict. It is a WITNESSED property and not a structural one -- the observation record still carries the deleted population, so a future arm could read it -- and the honest rung is the witness, not the shape. THE G0 HOST EXISTS AGAIN. scripts/rust_item_census.py was deleted by gunbc#9132 and the capability went to zero with it, undeclared, because the census's consumers were lanes rather than modules and no dependent could refuse. gunbc.rust_item_host_observation replaces it: two populations from two functions, item extraction through gunbc.rust_item_scan whose item-keyword population is READ FROM the extdeps.languages.rust grammar authority rather than re-coined as the deleted script's regex ITEM_PATTERNS were. ENROLLED: seed_growth_roster_well_formed_gate_holds (roster well-formedness) and seed_growth_admit_change (the completeness join against a real changed-item population). WHAT IS NOT CLAIMED: no required CI phase invokes the join yet, so the rung is the ROUTE existing and its wall executing under witnesses -- not a gate armed on every push. Arming it is a claim_executor phase, which is itself hand-Rust growth this very join would have to admit, and it lands separately." +// THE MODIFIED POPULATION IS REPORTED AND NOT YET REFUSED, AND SAYING WHICH IS THE POINT. +// +// A modification is the arm that decides whether this construction does anything: a worker can add +// an entire capability to an existing two-thousand-line function without creating one new item, so +// an admission that only refused ADDITIONS would leave the freeze open through the largest +// functions in the seed. The rule that governs it is that a modification may not ORIGINATE semantic +// capability in Rust, which is what ExistingSeedItemModified's capability_origin states. +// +// What is NOT here is a roster of those dispositions, and its absence is deliberate rather than +// forgotten: the terminal roster closes with the final claim_executor cut, and hand-transcribing it +// today would mean authoring roughly fifteen hundred moving declarations that can omit items while +// staying internally well-formed -- rung inflation wearing a roster. So this field makes the +// population VISIBLE and COUNTABLE, which is what a later roster needs to be derivable at all, and +// the refusal lands with that roster. Reporting it is weaker than refusing it and is labelled so. type SeedGrowthAdmissionVerdict = SeedGrowthAdmissionComplete { justified_items: List, + modified_items_requiring_disposition: List, stale_justifications: List, duplicate_justifications: List, uncitable_items: List @@ -39,42 +70,56 @@ type SeedGrowthAdmissionVerdict detail: String } +// A MODIFICATION CARRIES ITS CAPABILITY ORIGIN, AND THAT FIELD IS THE WHOLE POINT OF THE ARM. +// +// ExistingSeedItemModified without it reads as "the item already existed, therefore anything may be +// added to it" -- and a worker can add an ENTIRE CAPABILITY to an existing two-thousand-line +// function without creating one new Rust item. Under that reading the forward freeze is reopened +// through the largest functions in the seed and the whole construction is decorative, refusing only +// the additions nobody needed to hide. So the arm states where the capability came from: +// NoNewCapability (the edit originates none) or CapabilityAlreadyModeled (the capability exists in +// the substrate and this is its realization). There is deliberately no third value naming a +// capability originated in Rust, because that is the state the freeze forbids; it is unrepresentable +// here rather than checked, which is the construction direction DESIGN section 5 asks for. +type CapabilityOrigin + = NoNewCapability + | CapabilityAlreadyModeled + type SeedGrowthChangeDisposition = SeedBridgeAuthorized - | ExistingSeedItemModified + | ExistingSeedItemModified { + item: DeclarationRef, + dag_authority: DeclarationRef, + capability_origin: CapabilityOrigin, + current_boundary: String + } | GeneratedInstead | HandRustDeleted | RetainedKernel | GrowthRefused -data seed_growth_forward_freeze_policy_note: String = "MANUAL STOP-LINE, NOT MECHANICALLY ENFORCED. Every PR that adds hand-written src/v1 Rust MUST enumerate: exact item identity, .dag authority, why Rust is still needed, owning lane, deletion trigger, current boundary, hand-item delta, and hand-LOC delta. Unenumerated hand growth is a stop-line -- no netting deletions against additions. Generated Rust is exempt only with generation-authority proof (gunbc.stage0_rust_source_lifecycle_scaffold derived_generated_stage0_repo_paths or gunbc.generated_artifact). THIS ROW SAID (enforceable now) UNTIL 2026-08-24 AND THAT WAS RUNG INFLATION, contradicted by seed_growth_admission_note two rows down in this same file: what is ENROLLED is seed_growth_roster_well_formed_gate_holds, which checks the roster against ITSELF -- non-empty, well-formed rows, no duplicate DeclarationRef keys -- and never reads a diff. seed_growth_join_declarations, the join that would refuse an unenumerated addition, is NOT ENROLLED because no changed-item population exists to join against. So no mechanism can currently refuse hand growth, and the word (enforceable) invited exactly the reading DESIGN 4b names as worse than sitting low: a class that is never ranked for climbing because its carrier already claims the rung. RUNG: mitigatable, by review diligence alone. MEASURED, which is why this is a correction and not a scruple: of the four PRs that added hand src/v1 Rust in the 48 hours to 2026-08-24, ZERO were caught by an unassisted reviewer. All four were caught by a follow-up investigation after they had already reached approval. A policy is not worse than no policy when it is labelled as an unenforced specification; it is worse when its presence is read as coverage, and that is what this row was doing. NEXT-RUNG TRIGGER (DESIGN 4b(2)): rust_item_host_observation supplying a changed-item population, at which point seed_growth_join_declarations enrolls and the class reaches mechanically preventable. Two conditions on that enrolment, both learned the hard way and recorded so the next author does not rediscover them. FIRST, the join must be SPLIT: as written seed_growth_join_declarations uses one candidate set both to find unjustified additions and to declare roster rows stale, so a changed-item population makes every justification for an untouched item look stale, while a live population no longer isolates what this PR added -- two populations, two functions. SECOND, an extractor that cannot attribute a changed hand-Rust region must REFUSE rather than report an empty delta: a hand .rs file changed while the delta is empty is the empty-observation narrow DESIGN 5 forbids, and it is the one failure mode that would make the gate read green while admitting exactly what it exists to refuse. ALSO DERIVABLE, NOT AUTHORED: hand-item delta and hand-LOC delta are functions of the diff, so requiring an author to type them creates a second representation of a fact the diff already owns (DESIGN 2). They are still required above because nothing derives them yet, and the cost is already measured -- a seed-growth receipt on gunbc#9095 claimed +195/+65 against a measured +203/-0, +60/-5, +102/-0. When the producer lands, the gate should PRINT these and the author should stop authoring them." +data seed_growth_forward_freeze_policy_note: String = "THE STOP-LINE HAS A MECHANISM AGAIN, AND THIS ROW STATES ITS RUNG RATHER THAN ITS AMBITION. Every PR that adds hand-written src/v1 Rust MUST enumerate: exact item identity, .dag authority, why Rust is still needed, owning lane, deletion trigger, current boundary. Unenumerated hand growth is a stop-line -- no netting deletions against additions, which seed_growth_deletions_offset_nothing now executes rather than merely instructing: no arm of seed_growth_admit_change reads the deleted population, and the witness runs one addition through two observations whose deletions differ and requires one verdict. Generated Rust is exempt only with generation-authority proof (gunbc.stage0_rust_source_lifecycle_scaffold derived_generated_stage0_repo_paths or gunbc.generated_artifact), and that classification is now DERIVED by gunbc.rust_item_host_observation rather than asserted by the author. THIS ROW SAID (enforceable now) UNTIL 2026-08-24 AND THAT WAS RUNG INFLATION; it then said no mechanism could refuse hand growth, which was true until this change. MEASURED, and it is why the mechanism was worth building rather than the policy worth restating: of the four PRs that added hand src/v1 Rust in the 48 hours to 2026-08-24, ZERO were caught by an unassisted reviewer -- all four were caught by a follow-up investigation after they had already reached approval. THE TWO CONDITIONS THIS ROW SET ON ENROLMENT ARE BOTH MET, and they are recorded as met rather than deleted, because a condition that vanishes when it is satisfied leaves the next author unable to tell whether it was answered or forgotten. FIRST, THE JOIN IS SPLIT: seed_growth_unjustified_additions reads the CHANGE population and seed_growth_stale_justifications reads the LIVE population, so a justification for an untouched item is no longer rendered stale by a change population, and a live population no longer hides what this change added. SECOND, THE EXTRACTOR REFUSES RATHER THAN REPORTING AN EMPTY DELTA: every arm of RustItemChangeObservation that could not attribute a changed hand-Rust region is a typed, located refusal, and seed_growth_admit_change maps each of them to SeedGrowthAdmissionRefused rather than to a complete verdict with an empty list. RUNG: mechanically preventable AS TO THE ROUTE, and no higher -- the wall executes under witnesses and is callable, and NO REQUIRED CI PHASE INVOKES IT YET. A mechanism no lane runs guards nothing, and calling this armed would be the inflation DESIGN section 4b(1) names as worse than sitting low. NEXT-RUNG TRIGGER: a required-run phase that computes the change observation and stops the line on SeedGrowthAdmissionRefused. ALSO DERIVABLE, NOT AUTHORED: hand-item delta and hand-LOC delta are functions of the diff, so requiring an author to type them creates a second representation of a fact the diff already owns (DESIGN 2) -- and the cost was measured, a seed-growth receipt on gunbc#9095 claiming +195/+65 against a measured +203/-0, +60/-5, +102/-0. They are dropped from the enumeration above as of this change, because the observation now derives them and an authored copy would be exactly that second representation." data seed_growth_justification_roster_note: String = "CLOSED ROSTER of every SeedGrowthJustification row in the corpus. G1 joins new/modified hand items against this list. Rows are authored beside the obligation they declare. THE LIVE POPULATION IS seed_growth_justification_roster(), AND IT IS THE ONLY AUTHORITY -- this row no longer enumerates it. Each new justification adds one entry to that function AND one data row in its owning module; it adds nothing here. \n\nWHY THE ENUMERATION WAS DELETED (2026-08-25). This sentence used to name every row, which made the prose and the function two WRITABLE AUTHORITIES over one population, and they diverged in both directions. The correctness defect: gunbc#9089 added its import and its function entry without naming its row here, so the function carried four while the prose said three, and nothing could detect it -- a row missing from the prose but present in the function conflicts with nothing, so no merge ever asks anyone to reconcile them. The operational symptom: across six consecutive roster unions on 2026-08-24/25, seed_growth_justification_roster() AUTO-MERGED every single time and this sentence was the ONLY conflict in the file. Six for six. Two representations of one fact, one of which a merge tool can reconcile and one of which it cannot -- so only the unreconcilable one generated work, and it was the one with no authority. DESIGN section 2, measured rather than argued.\n\nWHAT IS NOT REPLACED, because deleting a prose row takes everything in it unless the contents are enumerated first. No renderer is built: nothing consumes a rendered list of row names, and building one to preserve prose that should not exist would re-create the second representation in executable form. No parallel List is added either, for the same reason one function and one sentence failed -- two executable rosters drift exactly as prose and function did. IF a consumer ever needs each row's own declaration identity (the mechanical seed-growth join will, since it needs exact identities for diagnostics), the fix is to pair identity and value in ONE entry at construction -- a SeedGrowthJustificationEntry carrying { row: DeclarationRef, justification: SeedGrowthJustification } -- never a second list beside the first. Until such a consumer exists, deleting the duplication is the whole change.\n\nBOTH SIDES OF A ROSTER MERGE ARE STILL KEPT: this is a closed roster, so taking either side of a conflict whole silently deletes the other branch's authority obligation rather than conflicting. That rule now applies to the function list alone, which is where it was always load-bearing and where git has in practice resolved it correctly every time." // UNCITABLE ITEMS (prose-only until std.decl_ref gains ImplMethod). stage0_rust_observation_seed_growth_justification documents one: impl std::fmt::Display for Stage0CargoBinManifestParseRefusal in v1_compiler.cli_run — seven hand items, six citable DeclarationRef rows. G1 reports uncitable_items separately; they do NOT satisfy the forward-freeze enumeration via DeclarationRef alone. -data seed_growth_admission_join_scaffold: Disposition = Scaffold { - dissolves_to: SingleAuthority, - bind: DeclarationRef { - module_path: "gunbc.seed_growth_admission", - decl_name: "seed_growth_join_declarations", - field: WholeDeclaration - } -} - -data seed_growth_admission_join_scaffold_note: String = "SCAFFOLD — seed_growth_join_declarations is NOT an enrolled admission gate until rust_item_host_observation supplies diff DeclarationRef candidates. Joining the roster against itself is a tautology and must not green as completeness admission (review 49332). Dissolve-on: diff population carrier lands and enrolls join against merge-base candidates." - -data seed_growth_g0_census_host_scaffold: Disposition = Scaffold { - dissolves_to: RealizationDispatch, - bind: DeclarationRef { - module_path: "gunbc.rust_item_identity", - decl_name: "rust_item_host_observation_scaffold_note", - field: WholeDeclaration - } -} - -data seed_growth_g0_census_host_scaffold_note: String = "SCAFFOLD, AND ITS SUBJECT IS ALREADY GONE. scripts/rust_item_census.py WAS the first hand-authored Python realization in scripts/; item extraction used regex (its ITEM_PATTERNS) parallel to but outside the dag/extdeps/languages/ rust grammar authority, while generated-vs-hand classification alone was authority-derived. THE FILE WAS DELETED BY gunbc#9132 (measurement-corpus bankruptcy, no .sh or .py left outside .githooks), so every sentence here is past tense as of 2026-08-24. Dissolve-on, stated as the two clauses it always was: (a) script deleted -- SATISFIED, by #9132 rather than by this lane; (b) gunbc.rust_item_identity rust_item_host_observation_scaffold_note producer enrolls -- OUTSTANDING. The disposition row is RETAINED rather than retired because (b) is unmet and because it has consumers (gunbc.bare_reference_scanner_admission, gunbc.rust_item_identity, seed_growth_admission_witness_test); retiring it on the strength of (a) alone would delete a live obligation because half its trigger fired. Same trigger retires seed_growth_admission_join_scaffold admission wiring." - -data seed_growth_admission_g0_population_note: String = "G0 CENSUS HOST: NONE. scripts/rust_item_census.py (SCAFFOLD, see seed_growth_g0_census_host_scaffold) was DELETED by gunbc#9132, so there is no producer to run and the item-grain population is not measurable today by any route in this repository. WHAT FOLLOWS DESCRIBES THE MECHANISM THAT WOULD MEASURE IT, retained because the laws survive the producer: population was measured at execution from git-tracked .rs files; generated-vs-hand is derived from gunbc.stage0_rust_source_lifecycle_scaffold derived_generated_stage0_repo_paths and gunbc.generated_artifact artifact_path — unclassified stage0/src paths refuse. This note does NOT pin numeric literals (DESIGN §3 stale-citation class) -- and the instruction that used to stand here, \"run the script at HEAD for the live receipt\", named a file that no longer exists; there is no live receipt until the producer lands. Supersedes cli_run_hollowing_plan §0 prose dated 2026-07-23. G1 compares merge-base diff against SeedGrowthJustification rows once rust_item_host_observation supplies DeclarationRef candidates." +// SCAFFOLD ROWS RETIRED, AND THE RETIREMENT IS RECORDED RATHER THAN SILENT. +// +// Two Scaffold dispositions stood here. seed_growth_admission_join_scaffold marked +// seed_growth_join_declarations as not-an-admission-gate "until rust_item_host_observation supplies +// diff DeclarationRef candidates"; that function is gone, replaced by the split pair below, and the +// candidates now exist. seed_growth_g0_census_host_scaffold marked scripts/rust_item_census.py, and +// stated its trigger as the two clauses it always was: (a) the script deleted -- satisfied by +// gunbc#9132 rather than by this lane, and (b) a producer enrolling -- outstanding until now. Both +// clauses are met, so the row retires by its own terms rather than by an author's judgement. +// +// THE HALF-FIRED TRIGGER IS THE LESSON AND IT IS KEPT. Clause (a) was satisfied by an unrelated cut +// that restored nothing, which is precisely the shape DESIGN section 4b(3) forbids when it requires +// a restoration trigger to name the CAPABILITY rather than an artifact that would contribute to +// one: for three days the disposition read half-discharged while the thing it existed to protect +// was entirely absent. gunbc.rust_item_identity rust_item_host_observation_producer_note carries +// that receipt at the site where the trigger was written. +data seed_growth_admission_g0_population_note: String = "G0 CENSUS HOST: gunbc.rust_item_host_observation. Population is measured at execution from revision-addressed .rs blobs; generated-versus-hand is DERIVED from gunbc.stage0_rust_source_lifecycle_scaffold derived_generated_stage0_repo_paths and gunbc.generated_artifact artifact_path, and a path no namespace row claims REFUSES rather than acquiring a plausible module path. This note pins no numeric literals (DESIGN section 3 stale-citation class) and names no transcribed measurement: the instrument is the function, and a figure is read by running it. G1 -- the comparison of the merge-base-to-HEAD change against SeedGrowthJustification rows -- is seed_growth_admit_change below." fn seed_growth_justification_roster() -> List { [ @@ -123,6 +168,11 @@ fn seed_growth_roster_declaration_keys() -> List { |> map(r => declaration_ref_display_key(ref: r)) } +fn seed_growth_roster_duplicate_keys() -> List { + let keys = seed_growth_roster_declaration_keys() + filter(xs: keys, predicate: fn(key) { path_occurrence_count(paths: keys, target: key) > 1 }) +} + fn seed_growth_roster_has_duplicate_keys() -> Bool { let keys = seed_growth_roster_declaration_keys() any( @@ -137,36 +187,111 @@ fn seed_growth_uncitable_item_keys() -> List { ] } -fn seed_growth_find_stale( - roster: List, - candidates: List -) -> List { +// TWO POPULATIONS, TWO FUNCTIONS, AND NEITHER MAY SERVE FOR THE OTHER. +// +// The single-candidate-set form that stood here answered both questions from one list, so a +// changed-item population rendered every justification for an untouched item stale, and a live +// population no longer isolated what a change added. The denominators genuinely differ: staleness +// is a question about the CURRENT surface, unjustified addition is a question about the CHANGE. +// gunbc.rust_item_host_observation produces them separately for exactly this reason. +fn seed_growth_stale_justifications(live_items: List) -> List { filter( - xs: roster, - predicate: fn(r) { !declaration_ref_in_list(target: r, refs: candidates) } + xs: seed_growth_roster_all_declarations(), + predicate: fn(r) { !declaration_ref_in_list(target: r, refs: live_items) } ) |> map(r => declaration_ref_display_key(ref: r)) } -fn seed_growth_join_declarations( - candidates: List -) -> SeedGrowthAdmissionVerdict { +fn seed_growth_unjustified_additions(added: List) -> List { let roster = seed_growth_roster_all_declarations() - let unjustified = filter( - xs: candidates, + filter( + xs: added, predicate: fn(r) { !declaration_ref_in_list(target: r, refs: roster) } ) - if length(xs: unjustified) > 0 { - SeedGrowthAdmissionRefused { - unjustified_additions: unjustified |> map(r => declaration_ref_display_key(ref: r)), - detail: "hand Rust item(s) lack SeedGrowthJustification row" - } - } else { - SeedGrowthAdmissionComplete { - justified_items: candidates |> map(r => declaration_ref_display_key(ref: r)), - stale_justifications: seed_growth_find_stale(roster: roster, candidates: candidates), - duplicate_justifications: [], - uncitable_items: seed_growth_uncitable_item_keys() + |> map(r => declaration_ref_display_key(ref: r)) +} + +// AN ITEM WHOSE MODULE PATH COULD NOT BE DERIVED IS A REFUSAL, NOT AN OMISSION. A hand Rust item +// under a tree rust_source_namespace_rows does not claim has no DeclarationRef, so it can neither be +// found in the roster nor reported as unjustified by key -- and dropping it would let a whole +// subtree of the seed grow invisibly. It refuses, naming the item key and the missing mapping. +fn seed_growth_added_projections(added_items: List) -> List { + added_items |> map(item => item_declaration_ref(item: item)) +} + +// THE DELETED POPULATION IS NOT A PARAMETER OF THIS FUNCTION, AND THAT IS THE WHOLE MECHANISM. +// "Rust deleted elsewhere licenses Rust added here" is the offset this policy rejects, and the +// robust way to reject it is to give the verdict no access to the deletions rather than to remember +// not to consult them. seed_growth_deletions_offset_nothing is the executing evidence: it runs the +// same addition through observations whose deleted populations differ and requires one verdict. +fn seed_growth_admit_change( + change: RustItemChangeObservation, + live: LiveHandRustObservation +) -> SeedGrowthAdmissionVerdict { + match change { + RustItemChangeUnattributed { repo_path: path, line: line, detail: detail } => + SeedGrowthAdmissionRefused { + unjustified_additions: [], + detail: concat( + "changed hand Rust could not be attributed to an item in ", + concat(path, concat(": ", detail)) + ) + } + RustItemChangeScanRefused { repo_path: path, line: _, line_text: _, detail: detail } => + SeedGrowthAdmissionRefused { + unjustified_additions: [], + detail: concat("hand Rust source could not be read at item grain in ", concat(path, concat(": ", detail))) + } + RustItemChangeHostRefused { detail: detail } => + SeedGrowthAdmissionRefused { + unjustified_additions: [], + detail: concat("the change population was not observed: ", detail) + } + RustItemChangeObserved { + added_hand_items: added_items, + modified_hand_items: modified_items, + deleted_hand_items: _, + generated_changes: _ + } => { + let projections = seed_growth_added_projections(added_items: added_items) + let unmappable = projected_refused_keys(projections: projections) + let unjustified = seed_growth_unjustified_additions( + added: projected_declaration_refs(projections: projections) + ) + if length(xs: unmappable) > 0 { + SeedGrowthAdmissionRefused { + unjustified_additions: unmappable, + detail: "added hand Rust item(s) live under a tree rust_source_namespace_rows does not claim, so no DeclarationRef can address them" + } + } else if length(xs: unjustified) > 0 { + SeedGrowthAdmissionRefused { + unjustified_additions: unjustified, + detail: "hand Rust item(s) lack SeedGrowthJustification row" + } + } else { + match live { + LiveHandRustHostRefused { detail: detail } => + SeedGrowthAdmissionRefused { + unjustified_additions: [], + detail: concat("the live hand-Rust population was not observed, so staleness cannot be reported: ", detail) + } + LiveHandRustObserved { items: live_items, unscannable: _ } => + SeedGrowthAdmissionComplete { + justified_items: added_items |> map(item => rust_item_identity_key(item: item)), + modified_items_requiring_disposition: modified_items |> map(item => rust_item_identity_key(item: item)), + stale_justifications: seed_growth_stale_justifications( + live_items: projected_declaration_refs( + projections: live_items |> map(item => item_declaration_ref(item: item)) + ) + ), + duplicate_justifications: seed_growth_roster_duplicate_keys(), + uncitable_items: list_append( + left: seed_growth_uncitable_item_keys(), + right: projected_uncitable_keys(projections: projections) + ) + } + } + } } } } diff --git a/dag/test/claim/rust_item_host_observation_witness_test.dag b/dag/test/claim/rust_item_host_observation_witness_test.dag new file mode 100644 index 00000000000..a5989d85543 --- /dev/null +++ b/dag/test/claim/rust_item_host_observation_witness_test.dag @@ -0,0 +1,356 @@ +module test.claim.rust_item_host_observation_witness_test + +import gunbc.rust_item_host_observation { + HunkRangeParsed, + HunkRangeRefused, + ItemDeclarationRefProjected, + ItemDeclarationRefRefused, + ItemDeclarationRefUncitable, + LineRange, + LiveHandRustObserved, + RustFileChangeInput, + RustItemChangeObserved, + RustItemChangeHostRefused, + RustItemChangeScanRefused, + RustItemChangeUnattributed, + RustModulePathProjected, + RustModulePathRefused, + item_declaration_ref, + observe_live_hand_rust, + observe_rust_item_change, + parse_hunk_header, + rust_module_path_for_repo_path +} +import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } +import gunbc.seed_growth_admission { + SeedGrowthAdmissionComplete, + SeedGrowthAdmissionRefused, + seed_growth_admit_change +} +import std.types { Bool, Int, List, String } +import v2.std.algebra { any, length, list_map } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE FIXTURES ARE SOURCE AND DIFF TEXT, NOT CONSTRUCTED OBSERVATIONS, wherever the property under +// test belongs to the reader rather than to the join. A witness that hands the join a hand-built +// item list proves the join and nothing about whether the items would ever have been found. +data cli_run_path: String = "src/v1/stage0/src/cli_run.rs" + +data base_cli_run: String = "use std::fmt;\n\npub fn existing_helper() -> u32 {\n 1\n}\n" + +data head_with_justified_addition: String = "use std::fmt;\n\npub fn existing_helper() -> u32 {\n 1\n}\n\npub fn parse_stage0_cargo_manifest_bin_paths() -> u32 {\n 2\n}\n" + +data head_with_unjustified_addition: String = "use std::fmt;\n\npub fn existing_helper() -> u32 {\n 1\n}\n\npub fn totally_unjustified_probe() -> u32 {\n 2\n}\n" + +data head_with_modified_existing_item: String = "use std::fmt;\n\npub fn existing_helper() -> u32 {\n 7\n}\n" + +data head_with_macro_block_addition: String = "use std::cell::RefCell;\n\npub fn existing_helper() -> u32 {\n 1\n}\n\nthread_local! {\n static COUNTER: RefCell = RefCell::new(0);\n}\n" + +data head_with_unreadable_region: String = "use std::fmt;\n\npub fn existing_helper() -> u32 {\n 1\n}\n\n{\n 1\n}\n" + +fn added_change(head_content: String, new_ranges: List) -> List { + [ + RustFileChangeInput { + repo_path: cli_run_path, + base_content: base_cli_run, + head_content: head_content, + present_at_base: true, + present_at_head: true, + old_ranges: [], + new_ranges: new_ranges + } + ] +} + +fn live_of(head_content: String) -> LiveHandRustObserved { + observe_live_hand_rust( + sources: added_change(head_content: head_content, new_ranges: []), + generated_repo_paths: [] + ) +} + +// THE HUNK HEADER IS GIT'S FORMAT AND THE ELIDED COUNT MEANS ONE. A reader that defaulted the count +// to zero would report every single-line change as touching nothing, which is the empty-observation +// narrow arriving through arithmetic rather than through a failure arm. +test fn hunk_header_with_elided_count_means_one_line() -> Bool { + match parse_hunk_header(header: "@@ -20,0 +21 @@ fn helper() -> bool { true }") { + HunkRangeParsed { old_range: old_range, new_range: new_range } => + new_range.start == 21 && new_range.count == 1 && old_range.start == 20 && old_range.count == 0 + HunkRangeRefused { header: _, detail: _ } => false + } +} + +test fn an_undecodable_hunk_header_refuses_rather_than_yielding_no_range() -> Bool { + match parse_hunk_header(header: "@@ not a range at all @@") { + HunkRangeRefused { header: _, detail: _ } => true + HunkRangeParsed { old_range: _, new_range: _ } => false + } +} + +// LONGEST PREFIX WINS. src/v1/stage0/src/bin/ is a subtree of src/v1/stage0/src/, so a first-match +// reader gives every binary the library's namespace and the authored row order becomes silently +// load-bearing. +test fn the_bin_subtree_does_not_inherit_the_library_namespace() -> Bool { + match rust_module_path_for_repo_path(repo_path: "src/v1/stage0/src/bin/claim_executor.rs") { + RustModulePathProjected { module_path: path } => path == "v1_compiler.bin.claim_executor" + RustModulePathRefused { repo_path: _, detail: _ } => false + } +} + +test fn a_path_no_namespace_row_claims_refuses_rather_than_acquiring_one() -> Bool { + match rust_module_path_for_repo_path(repo_path: "src/v2/somewhere/else.rs") { + RustModulePathRefused { repo_path: _, detail: _ } => true + RustModulePathProjected { module_path: _ } => false + } +} + +test fn a_nested_source_path_keeps_its_directory_as_a_namespace_segment() -> Bool { + match rust_module_path_for_repo_path(repo_path: "src/v1/stage0/src/cli_run/bare_reference_scanner.rs") { + RustModulePathProjected { module_path: path } => path == "v1_compiler.cli_run.bare_reference_scanner" + RustModulePathRefused { repo_path: _, detail: _ } => false + } +} + +test fn an_impl_method_is_reported_uncitable_rather_than_dropped_or_forced() -> Bool { + match item_declaration_ref( + item: RustItemIdentity { + repo_path: cli_run_path, + kind: RustImplMethod, + name: "fmt", + impl_subject: "std::fmt::Display for Stage0CargoBinManifestParseRefusal", + enclosing_module: "", + start_line: 10, + loc: 3 + } + ) { + ItemDeclarationRefUncitable { item_key: _, detail: _ } => true + ItemDeclarationRefProjected { ref: _ } => false + ItemDeclarationRefRefused { item_key: _, detail: _ } => false + } +} + +fn change_of(head_content: String, new_ranges: List) -> RustItemChangeObserved { + observe_rust_item_change( + generated_repo_paths: [], + inputs: added_change(head_content: head_content, new_ranges: new_ranges) + ) +} + +test fn an_added_item_is_reported_added_and_an_untouched_item_is_not_modified() -> Bool { + match change_of(head_content: head_with_unjustified_addition, new_ranges: [LineRange { start: 6, count: 4 }]) { + RustItemChangeObserved { + added_hand_items: added, + modified_hand_items: modified, + deleted_hand_items: deleted, + generated_changes: generated + } => + length(xs: added) == 1 + && length(xs: modified) == 0 + && length(xs: deleted) == 0 + && length(xs: generated) == 0 + RustItemChangeUnattributed { repo_path: _, line: _, detail: _ } => false + RustItemChangeScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustItemChangeHostRefused { detail: _ } => false + } +} + +// A CHANGE INSIDE AN EXISTING ITEM IS A MODIFICATION AND NOT AN ADDITION, which is the arm that +// decides whether this construction does anything: a worker can add a whole capability inside an +// existing function without creating one item, and a reader that reported nothing there would +// leave the freeze open through the largest functions in the seed. +test fn a_body_edit_inside_an_existing_item_is_reported_modified() -> Bool { + match change_of(head_content: head_with_modified_existing_item, new_ranges: [LineRange { start: 4, count: 1 }]) { + RustItemChangeObserved { + added_hand_items: added, + modified_hand_items: modified, + deleted_hand_items: deleted, + generated_changes: _ + } => + length(xs: added) == 0 + && length(xs: deleted) == 0 + && any( + xs: list_map(xs: modified, f: fn(i) { rust_item_identity_key(item: i) }), + predicate: fn(k) { k == "src/v1/stage0/src/cli_run.rs::fn::existing_helper" } + ) + RustItemChangeUnattributed { repo_path: _, line: _, detail: _ } => false + RustItemChangeScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustItemChangeHostRefused { detail: _ } => false + } +} + +// A CHANGED HAND-RUST FILE THE READER CANNOT PLACE REFUSES. It does not report an empty delta, and +// this is the arm the whole carrier exists for: an unattributed change rendered as "nothing +// changed" is silently uncovered, where a widen would merely be expensive. +test fn a_changed_region_the_reader_cannot_attribute_refuses() -> Bool { + match change_of(head_content: head_with_unreadable_region, new_ranges: [LineRange { start: 7, count: 3 }]) { + RustItemChangeScanRefused { repo_path: path, line: _, line_text: _, detail: _ } => path == cli_run_path + RustItemChangeObserved { + added_hand_items: _, + modified_hand_items: _, + deleted_hand_items: _, + generated_changes: _ + } => false + RustItemChangeUnattributed { repo_path: _, line: _, detail: _ } => false + RustItemChangeHostRefused { detail: _ } => false + } +} + +// A CHANGE INSIDE A MACRO BLOCK REFUSES, AND THIS IS THE ARM MOST LIKELY TO HAVE BEEN A FAIL-OPEN. +// The macro block scans cleanly -- the file is not refused, the following items are found -- so the +// tempting answer for a line inside it is "no item changed", which is exactly the empty-observation +// narrow: a `static` added inside `thread_local!` is hand-Rust growth that no roster row names. The +// reader has no name for it, so the join stops the line rather than reporting nothing. +test fn a_change_inside_a_macro_block_refuses_rather_than_reporting_no_item() -> Bool { + match change_of(head_content: head_with_macro_block_addition, new_ranges: [LineRange { start: 8, count: 1 }]) { + RustItemChangeUnattributed { repo_path: path, line: line, detail: _ } => + path == cli_run_path && line == 8 + RustItemChangeObserved { + added_hand_items: _, + modified_hand_items: _, + deleted_hand_items: _, + generated_changes: _ + } => false + RustItemChangeScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustItemChangeHostRefused { detail: _ } => false + } +} + +test fn a_generated_path_change_is_classified_and_never_scanned_for_items() -> Bool { + match observe_rust_item_change( + generated_repo_paths: [cli_run_path], + inputs: added_change(head_content: head_with_unreadable_region, new_ranges: [LineRange { start: 7, count: 3 }]) + ) { + RustItemChangeObserved { + added_hand_items: added, + modified_hand_items: _, + deleted_hand_items: _, + generated_changes: generated + } => length(xs: added) == 0 && length(xs: generated) == 1 + RustItemChangeUnattributed { repo_path: _, line: _, detail: _ } => false + RustItemChangeScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustItemChangeHostRefused { detail: _ } => false + } +} + +test fn an_unjustified_addition_refuses_and_names_the_item() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_unjustified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_unjustified_addition) + ) { + SeedGrowthAdmissionRefused { unjustified_additions: unjustified, detail: _ } => + any(xs: unjustified, predicate: fn(k) { string_contains(s: k, pattern: "totally_unjustified_probe") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// THE POSITIVE CONTROL. Without it the refusal above is satisfied by a join that refuses +// everything, which is a wall that stops the line for every change and therefore stops nothing. +test fn an_addition_the_roster_already_names_is_admitted() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition) + ) { + SeedGrowthAdmissionComplete { + justified_items: justified, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: duplicates, + uncitable_items: _ + } => length(xs: justified) == 1 && length(xs: duplicates) == 0 + SeedGrowthAdmissionRefused { unjustified_additions: _, detail: _ } => false + } +} + +// DELETIONS OFFSET NOTHING, AND IT IS EXECUTED RATHER THAN ASSERTED. This lane has deleted many +// thousands of lines of Rust and that buys exactly zero admission credit; the robust form of that +// rule is that the verdict has no access to the deleted population at all. The same unjustified +// addition is run against a change that also deletes a hand item, and the verdict must not move. +// A MODIFICATION IS REPORTED, BY NAME, AND IS NOT SILENTLY ABSENT. Nothing refuses it yet -- the +// disposition roster closes with the final claim_executor cut -- so the only thing standing between +// a capability added inside an existing function and total invisibility is that the population is +// carried and named. If this list were empty for a body edit, the modification arm would be +// decorative and the addition-only gate would be the whole mechanism. +test fn a_modified_item_is_reported_by_name_for_disposition() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_modified_existing_item, new_ranges: [LineRange { start: 4, count: 1 }]), + live: live_of(head_content: head_with_modified_existing_item) + ) { + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: modified, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => + any( + xs: modified, + predicate: fn(k) { k == "src/v1/stage0/src/cli_run.rs::fn::existing_helper" } + ) + SeedGrowthAdmissionRefused { unjustified_additions: _, detail: _ } => false + } +} + +test fn seed_growth_deletions_offset_nothing() -> Bool { + let with_deletion = observe_rust_item_change( + generated_repo_paths: [], + inputs: [ + RustFileChangeInput { + repo_path: cli_run_path, + base_content: base_cli_run, + head_content: head_with_unjustified_addition, + present_at_base: true, + present_at_head: true, + old_ranges: [], + new_ranges: [LineRange { start: 6, count: 4 }] + }, + RustFileChangeInput { + repo_path: "src/v1/stage0/src/departing.rs", + base_content: "pub fn departing_helper() -> u32 {\n 1\n}\n", + head_content: "", + present_at_base: true, + present_at_head: false, + old_ranges: [LineRange { start: 1, count: 3 }], + new_ranges: [] + } + ] + ) + match seed_growth_admit_change(change: with_deletion, live: live_of(head_content: head_with_unjustified_addition)) { + SeedGrowthAdmissionRefused { unjustified_additions: unjustified, detail: _ } => + any(xs: unjustified, predicate: fn(k) { string_contains(s: k, pattern: "totally_unjustified_probe") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// A REFUSED OBSERVATION IS A REFUSED ADMISSION. The join may never render an unobservable change as +// a complete verdict with an empty addition list -- that is the exact shape by which a broken +// reader reads as coverage. +test fn an_unobservable_change_cannot_produce_a_complete_verdict() -> Bool { + match seed_growth_admit_change( + change: RustItemChangeHostRefused { detail: "git refused" }, + live: live_of(head_content: base_cli_run) + ) { + SeedGrowthAdmissionRefused { unjustified_additions: _, detail: detail } => + string_contains(s: detail, pattern: "not observed") + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} diff --git a/dag/test/claim/rust_item_identity_witness_test.dag b/dag/test/claim/rust_item_identity_witness_test.dag index 54d759e0ecd..07129ad85cf 100644 --- a/dag/test/claim/rust_item_identity_witness_test.dag +++ b/dag/test/claim/rust_item_identity_witness_test.dag @@ -4,18 +4,17 @@ import gunbc.rust_item_identity { RustFunction, RustImplMethod, RustItemIdentity, - rust_item_host_observation_scaffold_note, + rust_item_host_observation_producer_note, rust_item_identity_key, rust_item_identity_extension_proposal_note, rust_item_identity_note } -import gunbc.seed_growth_admission { - seed_growth_admission_g0_population_note, - seed_growth_admission_note, - seed_growth_justification_roster_note -} +import gunbc.seed_growth_admission { seed_growth_admission_note } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.text { String } +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + test fn rust_item_identity_note_cites_impl_method_gap() -> Bool { string_contains(s: rust_item_identity_note, pattern: "impl-block method") && string_contains(s: rust_item_identity_extension_proposal_note, pattern: "ImplMethod") @@ -27,6 +26,7 @@ test fn rust_item_identity_key_includes_impl_subject() -> Bool { kind: RustImplMethod, name: "fmt", impl_subject: "std::fmt::Display for Stage0CargoBinManifestParseRefusal", + enclosing_module: "", start_line: 100, loc: 5 } @@ -39,26 +39,56 @@ test fn rust_item_identity_key_omits_empty_impl_subject() -> Bool { kind: RustFunction, name: "parse_stage0_cargo_manifest_bin_paths", impl_subject: "", + enclosing_module: "", start_line: 200, loc: 50 } rust_item_identity_key(item: item) == "src/v1/stage0/src/cli_run.rs::fn::parse_stage0_cargo_manifest_bin_paths" } +// TWO SAME-NAMED ITEMS IN TWO INLINE MODULES OF ONE FILE MUST NOT SHARE A KEY. If they did, the +// admission join would find the second one already present in the roster on the strength of the +// first, and an addition would be admitted by a justification written about something else. This is +// the discriminating pair rather than a single positive: the same name in two enclosing modules, +// required to differ. +test fn same_name_in_two_inline_modules_yields_two_keys() -> Bool { + let left = RustItemIdentity { + repo_path: "src/v1/stage0/src/cli_run.rs", + kind: RustFunction, + name: "helper", + impl_subject: "", + enclosing_module: "bare_reference_scanner_tests", + start_line: 10, + loc: 3 + } + let right = RustItemIdentity { + repo_path: "src/v1/stage0/src/cli_run.rs", + kind: RustFunction, + name: "helper", + impl_subject: "", + enclosing_module: "stage0_cargo_manifest_parser_tests", + start_line: 40, + loc: 3 + } + rust_item_identity_key(item: left) != rust_item_identity_key(item: right) + && rust_item_identity_key(item: left) + == "src/v1/stage0/src/cli_run.rs::fn::bare_reference_scanner_tests.helper" +} + test fn seed_growth_admission_note_cites_seed_growth_authority() -> Bool { string_contains(s: seed_growth_admission_note, pattern: "gunbc.seed_growth") && string_contains(s: seed_growth_admission_note, pattern: "completeness join") - && string_contains(s: seed_growth_justification_roster_note, pattern: "CLOSED ROSTER") -} - -test fn g0_population_note_cites_census_host() -> Bool { - string_contains(s: seed_growth_admission_g0_population_note, pattern: "scripts/rust_item_census.py") - && string_contains(s: seed_growth_admission_g0_population_note, pattern: "SCAFFOLD") - && string_contains(s: seed_growth_admission_g0_population_note, pattern: "does NOT pin numeric literals") - && string_contains(s: seed_growth_admission_g0_population_note, pattern: "cli_run_hollowing_plan") } -test fn rust_item_host_observation_scaffold_names_python_dissolution() -> Bool { - string_contains(s: rust_item_host_observation_scaffold_note, pattern: "rust_item_census.py") - && string_contains(s: rust_item_host_observation_scaffold_note, pattern: "extdeps/languages/rust") +// WHAT REPLACED THE TWO PROSE-MATCH ARMS THAT STOOD HERE, AND WHY THEY WERE WORSE THAN EMPTY. Two +// witnesses asserted that prose rows CONTAINED the substring "scripts/rust_item_census.py" -- a path +// gunbc#9132 had already deleted from the tree. Their only failure mode was that someone edited a +// sentence, so they could not tell a repair from a regression, and here they actively defended the +// regression: while they were green the repository's item-grain census capability was at zero and +// the rows saying so were pinned in place. The producer note is now asserted for what it must +// RECORD -- that the trigger it replaced was a conjunction an unrelated cut half-satisfied -- which +// is the fact a later reader needs and the one that would be lost if the row were quietly rewritten. +test fn the_producer_note_records_the_half_fired_trigger() -> Bool { + string_contains(s: rust_item_host_observation_producer_note, pattern: "CONJUNCTION") + && string_contains(s: rust_item_host_observation_producer_note, pattern: "gunbc.rust_item_scan") } diff --git a/dag/test/claim/rust_item_scan_witness_test.dag b/dag/test/claim/rust_item_scan_witness_test.dag new file mode 100644 index 00000000000..58bddd083a6 --- /dev/null +++ b/dag/test/claim/rust_item_scan_witness_test.dag @@ -0,0 +1,259 @@ +module test.claim.rust_item_scan_witness_test + +import gunbc.rust_item_identity { + RustFunction, + RustImplMethod, + RustItemIdentity, + rust_item_identity_key +} +import gunbc.rust_item_scan { + AttributedToFileLevel, + AttributedToItem, + AttributedToUnnamedMacroBlock, + AttributionOutOfRange, + RustFileScanned, + RustFileScanRefused, + attribute_scanned_line, + rust_item_kind_keyword_mapping_is_total, + scan_item_keys, + scan_rust_source +} +import std.types { Bool, Int, List, String } +import v2.std.algebra { any, length } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// EVERY FIXTURE PLANTS WHAT IT ASSERTS, and the reds are authored rather than argued for. The scan +// is a recognizer over source text, so the discriminating inputs are source text: a file whose +// items are known by name, and files carrying exactly the shapes that would make a recognizer +// silently wrong -- a macro invocation at a scope boundary, and Rust-looking source inside a raw +// string literal, which is the defect the deleted Python census actually committed (nine phantom +// items in one diff, recorded in gunbc.bare_reference_scanner_admission). +data fixture_ordinary_source: String = "use std::fmt;\nuse std::io::Write;\n\npub struct Thing {\n field: u32,\n}\n\n/// doc line\n#[derive(Debug)]\npub enum Mood {\n Calm,\n Loud,\n}\n\npub fn free_function(x: u32) -> u32 {\n x + 1\n}\n\nimpl std::fmt::Display for Mood {\n fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n write!(f, \"mood\")\n }\n}\n\nmod inner_tests {\n use super::*;\n\n fn helper() -> bool {\n true\n }\n}\n" + +data fixture_raw_string_reaching_column_zero: String = "pub fn fixture_text() -> &'static str {\n r#\"\nfn phantom_item() -> u32 {\n 1\n}\n\"#\n}\n" + +data fixture_header_opened_raw_string: String = "pub const FIXTURE_SRC: &str = r#\"\nfn phantom_item() -> u32 {\n 1\n}\n\"#;\n\npub fn after_the_literal() -> u32 {\n 1\n}\n" + +data fixture_const_and_const_fn: String = "pub const LIMIT: u32 = 4;\n\npub const fn doubled() -> u32 {\n LIMIT * 2\n}\n" + +// THE WRAPPED FORMS ARE THE POPULATION, NOT THE EXCEPTION. Measured over the seed's hand-maintained +// Rust before these three scopes existed, wrapped `use` lists alone put 58 files out of reach and +// multi-line `#[derive(...)]` a further dozen -- a reader that refused them would have been safe and +// useless, which is the other way a fail-closed recognizer fails. Each fixture carries the wrapped +// form AND a following item, because finding the following item is what separates a reader that +// consumed the continuation from one that swallowed the rest of the file. +data fixture_wrapped_use_and_derive: String = "use std::collections::{\n HashMap, HashSet,\n};\n\n#[derive(\n Debug, Clone, PartialEq,\n)]\npub struct Wrapped {\n field: u32,\n}\n\npub fn after_the_wrapping() -> u32 {\n 1\n}\n" + +data fixture_macro_block: String = "use std::cell::RefCell;\n\nthread_local! {\n static COUNTER: RefCell = RefCell::new(0);\n}\n\npub fn after_the_macro() -> u32 {\n 1\n}\n" + +fn ordinary_scan_keys() -> List { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_ordinary_source) { + RustFileScanned { repo_path: _, items: items, macro_regions: _, line_count: _ } => scan_item_keys(items: items) + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => [] + } +} + +fn key_present(keys: List, key: String) -> Bool { + any(xs: keys, predicate: fn(k) { k == key }) +} + +// THE ITEMS ARE REQUIRED BY NAME AND NOT BY COUNT. A count assertion is satisfied by a recognizer +// that finds the right NUMBER of wrong things -- which is exactly how the deleted census reported +// eighteen items for a nine-item diff -- so every arm here names the identity it expects. +test fn scan_recovers_every_item_by_name() -> Bool { + let keys = ordinary_scan_keys() + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::struct::Thing") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::enum::Mood") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::free_function") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::impl_block::std::fmt::Display for Mood") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::impl_method::std::fmt::Display for Mood::fmt") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::mod::inner_tests") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::inner_tests.helper") +} + +// THE INLINE MODULE'S MEMBER CARRIES ITS MODULE PATH, which is the fact that makes the citation +// vocabulary joinable: gunbc.declaration_index_seed_growth cites seven declarations under +// v1_compiler.cli_run.bare_reference_scanner_tests. A reader that recorded `helper` bare would +// collide it with any file-level `helper` in the same file, and the join would silently treat an +// addition as already justified. +test fn inline_module_members_are_qualified_not_bare() -> Bool { + let keys = ordinary_scan_keys() + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::inner_tests.helper") + && !key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::helper") +} + +test fn a_wrapped_use_list_and_derive_attribute_do_not_hide_the_following_item() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_wrapped_use_and_derive) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: macro_regions, line_count: line_count } => { + let keys = scan_item_keys(items: items) + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::struct::Wrapped") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::after_the_wrapping") + && match attribute_scanned_line( + items: items, + macro_regions: macro_regions, + line_count: line_count, + line: 5 + ) { + AttributedToItem { item_key: key } => key == "src/v1/stage0/src/probe.rs::struct::Wrapped" + AttributedToFileLevel => false + AttributedToUnnamedMacroBlock { macro_name: _, line: _ } => false + AttributionOutOfRange { line: _, line_count: _ } => false + } + } + } +} + +// A MACRO BLOCK IS AN EXTENT WITH NO ITEM AND A CHANGE INSIDE IT IS NOT FILE-LEVEL. Three facts are +// required together and each is a different way to get this wrong: the file must scan (refusing it +// would put cli_run.rs, which carries a thread_local! block, permanently out of the census); the +// macro must mint no item (the grammar authority has no macro item form, and inventing one here +// would be the second roster this reader exists not to author); and a line inside it must NOT come +// back file-level, because a static added inside thread_local! is hand-Rust growth and file-level is +// the answer that would admit it silently. +test fn a_macro_block_is_an_extent_with_no_item_and_its_lines_are_unattributed() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_macro_block) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: macro_regions, line_count: line_count } => { + let keys = scan_item_keys(items: items) + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::after_the_macro") + && !key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::static::COUNTER") + && length(xs: macro_regions) == 1 + && match attribute_scanned_line( + items: items, + macro_regions: macro_regions, + line_count: line_count, + line: 4 + ) { + AttributedToUnnamedMacroBlock { macro_name: name, line: _ } => name == "thread_local" + AttributedToFileLevel => false + AttributedToItem { item_key: _ } => false + AttributionOutOfRange { line: _, line_count: _ } => false + } + } + } +} + +// AN UNRECOGNIZED STRUCTURAL LINE STILL REFUSES, and this arm is what keeps the three continuation +// scopes above from having quietly become a catch-all. `lazy_static!` opens a macro block, so the +// fixture that used to make this point no longer does; a bare brace at a scope boundary is not a +// macro, not an item header, not a use and not an attribute, and there is no arm that admits it. +test fn an_unrecognized_structural_line_refuses() -> Bool { + match scan_rust_source( + repo_path: "src/v1/stage0/src/probe.rs", + content: "use std::fmt;\n\n{\n 1\n}\n" + ) { + RustFileScanRefused { repo_path: _, line: line, line_text: _, detail: _ } => line == 3 + RustFileScanned { repo_path: _, items: _, macro_regions: _, line_count: _ } => false + } +} + +// THE PHANTOM-ITEM DEFECT, AUTHORED AS ITS OWN RED, IN BOTH OF ITS SHAPES. The deleted census read +// source as text and counted `fn phantom_item` inside a raw string literal as a Rust item, nine +// times in one diff. +// +// This fixture opens its literal inside a function BODY, which is the shape the reader tracks +// least: the opening delimiter never appears on an item header, so the literal's text arrives at a +// scope boundary unannounced. The reader has no arm that admits it -- what the text is not, is a +// recognized structural line -- so the file refuses rather than reporting an item nobody wrote. +// Refusing is the honest answer here and is what the change observation turns into a line-stop. +test fn source_inside_a_raw_string_literal_mints_no_item() -> Bool { + match scan_rust_source( + repo_path: "src/v1/stage0/src/probe.rs", + content: fixture_raw_string_reaching_column_zero + ) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => true + RustFileScanned { repo_path: _, items: items, macro_regions: _, line_count: _ } => + !key_present(keys: scan_item_keys(items: items), key: "src/v1/stage0/src/probe.rs::fn::phantom_item") + && false + } +} + +// THE SECOND SHAPE IS THE COMMON ONE IN THIS TREE AND IT MUST NOT MERELY REFUSE. A `const … = r#"` +// fixture table is how most Rust source in this seed carries planted .dag and Rust text, and a +// reader that refused every such file would refuse most of the population it exists to census -- +// safe, and useless. The literal's terminator is recovered from its own opening delimiter, so the +// constant closes where it really closes, the item after it is found, and the declaration planted +// inside the literal is not. All three are required together: finding the following item is what +// separates a reader that skipped the literal from one that swallowed the rest of the file. +test fn a_header_opened_raw_string_is_skipped_whole_and_mints_no_item() -> Bool { + match scan_rust_source( + repo_path: "src/v1/stage0/src/probe.rs", + content: fixture_header_opened_raw_string + ) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: _, line_count: _ } => { + let keys = scan_item_keys(items: items) + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::const::FIXTURE_SRC") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::after_the_literal") + && !key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::phantom_item") + } + } +} + +// `const` IS BOTH A MODIFIER AND AN ITEM KEYWORD, and the two spellings must not collapse. A reader +// that peeled `const` unconditionally records the constant as a function named nothing; one that +// never peeled it records `const fn doubled` as a constant named `fn`. Both fixtures are in one +// file so a repair in either direction reds here. +test fn const_item_and_const_fn_are_distinguished() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_const_and_const_fn) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: _, line_count: _ } => { + let keys = scan_item_keys(items: items) + key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::const::LIMIT") + && key_present(keys: keys, key: "src/v1/stage0/src/probe.rs::fn::doubled") + } + } +} + +// THE INNERMOST CONTAINING ITEM WINS. A method's lines lie inside both the method and its impl +// block; reporting the impl block would attribute a method edit to the enclosing scope, which is +// how a new capability lands with nothing to report. +test fn attribution_reports_the_innermost_item() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_ordinary_source) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: macro_regions, line_count: line_count } => + match attribute_scanned_line(items: items, macro_regions: macro_regions, line_count: line_count, line: 20) { + AttributedToItem { item_key: key } => + key == "src/v1/stage0/src/probe.rs::impl_method::std::fmt::Display for Mood::fmt" + AttributedToFileLevel => false + AttributedToUnnamedMacroBlock { macro_name: _, line: _ } => false + AttributionOutOfRange { line: _, line_count: _ } => false + } + } +} + +test fn a_use_line_attributes_to_file_level_and_not_to_an_item() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_ordinary_source) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: macro_regions, line_count: line_count } => + match attribute_scanned_line(items: items, macro_regions: macro_regions, line_count: line_count, line: 1) { + AttributedToFileLevel => true + AttributedToItem { item_key: _ } => false + AttributedToUnnamedMacroBlock { macro_name: _, line: _ } => false + AttributionOutOfRange { line: _, line_count: _ } => false + } + } +} + +test fn a_line_beyond_the_file_is_out_of_range_and_not_file_level() -> Bool { + match scan_rust_source(repo_path: "src/v1/stage0/src/probe.rs", content: fixture_const_and_const_fn) { + RustFileScanRefused { repo_path: _, line: _, line_text: _, detail: _ } => false + RustFileScanned { repo_path: _, items: items, macro_regions: macro_regions, line_count: line_count } => + match attribute_scanned_line(items: items, macro_regions: macro_regions, line_count: line_count, line: line_count + 50) { + AttributionOutOfRange { line: _, line_count: _ } => true + AttributedToFileLevel => false + AttributedToUnnamedMacroBlock { macro_name: _, line: _ } => false + AttributedToItem { item_key: _ } => false + } + } +} + +// THE KEYWORD MAPPING IS CHECKED AGAINST THE GRAMMAR AUTHORITY RATHER THAN AGAINST ITSELF. A +// keyword added to extdeps.languages.rust.syntax rust_item_forms and not named in the kind mapping +// would otherwise acquire a plausible kind in silence. +test fn every_grammar_authority_item_keyword_has_a_named_kind() -> Bool { + rust_item_kind_keyword_mapping_is_total() +} diff --git a/dag/test/claim/seed_growth_admission_witness_test.dag b/dag/test/claim/seed_growth_admission_witness_test.dag index 5da5d71fa63..f9a4de4ffa4 100644 --- a/dag/test/claim/seed_growth_admission_witness_test.dag +++ b/dag/test/claim/seed_growth_admission_witness_test.dag @@ -2,20 +2,16 @@ module test.claim.seed_growth_admission_witness import gunbc.roadmap_authority { observation_scoped_run_seed_growth_justification } import gunbc.seed_growth_admission { - SeedGrowthAdmissionComplete, - SeedGrowthAdmissionRefused, seed_growth_admission_g0_population_note, - seed_growth_admission_join_scaffold, - seed_growth_g0_census_host_scaffold, seed_growth_forward_freeze_policy_note, - seed_growth_join_declarations, + seed_growth_unjustified_additions, + seed_growth_stale_justifications, seed_growth_roster_all_declarations, seed_growth_roster_mechanical_checks_holds, seed_growth_roster_well_formed_gate_holds } import gunbc.stage0_rust_host_observation { stage0_rust_observation_seed_growth_justification } import std.decl_ref { DeclarationRef, WholeDeclaration } -import std.disposition { Scaffold } import std.roster_frontier { declaration_ref_display_key } import v2.std.algebra { any, length } import v2.std.text { String } @@ -24,19 +20,28 @@ test fn seed_growth_roster_mechanical_checks_hold() -> Bool { seed_growth_roster_mechanical_checks_holds() } +// THE JOIN IS SPLIT AND THE SPLIT IS THE ASSERTION, not an implementation detail beside it. Under +// the single-candidate-set form these two calls were one, so the population that found an +// unjustified addition was the same one that declared every untouched roster row stale. Here the +// SAME candidate list is passed to both functions and they must answer differently: the rogue is an +// unjustified addition, and passing it as the live population makes every real roster row stale +// rather than none. A reimplementation that collapsed them again reds on the second conjunct. test fn seed_growth_join_refuses_unknown_declaration() -> Bool { let rogue = DeclarationRef { module_path: "v1_compiler.cli_run", decl_name: "totally_unjustified_hand_rust_symbol", field: WholeDeclaration } - match seed_growth_join_declarations(candidates: [rogue]) { - SeedGrowthAdmissionRefused { unjustified_additions: bad, detail: detail } => - length(xs: bad) == 1 - && string_contains(s: detail, pattern: "lack SeedGrowthJustification") - SeedGrowthAdmissionComplete { justified_items: _, stale_justifications: _, duplicate_justifications: _, uncitable_items: _ } => - false - } + length(xs: seed_growth_unjustified_additions(added: [rogue])) == 1 + && length(xs: seed_growth_stale_justifications(live_items: [rogue])) + == length(xs: seed_growth_roster_all_declarations()) +} + +// A ROSTER ROW THAT STILL NAMES A LIVE ITEM IS NOT STALE. Without this control the staleness +// function above is satisfied by one that reports everything stale, which reports nothing. +test fn a_live_roster_declaration_is_not_reported_stale() -> Bool { + let live = seed_growth_roster_all_declarations() + length(xs: seed_growth_stale_justifications(live_items: live)) == 0 } test fn forward_freeze_policy_requires_item_enumeration() -> Bool { @@ -48,25 +53,18 @@ test fn witness_seed_growth_roster_well_formed_gate_holds() -> Bool { seed_growth_roster_well_formed_gate_holds() } -test fn seed_growth_admission_join_is_scaffold_until_diff_population() -> Bool { - match seed_growth_admission_join_scaffold { - Scaffold { dissolves_to: _, bind: _ } => true - _ => false - } -} - -test fn seed_growth_g0_census_host_is_scaffold_until_modeled_producer() -> Bool { - match seed_growth_g0_census_host_scaffold { - Scaffold { dissolves_to: _, bind: bind } => - bind.module_path == "gunbc.rust_item_identity" - && bind.decl_name == "rust_item_host_observation_scaffold_note" - _ => false - } -} - -test fn g0_population_note_supersedes_stale_cli_run_prose() -> Bool { - string_contains(s: seed_growth_admission_g0_population_note, pattern: "cli_run_hollowing_plan") - && string_contains(s: seed_growth_admission_g0_population_note, pattern: "does NOT pin numeric literals") +// WHAT REPLACED THE TWO SCAFFOLD WITNESSES, AND WHY THIS IS NOT AN EQUIVALENT CHECK IN A NEW +// SPELLING. Two arms here asserted that seed_growth_admission_join_scaffold and +// seed_growth_g0_census_host_scaffold were Scaffold dispositions, and a third asserted that a prose +// row CONTAINED two substrings. All three are gone with their subjects: the scaffolds retired when +// their triggers fired, and the prose assertion was the prose-self-match antipattern -- its only +// content was that someone had written a sentence, so it could not tell a repair from a regression. +// The g0 population note is now asserted through the mechanism it describes rather than through its +// own text: the census host it names is a function, and the arms above and in +// rust_item_host_observation_witness_test execute it. +test fn the_g0_population_note_names_a_host_that_resolves_to_a_function() -> Bool { + string_contains(s: seed_growth_admission_g0_population_note, pattern: "gunbc.rust_item_host_observation") + && seed_growth_roster_well_formed_gate_holds() } test fn stage0_justification_rows_are_in_roster() -> Bool { diff --git a/dag/tools/rust_item_census_instrument.dag b/dag/tools/rust_item_census_instrument.dag new file mode 100644 index 00000000000..2b06aa28a51 --- /dev/null +++ b/dag/tools/rust_item_census_instrument.dag @@ -0,0 +1,203 @@ +module tools.rust_item_census_instrument + +import extdeps.filesystem.filesystem_io { Filesystem } +import gunbc.rust_item_host_observation { + LiveHandRustObservation, + LiveHandRustObserved, + LiveHandRustHostRefused, + RustFileScanRefused, + RustItemChangeObservation, + RustItemChangeObserved, + RustItemChangeHostRefused, + RustItemChangeScanRefused, + RustItemChangeUnattributed, + current_rust_item_change_observation, + observe_current_live_hand_rust +} +import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } +import gunbc.rust_item_scan { RustFileScan, RustFileScanned } +import gunbc.seed_growth_admission { + SeedGrowthAdmissionComplete, + SeedGrowthAdmissionRefused, + SeedGrowthAdmissionVerdict, + seed_growth_admit_change +} +import std.process { ProcessExit, ExitSuccess, exit_failure } +import std.types { Bool, Int, List, String } +import v2.std.algebra { length, list_append, list_map } + +// THE NAMED INSTRUMENT FOR THE ITEM-GRAIN CENSUS. +// +// gunbc run --source-root dag --source-root src/v2 \ +// --entry dag/tools/rust_item_census_instrument.dag --function census \ +// --arg report=/tmp/seed-growth.txt +// +// DESIGN's standing rule is name the instrument, never transcribe its output, and until this module +// landed there was no instrument to name: scripts/rust_item_census.py was deleted by gunbc#9132 and +// nothing replaced it, so every hand-item and hand-LOC figure quoted in a seed-growth receipt since +// then named nothing that could produce it. This is the producer those figures may cite. +// +// THE REPORT IS WRITTEN RATHER THAN RETURNED because `gunbc run` maps a returned value to an exit +// status and has no value-printing verb -- the same wall tools.emission_entry_instrument records. +// +// THE EXIT STATUS REPORTS THE SUBJECT HERE, AND THAT IS A DELIBERATE DIFFERENCE from the emission +// instrument beside it, whose status reports the instrument. This one is an ADMISSION verdict: its +// whole purpose is to stop a line, so a run that completes and finds an unjustified addition must +// not exit zero. A run that could not observe at all also exits nonzero, and the report names which +// of the two happened -- the distinction is carried in the text rather than in the status, because +// an admission gate that exited zero on an unobservable change would be the empty-observation +// narrow arriving through the exit code. +data rust_item_census_instrument_note: String = "Named instrument for the item-grain Rust census. Writes the merge-base-to-HEAD change population, the live hand-Rust population, and the seed-growth admission verdict computed from them. Exit status is the ADMISSION verdict, not the instrument's own completion: an unjustified addition and an unobservable change both exit nonzero, and the report says which." + +fn item_row(item: RustItemIdentity) -> String { + concat( + rust_item_identity_key(item: item), + concat("\tstart=", concat(item.start_line as String, concat("\tloc=", item.loc as String))) + ) +} + +fn section(title: String, rows: List) -> List { + list_append( + left: [concat(title, concat(" (", concat(length(xs: rows) as String, ")")))], + right: rows + ) +} + +fn change_rows(change: RustItemChangeObservation) -> List { + match change { + RustItemChangeObserved { + added_hand_items: added, + modified_hand_items: modified, + deleted_hand_items: deleted, + generated_changes: generated + } => + list_append( + left: list_append( + left: section(title: "added-hand-items", rows: list_map(xs: added, f: fn(i) { item_row(item: i) })), + right: section(title: "modified-hand-items", rows: list_map(xs: modified, f: fn(i) { item_row(item: i) })) + ), + right: list_append( + left: section(title: "deleted-hand-items", rows: list_map(xs: deleted, f: fn(i) { item_row(item: i) })), + right: section(title: "generated-path-changes", rows: generated) + ) + ) + RustItemChangeUnattributed { repo_path: path, line: line, detail: detail } => + ["change-observation REFUSED: unattributed", concat(path, concat(":", concat(line as String, concat(" ", detail))))] + RustItemChangeScanRefused { repo_path: path, line: line, line_text: text, detail: detail } => + [ + "change-observation REFUSED: source not readable at item grain", + concat(path, concat(":", concat(line as String, concat(" ", detail)))), + concat(" line: ", text) + ] + RustItemChangeHostRefused { detail: detail } => + ["change-observation REFUSED: host", detail] + } +} + +fn scan_refusal_row(scan: RustFileScan) -> String { + match scan { + RustFileScanRefused { repo_path: path, line: line, line_text: text, detail: detail } => + concat(path, concat(":", concat(line as String, concat(" ", concat(detail, concat(" -- line: ", text)))))) + RustFileScanned { repo_path: path, items: _, macro_regions: _, line_count: _ } => path + } +} + +fn live_rows(live: LiveHandRustObservation) -> List { + match live { + LiveHandRustObserved { items: items, unscannable: unscannable } => + list_append( + left: [concat("live-hand-items (", concat(length(xs: items) as String, ")"))], + right: section( + title: "live-files-not-readable-at-item-grain", + rows: list_map(xs: unscannable, f: fn(s) { scan_refusal_row(scan: s) }) + ) + ) + LiveHandRustHostRefused { detail: detail } => ["live-observation REFUSED: host", detail] + } +} + +fn verdict_rows(verdict: SeedGrowthAdmissionVerdict) -> List { + match verdict { + SeedGrowthAdmissionComplete { + justified_items: justified, + modified_items_requiring_disposition: modified, + stale_justifications: stale, + duplicate_justifications: duplicates, + uncitable_items: uncitable + } => + list_append( + left: list_append( + left: ["admission: COMPLETE"], + right: list_append( + left: section(title: "justified-additions", rows: justified), + right: section(title: "modified-items-requiring-disposition", rows: modified) + ) + ), + right: list_append( + left: section(title: "stale-justifications", rows: stale), + right: list_append( + left: section(title: "duplicate-justifications", rows: duplicates), + right: section(title: "uncitable-items", rows: uncitable) + ) + ) + ) + SeedGrowthAdmissionRefused { unjustified_additions: unjustified, detail: detail } => + list_append( + left: ["admission: REFUSED", detail], + right: section(title: "unjustified-additions", rows: unjustified) + ) + } +} + +fn verdict_is_complete(verdict: SeedGrowthAdmissionVerdict) -> Bool { + match verdict { + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => true + SeedGrowthAdmissionRefused { unjustified_additions: _, detail: _ } => false + } +} + +fn census_report_text( + change: RustItemChangeObservation, + live: LiveHandRustObservation, + verdict: SeedGrowthAdmissionVerdict +) -> String { + concat( + join( + list_append( + left: list_append(left: change_rows(change: change), right: live_rows(live: live)), + right: verdict_rows(verdict: verdict) + ), + "\n" + ), + "\n" + ) +} + +fn census(report: String) -> ProcessExit { + if report == "" { + exit_failure(reason: "census: --arg report= is required — the census is written, not printed") + } else { + let change = current_rust_item_change_observation() + let live = observe_current_live_hand_rust() + let verdict = seed_growth_admit_change(change: change, live: live) + let write = Filesystem.Write(path: report, content: census_report_text(change: change, live: live, verdict: verdict)) + if !write.success { + exit_failure(reason: concat("census: could not write the report to ", report)) + } else if verdict_is_complete(verdict: verdict) { + ExitSuccess + } else { + exit_failure( + reason: concat( + "census: seed-growth admission refused — the report at ", + concat(report, " names the additions or the refusal") + ) + ) + } + } +} From 0b05d6a0c02ed7aee3d622f8d384091237a31c77 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 13:22:57 +0000 Subject: [PATCH 2/3] The origin relation: an added or modified hand-Rust item must show its capability was modeled before it, and that the model constrains it CapabilityAlreadyModeled was payload-free, so it could be satisfied by inspecting the final candidate -- which cannot distinguish a model that existed before its realization from a model authored beside the realization that inspired it. That distinction is the whole question the arm existed to ask, so the vocabulary moves to gunbc.capability_origin and grows a payload: an authority, and where a capability introduced in this change came from. THREE TESTS, ESTABLISHING THREE DIFFERENT THINGS. The leaf test proves Rust owns no semantic decision. The origin test proves the semantics predate the Rust: remove the hand-Rust delta, build E1, and the authority must still hold. The conformance test proves the model CONSTRAINS the Rust rather than merely agreeing with it: mutate the realization alone, and the predicted evidence must go red while E1 stays green. A retro-described implementation passes the first two and fails the third, because its fixtures were derived from the behavior they check -- which is why FixtureProvenance refuses GeneratedByRealization even when the mutation went red. ADDITIONS ARE COVERED, NOT ONLY MODIFICATIONS. A newly added item can carry a perfectly good justification row and still have been retro-modeled in the same change, so the relation participates in the addition arm and roster membership is no longer sufficient. THE EPOCH IS IDENTIFIED AND EXPIRES. ModelEpochRef names the exact derived E1 population, and the subject carries both the base and the head it was derived against. A probe taken against another base refuses rather than admitting -- the reversed-diff class stated as evidence rather than as advice. RUNG: the vocabulary is structural (no arm means "the Rust behaves this way, therefore this is the intended model", and no not-executed arm has a path to an admitting verdict). The tests themselves are executed by nothing yet, so every real call today refuses. The next-rung trigger names the capability. Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/capability_origin.dag | 492 ++++++++++++++++++ dag/gunbc/seed_growth_admission.dag | 40 +- ...ust_item_host_observation_witness_test.dag | 267 +++++++++- dag/tools/rust_item_census_instrument.dag | 3 +- 4 files changed, 788 insertions(+), 14 deletions(-) create mode 100644 dag/gunbc/capability_origin.dag diff --git a/dag/gunbc/capability_origin.dag b/dag/gunbc/capability_origin.dag new file mode 100644 index 00000000000..63a4144f2ce --- /dev/null +++ b/dag/gunbc/capability_origin.dag @@ -0,0 +1,492 @@ +module gunbc.capability_origin + +import extdeps.external_authority { ExternalAuthority } +import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } +import std.decl_ref { DeclarationRef } +import std.roster_frontier { declaration_ref_display_key } +import std.types { Bool, List, String } +import v2.std.algebra { HeadAbsent, HeadFound, filter, fold_list, list_append, list_head, list_map } + +// WHERE THE SEMANTICS CAME FROM IS A DIFFERENT QUESTION FROM WHETHER RUST OWNS A DECISION. +// +// The leaf test asks whether a hand-Rust item makes a semantic choice the substrate should own. An +// implementation described back into the model AFTER it was written passes that test perfectly: the +// .dag authority exists, the Rust defers to it, every arm lines up. What it does not establish is +// that the model was prior -- and a model authored beside the realization that inspired it is the +// realization legislating for the substrate with an extra step. +// +// So this module carries the ORIGIN relation, and it is a second test beside the leaf test rather +// than a replacement for it. +data capability_origin_note: String = "ORIGIN RELATION for hand-authored Rust growth, the second test beside gunbc.seed_growth_admission's leaf test. THE LEAF TEST asks whether Rust owns a semantic decision; THE ORIGIN TEST asks where the semantics came from, and a retro-described implementation passes the leaf test perfectly. THE FIELD THIS REPLACES: CapabilityAlreadyModeled was payload-free, so it could be satisfied by inspecting the final candidate -- which cannot distinguish a model that existed before its realization from a model added beside the realization that inspired it, and that distinction is the whole question. THERE IS DELIBERATELY NO ARM MEANING 'the Rust behaves this way, therefore this is the intended model': that state is unrepresentable here rather than checked (DESIGN section 5, construction over validation). REJECTED AS EVIDENCE, each because it is satisfiable by a retro-described implementation: that the .dag was committed first (sufficient but neither necessary nor conclusive, and erased by squash merge); that the model carries refusal arms the realization does not implement (establishes an implementation gap, not model independence); final-tree agreement; the full candidate compiling; and a comment in the Rust naming the model." + +// THE GROUND IS WHAT MAKES A MODEL INTRODUCED IN THIS CHANGE INDEPENDENT OF THE RUST BESIDE IT. +// +// A capability already on base needs no ground -- it was there before this change could have +// inspired it. A capability introduced in the same change does, and it must come from somewhere +// other than the realization: a predecessor whose contract it preserves, an external contract the +// repository does not author, or an operator ruling. Nothing else grounds it, and the absence of a +// fourth arm is the point. +type CapabilityGround + = ExistingCapabilityMigration { + predecessor: DeclarationRef, + preserved_contract: DeclarationRef + } + | ExternalContractGround { authority: ExternalAuthority } + | OperatorRatifiedModel { ruling: DeclarationRef } + +type ModeledCapabilityOrigin + = CapabilityPresentOnBase { authority: DeclarationRef } + | CapabilityIntroducedInModelEpoch { + authority: DeclarationRef, + model_epoch: ModelEpochRef, + ground: CapabilityGround + } + +// NoNewCapability STAYS CORRECT and is not a weaker sibling of the modeled arm: an edit that only +// repairs, narrows, moves or deletes originates nothing, so there is no origin to state and +// demanding one would make every mechanical edit carry ceremony. +type CapabilityOrigin + = NoNewCapability + | ModeledCapability { origin: ModeledCapabilityOrigin } + +fn capability_origin_authority(origin: ModeledCapabilityOrigin) -> DeclarationRef { + match origin { + CapabilityPresentOnBase { authority: authority } => authority + CapabilityIntroducedInModelEpoch { authority: authority, model_epoch: _, ground: _ } => authority + } +} + +// THE TWO SUBJECTS, AND WHY THEY ARE CONSTRUCTED RATHER THAN OBSERVED. +// +// "Which file was typed first" is not usable: it is unknowable from the candidate, it is +// rewritable, and squash merge erases it. So the instrument constructs two logical subjects from +// one candidate and asks a question of the first. +// +// E1 model epoch base + the .dag delta, MINUS every added or modified hand-Rust item +// E2 realization epoch E1 + the authorized native leaves +// +// One commit may carry both deltas; nothing here requires two commits, and no author states which +// epoch they were working in. +// +// DELETIONS ARE NOT EXCLUDED FROM E1, and that is a decision rather than an oversight. Excluding a +// deletion would mean RESTORING the deleted item into the model epoch, which is the opposite of +// what the exclusion is for: the exclusion removes what could have originated the capability, and a +// deletion originates nothing. This mirrors the rule one module over that deletions offset nothing. +// THE EPOCH IS IDENTIFIED, NOT DESCRIBED. An origin arm that names only an authority and a ground +// is still a claim about the FINAL CANDIDATE -- it says a model exists and where it came from, and +// says nothing about which tree was actually tested. ModelEpochRef names the exact derived E1 +// population, so the admission join can establish that the cited authority held in a tree that +// existed before the Rust delta was admitted, rather than in the candidate that contains it. +type ModelEpochRef + = SyntheticTreeIdentity { tree_id: String } + | ModelOnlyContentIdentity { content_hash: String } + +fn model_epoch_ref_key(ref: ModelEpochRef) -> String { + match ref { + SyntheticTreeIdentity { tree_id: tree_id } => concat("tree:", tree_id) + ModelOnlyContentIdentity { content_hash: content_hash } => concat("content:", content_hash) + } +} + +// A CLAIM ABOUT A DELTA REQUIRES AN OBSERVER WHOSE BASE IS THE DELTA'S ACTUAL BASE. +// +// This carrier holds BOTH revisions because holding only one makes the failure unrepresentable in +// the wrong direction: with a base alone, a candidate that has since integrated main reads as +// congruent; with a head alone, an epoch derived against the wrong base reads as congruent. The +// pair is what expires. +// +// RECEIPT, AND IT IS WHY THIS IS A FIELD RATHER THAN A CONVENTION. A structural finding was raised +// against gunbc#9426 on 2026-08-27 reporting five scm production modules changed, a witness file +// deleted and a type removed. Every one of those was main's own work rendered IN REVERSE, because +// the diff was taken two-dot from CURRENT main to a branch whose base predated it -- main-side +// additions classified as candidate deletions. Nothing in the report was true and all of it was +// internally consistent, which is the shape that gets past a reader. So the epoch records the base +// it was derived against and the join refuses on mismatch: the class becomes unrepresentable rather +// than discouraged. +type ModelEpochSubject { + base_revision: String, + head_revision: String, + epoch_ref: ModelEpochRef, + excluded_hand_item_keys: List +} + +fn model_epoch_subject( + base_revision: String, + head_revision: String, + epoch_ref: ModelEpochRef, + added: List, + modified: List +) -> ModelEpochSubject { + ModelEpochSubject { + base_revision: base_revision, + head_revision: head_revision, + epoch_ref: epoch_ref, + excluded_hand_item_keys: list_append( + left: list_map(xs: added, f: fn(item) { rust_item_identity_key(item: item) }), + right: list_map(xs: modified, f: fn(item) { rust_item_identity_key(item: item) }) + ) + } +} + +// THE EPOCH EXPIRES WHEN EITHER END MOVES. A base that advanced or a branch that integrated main +// invalidates both E1 and E2, and they are reconstructed rather than re-cited. +fn epoch_congruent(epoch: ModelEpochSubject, actual_base: String, actual_head: String) -> Bool { + epoch.base_revision == actual_base && epoch.head_revision == actual_head +} + +fn realization_epoch_leaf_keys(added: List) -> List { + list_map(xs: added, f: fn(item) { rust_item_identity_key(item: item) }) +} + +// THE ORIGIN TEST IS AN EXECUTION, AND THIS CARRIER CANNOT PRETEND IT RAN. +// +// The test: remove the hand-Rust delta entirely, build E1, and require that the capability +// authority still resolves, compiles, executes its semantic witnesses and produces its predicted +// reds. If producing the model projection REQUIRES the new hand-Rust leaf, the model is not +// independently prior and that substrate capability separates first. +// +// A probe that did not run and a probe that found the authority absent must not render alike, so +// ProbeNotExecuted is its own arm and never collapses into absence -- an unreached probe reported +// as a clean answer is the execution-provenance loss DESIGN records. +type ModelEpochProbeResult + = AuthorityHoldsInModelEpoch { authority: DeclarationRef, epoch: ModelEpochSubject } + | AuthorityRequiresRealization { + authority: DeclarationRef, + epoch: ModelEpochSubject, + detail: String + } + | ProbeNotExecuted { authority: DeclarationRef, detail: String } + +type OriginTestVerdict + = ModelIndependentlyPrior { authority: DeclarationRef } + | OriginRefused { authority: DeclarationRef, detail: String } + +// A capability already on base is prior by construction and needs no probe: the base predates the +// change, so nothing in the change could have originated it. Only the introduced arm is probed. +fn origin_test_verdict(origin: ModeledCapabilityOrigin, probe: ModelEpochProbeResult) -> OriginTestVerdict { + match origin { + CapabilityPresentOnBase { authority: authority } => ModelIndependentlyPrior { authority: authority } + CapabilityIntroducedInModelEpoch { authority: authority, model_epoch: cited_epoch, ground: _ } => + match probe { + AuthorityHoldsInModelEpoch { authority: probed, epoch: probed_epoch } => + if model_epoch_ref_key(ref: probed_epoch.epoch_ref) == model_epoch_ref_key(ref: cited_epoch) { + ModelIndependentlyPrior { authority: probed } + } else { + OriginRefused { + authority: probed, + detail: concat( + "the probe tested a different model epoch than the origin arm cites: probed ", + concat( + model_epoch_ref_key(ref: probed_epoch.epoch_ref), + concat(", cited ", model_epoch_ref_key(ref: cited_epoch)) + ) + ) + } + } + AuthorityRequiresRealization { authority: probed, epoch: _, detail: detail } => + OriginRefused { + authority: probed, + detail: concat("model projection requires the new hand-Rust leaf; that substrate capability separates first: ", detail) + } + ProbeNotExecuted { authority: probed, detail: detail } => + OriginRefused { + authority: probed, + detail: concat("origin test did not execute; an unperformed test admits nothing: ", detail) + } + } + } +} + +fn origin_verdict_admits(verdict: OriginTestVerdict) -> Bool { + match verdict { + ModelIndependentlyPrior { authority: _ } => true + OriginRefused { authority: _, detail: _ } => false + } +} + +fn origin_verdict_display(verdict: OriginTestVerdict) -> String { + match verdict { + ModelIndependentlyPrior { authority: authority } => declaration_ref_display_key(ref: authority) + OriginRefused { authority: authority, detail: detail } => + concat(declaration_ref_display_key(ref: authority), concat(": ", detail)) + } +} + +// AN ADDED LEAF CARRIES ITS ORIGIN TOO, AND THIS IS THE HALF AN ADMISSION WALL LOSES FIRST. +// +// The modification arm closes the large-existing-function escape: capability added to a +// two-thousand-line function without minting one new item. But a NEWLY ADDED item can carry a +// perfectly good justification row and still have been retro-modeled in the same change, so the +// origin relation participates in the addition arm as well. Everything below is stated once and +// read twice -- prospectively by the admission join, contemporaneously by the terminal roster join +// when the final cut lands -- and no parallel list is introduced for either reader. +type AuthorizedTemporarySeedBridge { + item: DeclarationRef, + dag_authority: DeclarationRef, + origin: ModeledCapabilityOrigin, + realization_role: String, + owning_lane: String, + deletion_trigger: String, + current_boundary: String +} + +fn seed_bridge_states_its_origin(bridge: AuthorizedTemporarySeedBridge) -> Bool { + declaration_ref_display_key(ref: capability_origin_authority(origin: bridge.origin)) + == declaration_ref_display_key(ref: bridge.dag_authority) +} + +fn seed_bridge_well_formed(bridge: AuthorizedTemporarySeedBridge) -> Bool { + seed_bridge_states_its_origin(bridge: bridge) + && bridge.realization_role != "" + && bridge.owning_lane != "" + && bridge.deletion_trigger != "" + && bridge.current_boundary != "" +} + +data capability_origin_rung_note: String = "RUNG, STATED AT THE GRAIN THAT IS HONEST RATHER THAN THE ONE THAT READS BEST. THE VOCABULARY IS STRUCTURAL: no arm of CapabilityGround or ModeledCapabilityOrigin means 'the Rust behaves this way, therefore this is the intended model', so that claim is unwritable here rather than refused by a check; origin_test_verdict has no path from ProbeNotExecuted to an admitting verdict, conformance_verdict has none from ConformanceProbeNotExecuted, and an added item whose bridge is absent refuses rather than falling through -- so an unperformed test cannot render as a passed one anywhere in this carrier. THREE TESTS, NOT ONE, AND THEY ESTABLISH DIFFERENT THINGS: the LEAF test (gunbc.seed_growth_admission) proves Rust owns no semantic decision; the ORIGIN test proves the semantics predate the Rust; the CONFORMANCE test proves the model CONSTRAINS the Rust rather than merely agreeing with it. A retro-described implementation passes the first two and fails the third, because its fixtures were derived from the behavior they check -- which is why FixtureProvenance refuses GeneratedByRealization even when the mutation went red. THE TESTS THEMSELVES ARE NOT EXECUTED BY ANYTHING IN THIS REPOSITORY YET, and calling this carrier a wall would be exactly the inflation DESIGN section 4b(1) names as worse than sitting low. ModelEpochProbeResult and ConformanceProbeResult are the shapes an executing host must supply; no host supplies them, so every real call today lands on the not-executed arms and REFUSES, which is the correct fail-closed reading and not a passing one. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY RATHER THAN AN ARTIFACT THAT WOULD CONTRIBUTE TO ONE: an executable route that (1) materializes the E1 subject from a candidate against its ACTUAL proposed base -- base plus the .dag delta with every added and modified hand-Rust item removed -- (2) resolves and compiles the cited capability authority inside it and runs that authority's semantic controls, and (3) mutates the Rust realization alone and observes the predicted conformance evidence go red while E1 stays green, returning all of it from real builds rather than from an author's assertion. A route that constructs the subject without building it does not retire this row; neither does one that builds a single entry when the authority's controls span several; and neither does one that derives its base from whatever ref the caller happened to have." + +// THE PROBE IS LOOKED UP, NEVER SUPPLIED AS A VERDICT, AND THE DIFFERENCE IS THE WHOLE WALL. +// +// A caller that could hand in an OriginTestVerdict could hand in an admitting one, which would make +// this construction a place to write down a conclusion rather than a place to reach one. So the +// join accepts only ModelEpochProbeResult rows -- what an executing host observed -- and derives +// every verdict itself. A bridge whose authority no probe row names does not fall through to some +// neutral answer: it lands on ProbeNotExecuted and refuses. +fn probe_for_authority(probes: List, authority: DeclarationRef) -> ModelEpochProbeResult { + let wanted = declaration_ref_display_key(ref: authority) + let matching = filter( + xs: probes, + predicate: fn(p) { declaration_ref_display_key(ref: probe_authority(probe: p)) == wanted } + ) + match list_head(xs: matching) { + HeadFound { value: p } => p + HeadAbsent => ProbeNotExecuted { + authority: authority, + detail: "no model-epoch probe row names this authority" + } + } +} + +fn probe_authority(probe: ModelEpochProbeResult) -> DeclarationRef { + match probe { + AuthorityHoldsInModelEpoch { authority: authority, epoch: _ } => authority + AuthorityRequiresRealization { authority: authority, epoch: _, detail: _ } => authority + ProbeNotExecuted { authority: authority, detail: _ } => authority + } +} + +fn bridge_origin_verdict(bridge: AuthorizedTemporarySeedBridge, evidence: AdmissionEvidence) -> OriginTestVerdict { + let authority = capability_origin_authority(origin: bridge.origin) + let probe = probe_for_authority(probes: evidence.model_epoch_probes, authority: authority) + if !probe_epoch_congruent(probe: probe, evidence: evidence) { + OriginRefused { + authority: authority, + detail: "the model-epoch probe was taken against a different base or head than the change being admitted, so its epoch has expired and E1 must be reconstructed" + } + } else { + origin_test_verdict(origin: bridge.origin, probe: probe) + } +} + +fn bridge_for_item(bridges: List, item: DeclarationRef) -> List { + let wanted = declaration_ref_display_key(ref: item) + filter( + xs: bridges, + predicate: fn(b) { declaration_ref_display_key(ref: b.item) == wanted } + ) +} + +// AN ADDED ITEM WITH NO BRIDGE ROW REFUSES HERE TOO, AND THAT IS NOT A DUPLICATE OF THE LEAF TEST. +// The leaf test asks whether a SeedGrowthJustification row names the item; this asks whether the +// capability it realizes was independently prior, and then whether the model constrains it. A row +// can name an item perfectly while the model beside it was written to describe the Rust, so all +// three questions are asked of every addition. +fn item_origin_refusal(bridges: List, evidence: AdmissionEvidence, item: DeclarationRef) -> List { + match list_head(xs: bridge_for_item(bridges: bridges, item: item)) { + HeadAbsent => [ + concat( + declaration_ref_display_key(ref: item), + ": no AuthorizedTemporarySeedBridge states this item's capability origin" + ) + ] + HeadFound { value: bridge } => { + let origin = bridge_origin_verdict(bridge: bridge, evidence: evidence) + let conformance = conformance_verdict( + probe: conformance_probe_for_authority( + probes: evidence.conformance_probes, + authority: capability_origin_authority(origin: bridge.origin) + ) + ) + if !seed_bridge_well_formed(bridge: bridge) { + [ + concat( + declaration_ref_display_key(ref: item), + ": AuthorizedTemporarySeedBridge is not well formed -- role, lane, deletion trigger, boundary and a stated origin authority matching the .dag authority are all required" + ) + ] + } else if !origin_verdict_admits(verdict: origin) { + [origin_verdict_display(verdict: origin)] + } else if !conformance_verdict_admits(verdict: conformance) { + [conformance_verdict_display(verdict: conformance)] + } else { + [] as List + } + } + } +} + +fn origin_refusals(bridges: List, evidence: AdmissionEvidence, added: List) -> List { + fold_list( + xs: added, + empty: [] as List, + cons: fn(acc, item) { + list_append( + left: acc, + right: item_origin_refusal(bridges: bridges, evidence: evidence, item: item) + ) + } + ) +} + + +// THE THIRD TEST: DOES THE MODEL CONSTRAIN THE RUST, OR MERELY AGREE WITH IT? +// +// The leaf test proves Rust owns no decision. The origin test proves the semantics predate the +// Rust. Neither separates +// +// model -> expected observation contract -> Rust conformance +// +// from +// +// Rust behavior -> matching model fixture -> circular green +// +// because a retro-described implementation passes both: its authority resolves, its ground is real, +// and its fixtures agree with it exactly -- they were derived from the behavior they check. The +// distinguishing experiment is directional: MUTATE ONLY THE RUST REALIZATION. The predicted +// conformance evidence must go RED and the model epoch's own semantic tests must stay GREEN. A +// mutation nothing notices establishes that the fixtures follow the realization. +type FixtureProvenance + = AuthoredAgainstModelContract { contract: DeclarationRef } + | GeneratedByRealization { detail: String } + +type ConformanceProbeResult + = RealizationMutationRefused { + authority: DeclarationRef, + epoch: ModelEpochSubject, + fixture_provenance: FixtureProvenance + } + | RealizationMutationUnnoticed { + authority: DeclarationRef, + epoch: ModelEpochSubject, + detail: String + } + | ConformanceProbeNotExecuted { authority: DeclarationRef, detail: String } + +type ConformanceVerdict + = ModelConstrainsRealization { authority: DeclarationRef } + | ConformanceRefused { authority: DeclarationRef, detail: String } + +// FIXTURES GENERATED BY THE REALIZATION REFUSE EVEN WHEN THE MUTATION WENT RED, and that arm is not +// redundant with the mutation arm. A fixture recorded from the new implementation goes red under +// mutation perfectly well -- it is pinned to the behavior, so any change to the behavior breaks it. +// What it can never do is establish that the model predicted the behavior. Redness is necessary and +// provenance is what makes it evidence. +fn conformance_verdict(probe: ConformanceProbeResult) -> ConformanceVerdict { + match probe { + RealizationMutationRefused { authority: authority, epoch: _, fixture_provenance: provenance } => + match provenance { + AuthoredAgainstModelContract { contract: _ } => ModelConstrainsRealization { authority: authority } + GeneratedByRealization { detail: detail } => + ConformanceRefused { + authority: authority, + detail: concat("conformance fixtures were generated by the realization they check: ", detail) + } + } + RealizationMutationUnnoticed { authority: authority, epoch: _, detail: detail } => + ConformanceRefused { + authority: authority, + detail: concat("mutating the realization alone changed no predicted evidence, so the model does not constrain it: ", detail) + } + ConformanceProbeNotExecuted { authority: authority, detail: detail } => + ConformanceRefused { + authority: authority, + detail: concat("conformance test did not execute; an unperformed test admits nothing: ", detail) + } + } +} + +fn conformance_probe_authority(probe: ConformanceProbeResult) -> DeclarationRef { + match probe { + RealizationMutationRefused { authority: authority, epoch: _, fixture_provenance: _ } => authority + RealizationMutationUnnoticed { authority: authority, epoch: _, detail: _ } => authority + ConformanceProbeNotExecuted { authority: authority, detail: _ } => authority + } +} + +fn conformance_probe_for_authority(probes: List, authority: DeclarationRef) -> ConformanceProbeResult { + let wanted = declaration_ref_display_key(ref: authority) + let matching = filter( + xs: probes, + predicate: fn(p) { declaration_ref_display_key(ref: conformance_probe_authority(probe: p)) == wanted } + ) + match list_head(xs: matching) { + HeadFound { value: p } => p + HeadAbsent => ConformanceProbeNotExecuted { + authority: authority, + detail: "no realization-conformance probe row names this authority" + } + } +} + +fn conformance_verdict_admits(verdict: ConformanceVerdict) -> Bool { + match verdict { + ModelConstrainsRealization { authority: _ } => true + ConformanceRefused { authority: _, detail: _ } => false + } +} + +fn conformance_verdict_display(verdict: ConformanceVerdict) -> String { + match verdict { + ModelConstrainsRealization { authority: authority } => declaration_ref_display_key(ref: authority) + ConformanceRefused { authority: authority, detail: detail } => + concat(declaration_ref_display_key(ref: authority), concat(": ", detail)) + } +} + +// ONE CARRIER FOR WHAT AN EXECUTING HOST OBSERVED, AND IT CARRIES THE OBSERVER'S OWN BASE. +// +// The revisions are here rather than passed beside the evidence because the epoch-congruence check +// is a question about the observer, not about any one probe: a caller that could supply probes +// without stating which base it observed could hand in evidence derived against a base the change +// was never proposed against, which is exactly the reversed-diff class this module records above. +type AdmissionEvidence { + observed_base_revision: String, + observed_head_revision: String, + model_epoch_probes: List, + conformance_probes: List +} + +fn no_admission_evidence() -> AdmissionEvidence { + AdmissionEvidence { + observed_base_revision: "", + observed_head_revision: "", + model_epoch_probes: [], + conformance_probes: [] + } +} + +// EVERY PROBE MUST HAVE BEEN TAKEN AGAINST THE OBSERVER'S OWN BASE AND HEAD. A probe whose epoch +// names a different pair is evidence about a different change, and an expired epoch is not a weaker +// form of a current one. +fn probe_epoch_congruent(probe: ModelEpochProbeResult, evidence: AdmissionEvidence) -> Bool { + match probe { + AuthorityHoldsInModelEpoch { authority: _, epoch: epoch } => + epoch_congruent( + epoch: epoch, + actual_base: evidence.observed_base_revision, + actual_head: evidence.observed_head_revision + ) + AuthorityRequiresRealization { authority: _, epoch: _, detail: _ } => true + ProbeNotExecuted { authority: _, detail: _ } => true + } +} diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index b711c1c5157..d0e92192c60 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -26,6 +26,14 @@ import gunbc.rust_item_host_observation { projected_refused_keys, projected_uncitable_keys } +import gunbc.capability_origin { + AdmissionEvidence, + AuthorizedTemporarySeedBridge, + CapabilityOrigin, + ModeledCapability, + NoNewCapability, + origin_refusals +} import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } import gunbc.seed_growth { SeedGrowthJustification } import gunbc.stage0_rust_host_observation { stage0_rust_observation_seed_growth_justification } @@ -77,13 +85,22 @@ type SeedGrowthAdmissionVerdict // function without creating one new Rust item. Under that reading the forward freeze is reopened // through the largest functions in the seed and the whole construction is decorative, refusing only // the additions nobody needed to hide. So the arm states where the capability came from: -// NoNewCapability (the edit originates none) or CapabilityAlreadyModeled (the capability exists in -// the substrate and this is its realization). There is deliberately no third value naming a -// capability originated in Rust, because that is the state the freeze forbids; it is unrepresentable -// here rather than checked, which is the construction direction DESIGN section 5 asks for. -type CapabilityOrigin - = NoNewCapability - | CapabilityAlreadyModeled +// NoNewCapability (the edit originates none) or ModeledCapability carrying where the model came +// from. There is deliberately no value naming a capability originated in Rust, because that is the +// state the freeze forbids; it is unrepresentable rather than checked, which is the construction +// direction DESIGN section 5 asks for. +// +// THE VOCABULARY MOVED TO gunbc.capability_origin AND GREW A PAYLOAD, AND THE PAYLOAD IS THE +// CORRECTION. CapabilityAlreadyModeled stood here and was payload-free, so it could be satisfied by +// inspecting the final candidate -- which cannot distinguish a model that existed before its +// realization from a model added beside the realization that inspired it, and that distinction is +// the entire question the arm exists to ask. It now names its authority and, when the authority is +// itself new in this change, the independent ground that makes it prior. +// +// THE SAME RELATION GOVERNS ADDITIONS, which is the half an admission wall loses first: a newly +// added item can carry a perfectly good justification row and still have been retro-modeled in the +// same change, so seed_growth_admit_change asks both questions of every addition rather than +// treating roster membership as sufficient. type SeedGrowthChangeDisposition = SeedBridgeAuthorized @@ -226,7 +243,9 @@ fn seed_growth_added_projections(added_items: List) -> List, + evidence: AdmissionEvidence ) -> SeedGrowthAdmissionVerdict { match change { RustItemChangeUnattributed { repo_path: path, line: line, detail: detail } => @@ -268,6 +287,11 @@ fn seed_growth_admit_change( unjustified_additions: unjustified, detail: "hand Rust item(s) lack SeedGrowthJustification row" } + } else if length(xs: origin_refusals(bridges: bridges, evidence: evidence, added: projected_declaration_refs(projections: projections))) > 0 { + SeedGrowthAdmissionRefused { + unjustified_additions: origin_refusals(bridges: bridges, evidence: evidence, added: projected_declaration_refs(projections: projections)), + detail: "added hand Rust item(s) did not establish that the capability they realize was modeled independently of them" + } } else { match live { LiveHandRustHostRefused { detail: detail } => diff --git a/dag/test/claim/rust_item_host_observation_witness_test.dag b/dag/test/claim/rust_item_host_observation_witness_test.dag index a5989d85543..0a9042caef7 100644 --- a/dag/test/claim/rust_item_host_observation_witness_test.dag +++ b/dag/test/claim/rust_item_host_observation_witness_test.dag @@ -27,7 +27,23 @@ import gunbc.seed_growth_admission { SeedGrowthAdmissionRefused, seed_growth_admit_change } -import std.types { Bool, Int, List, String } +import gunbc.capability_origin { + AuthorizedTemporarySeedBridge, + CapabilityIntroducedInModelEpoch, + CapabilityPresentOnBase, + ExistingCapabilityMigration, + AdmissionEvidence, + AuthorityHoldsInModelEpoch, + AuthoredAgainstModelContract, + GeneratedByRealization, + ModelEpochSubject, + ModelOnlyContentIdentity, + RealizationMutationRefused, + RealizationMutationUnnoticed, + no_admission_evidence +} +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.types { Bool, Int, List, NonEmptyStr, String } import v2.std.algebra { any, length, list_map } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -237,7 +253,9 @@ test fn a_generated_path_change_is_classified_and_never_scanned_for_items() -> B test fn an_unjustified_addition_refuses_and_names_the_item() -> Bool { match seed_growth_admit_change( change: change_of(head_content: head_with_unjustified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), - live: live_of(head_content: head_with_unjustified_addition) + live: live_of(head_content: head_with_unjustified_addition), + bridges: [], + evidence: no_admission_evidence() ) { SeedGrowthAdmissionRefused { unjustified_additions: unjustified, detail: _ } => any(xs: unjustified, predicate: fn(k) { string_contains(s: k, pattern: "totally_unjustified_probe") }) @@ -251,12 +269,247 @@ test fn an_unjustified_addition_refuses_and_names_the_item() -> Bool { } } +// THE ORIGIN RELATION'S FIXTURES. The item the roster already justifies, stated three ways: with a +// bridge whose capability was already on base (prior by construction, no probe needed), with a +// bridge whose capability was introduced in this very change (prior only if a probe says the model +// epoch holds without the Rust), and with no bridge at all. +data justified_addition_item: DeclarationRef = DeclarationRef { + module_path: "v1_compiler.cli_run" as NonEmptyStr, + decl_name: "parse_stage0_cargo_manifest_bin_paths" as NonEmptyStr, + field: WholeDeclaration +} + +data justified_addition_authority: DeclarationRef = DeclarationRef { + module_path: "gunbc.stage0_rust_host_observation" as NonEmptyStr, + decl_name: "stage0_rust_observation_seed_growth_justification" as NonEmptyStr, + field: WholeDeclaration +} + +data justified_addition_bridge: AuthorizedTemporarySeedBridge = AuthorizedTemporarySeedBridge { + item: justified_addition_item, + dag_authority: justified_addition_authority, + origin: CapabilityPresentOnBase { authority: justified_addition_authority }, + realization_role: "host observation of a committed manifest", + owning_lane: "loyal-ram-883", + deletion_trigger: "the manifest read becomes a modeled filesystem operation", + current_boundary: "reads one committed file at a path the authority names" +} + +data retro_modeled_addition_bridge: AuthorizedTemporarySeedBridge = AuthorizedTemporarySeedBridge { + item: justified_addition_item, + dag_authority: justified_addition_authority, + origin: CapabilityIntroducedInModelEpoch { + authority: justified_addition_authority, + model_epoch: ModelOnlyContentIdentity { content_hash: "e1-model-only-fixture" }, + ground: ExistingCapabilityMigration { + predecessor: justified_addition_authority, + preserved_contract: justified_addition_authority + } + }, + realization_role: "host observation of a committed manifest", + owning_lane: "loyal-ram-883", + deletion_trigger: "the manifest read becomes a modeled filesystem operation", + current_boundary: "reads one committed file at a path the authority names" +} + +// A CAPABILITY INTRODUCED IN THIS CHANGE IS NOT PRIOR UNTIL A PROBE SAYS SO, AND AN ABSENT PROBE IS +// NOT A NEUTRAL ANSWER. This is the arm that makes the origin relation more than a field: the +// bridge is well formed, its ground is a real migration, the roster names the item, and the +// addition still refuses -- because nothing executed the test that would establish the model held +// without the Rust beside it. A probe that did not run rendering as a passed one is the +// execution-provenance loss the design document names, and here it has no spelling. +test fn a_capability_modeled_in_the_same_change_refuses_until_a_probe_runs() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition), + bridges: [retro_modeled_addition_bridge], + evidence: no_admission_evidence() + ) { + SeedGrowthAdmissionRefused { unjustified_additions: refusals, detail: _ } => + any(xs: refusals, predicate: fn(k) { string_contains(s: k, pattern: "origin test did not execute") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// ROSTER MEMBERSHIP IS NOT SUFFICIENT, and this arm is the difference between the two tests. The +// same addition the positive control admits refuses when no bridge states its capability origin: +// the SeedGrowthJustification row is identical in both runs, so what separates them is the origin +// relation and nothing else. +test fn a_roster_named_addition_still_refuses_when_no_bridge_states_its_origin() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition), + bridges: [], + evidence: no_admission_evidence() + ) { + SeedGrowthAdmissionRefused { unjustified_additions: refusals, detail: _ } => + any(xs: refusals, predicate: fn(k) { string_contains(s: k, pattern: "no AuthorizedTemporarySeedBridge") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// THE CONFORMANCE EVIDENCE. A realization mutation that the predicted evidence REFUSED, with +// fixtures authored against the model contract rather than recorded from the implementation. +data model_epoch_fixture: ModelEpochSubject = ModelEpochSubject { + base_revision: "base-rev", + head_revision: "head-rev", + epoch_ref: ModelOnlyContentIdentity { content_hash: "e1-model-only-fixture" }, + excluded_hand_item_keys: [] +} + +data conforming_evidence: AdmissionEvidence = AdmissionEvidence { + observed_base_revision: "base-rev", + observed_head_revision: "head-rev", + model_epoch_probes: [], + conformance_probes: [ + RealizationMutationRefused { + authority: justified_addition_authority, + epoch: model_epoch_fixture, + fixture_provenance: AuthoredAgainstModelContract { contract: justified_addition_authority } + } + ] +} + +// THE CIRCULAR-GREEN FIXTURE: the mutation went red, and the fixtures were recorded from the very +// implementation they check. Redness is necessary and provenance is what makes it evidence. +data realization_derived_evidence: AdmissionEvidence = AdmissionEvidence { + observed_base_revision: "base-rev", + observed_head_revision: "head-rev", + model_epoch_probes: [], + conformance_probes: [ + RealizationMutationRefused { + authority: justified_addition_authority, + epoch: model_epoch_fixture, + fixture_provenance: GeneratedByRealization { detail: "expected outputs captured from the new host function" } + } + ] +} + +// THE MUTATION NOTHING NOTICED: the model agrees with the Rust and constrains nothing. +data unconstrained_evidence: AdmissionEvidence = AdmissionEvidence { + observed_base_revision: "base-rev", + observed_head_revision: "head-rev", + model_epoch_probes: [], + conformance_probes: [ + RealizationMutationUnnoticed { + authority: justified_addition_authority, + epoch: model_epoch_fixture, + detail: "every predicted observation stayed green under a mutated realization" + } + ] +} + +// AN EXPIRED EPOCH: the probe was taken against a base the change was never proposed against, which +// is the reversed-diff class stated as evidence rather than as advice. +data expired_epoch_evidence: AdmissionEvidence = AdmissionEvidence { + observed_base_revision: "base-rev", + observed_head_revision: "head-rev", + model_epoch_probes: [ + AuthorityHoldsInModelEpoch { + authority: justified_addition_authority, + epoch: ModelEpochSubject { + base_revision: "some-other-base", + head_revision: "head-rev", + epoch_ref: ModelOnlyContentIdentity { content_hash: "e1-model-only-fixture" }, + excluded_hand_item_keys: [] + } + } + ], + conformance_probes: [ + RealizationMutationRefused { + authority: justified_addition_authority, + epoch: model_epoch_fixture, + fixture_provenance: AuthoredAgainstModelContract { contract: justified_addition_authority } + } + ] +} + +// FIXTURES RECORDED FROM THE REALIZATION REFUSE EVEN THOUGH THE MUTATION WENT RED. This is the arm +// that separates "the model constrains the Rust" from "the fixtures follow the Rust": a fixture +// pinned to an implementation breaks under any change to it, so redness alone proves nothing about +// where the expectation came from. +test fn conformance_fixtures_generated_by_the_realization_refuse_despite_a_red_mutation() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition), + bridges: [justified_addition_bridge], + evidence: realization_derived_evidence + ) { + SeedGrowthAdmissionRefused { unjustified_additions: refusals, detail: _ } => + any(xs: refusals, predicate: fn(k) { string_contains(s: k, pattern: "generated by the realization") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// A MUTATION NOTHING NOTICED IS NOT A PASS. The model agreeing with the Rust is the state this test +// exists to distinguish from the model predicting it. +test fn a_realization_mutation_no_evidence_noticed_refuses() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition), + bridges: [justified_addition_bridge], + evidence: unconstrained_evidence + ) { + SeedGrowthAdmissionRefused { unjustified_additions: refusals, detail: _ } => + any(xs: refusals, predicate: fn(k) { string_contains(s: k, pattern: "does not constrain") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + +// EVIDENCE TAKEN AGAINST A DIFFERENT BASE IS EVIDENCE ABOUT A DIFFERENT CHANGE. A probe whose epoch +// names a base the admitting observer did not observe expires, and an expired epoch is not a weaker +// form of a current one -- E1 is reconstructed rather than re-cited. +test fn a_probe_taken_against_another_base_expires_rather_than_admitting() -> Bool { + match seed_growth_admit_change( + change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), + live: live_of(head_content: head_with_justified_addition), + bridges: [retro_modeled_addition_bridge], + evidence: expired_epoch_evidence + ) { + SeedGrowthAdmissionRefused { unjustified_additions: refusals, detail: _ } => + any(xs: refusals, predicate: fn(k) { string_contains(s: k, pattern: "epoch has expired") }) + SeedGrowthAdmissionComplete { + justified_items: _, + modified_items_requiring_disposition: _, + stale_justifications: _, + duplicate_justifications: _, + uncitable_items: _ + } => false + } +} + // THE POSITIVE CONTROL. Without it the refusal above is satisfied by a join that refuses // everything, which is a wall that stops the line for every change and therefore stops nothing. test fn an_addition_the_roster_already_names_is_admitted() -> Bool { match seed_growth_admit_change( change: change_of(head_content: head_with_justified_addition, new_ranges: [LineRange { start: 6, count: 4 }]), - live: live_of(head_content: head_with_justified_addition) + live: live_of(head_content: head_with_justified_addition), + bridges: [justified_addition_bridge], + evidence: conforming_evidence ) { SeedGrowthAdmissionComplete { justified_items: justified, @@ -281,7 +534,9 @@ test fn an_addition_the_roster_already_names_is_admitted() -> Bool { test fn a_modified_item_is_reported_by_name_for_disposition() -> Bool { match seed_growth_admit_change( change: change_of(head_content: head_with_modified_existing_item, new_ranges: [LineRange { start: 4, count: 1 }]), - live: live_of(head_content: head_with_modified_existing_item) + live: live_of(head_content: head_with_modified_existing_item), + bridges: [], + evidence: no_admission_evidence() ) { SeedGrowthAdmissionComplete { justified_items: _, @@ -322,7 +577,7 @@ test fn seed_growth_deletions_offset_nothing() -> Bool { } ] ) - match seed_growth_admit_change(change: with_deletion, live: live_of(head_content: head_with_unjustified_addition)) { + match seed_growth_admit_change(change: with_deletion, live: live_of(head_content: head_with_unjustified_addition), bridges: [], evidence: no_admission_evidence()) { SeedGrowthAdmissionRefused { unjustified_additions: unjustified, detail: _ } => any(xs: unjustified, predicate: fn(k) { string_contains(s: k, pattern: "totally_unjustified_probe") }) SeedGrowthAdmissionComplete { @@ -340,6 +595,8 @@ test fn seed_growth_deletions_offset_nothing() -> Bool { // reader reads as coverage. test fn an_unobservable_change_cannot_produce_a_complete_verdict() -> Bool { match seed_growth_admit_change( + bridges: [], + evidence: no_admission_evidence(), change: RustItemChangeHostRefused { detail: "git refused" }, live: live_of(head_content: base_cli_run) ) { diff --git a/dag/tools/rust_item_census_instrument.dag b/dag/tools/rust_item_census_instrument.dag index 2b06aa28a51..9f46cb48d2f 100644 --- a/dag/tools/rust_item_census_instrument.dag +++ b/dag/tools/rust_item_census_instrument.dag @@ -16,6 +16,7 @@ import gunbc.rust_item_host_observation { } import gunbc.rust_item_identity { RustItemIdentity, rust_item_identity_key } import gunbc.rust_item_scan { RustFileScan, RustFileScanned } +import gunbc.capability_origin { no_admission_evidence } import gunbc.seed_growth_admission { SeedGrowthAdmissionComplete, SeedGrowthAdmissionRefused, @@ -185,7 +186,7 @@ fn census(report: String) -> ProcessExit { } else { let change = current_rust_item_change_observation() let live = observe_current_live_hand_rust() - let verdict = seed_growth_admit_change(change: change, live: live) + let verdict = seed_growth_admit_change(change: change, live: live, bridges: [], evidence: no_admission_evidence()) let write = Filesystem.Write(path: report, content: census_report_text(change: change, live: live, verdict: verdict)) if !write.success { exit_failure(reason: concat("census: could not write the report to ", report)) From b95c0e01076c1f93b7eb7d0ec526c352bc0e0a20 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 13:50:37 +0000 Subject: [PATCH 3/3] Bind the origin refusal population once instead of recomputing it for the length check (review 56637) Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/seed_growth_admission.dag | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index d0e92192c60..a0c0b2b11c4 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -277,6 +277,11 @@ fn seed_growth_admit_change( let unjustified = seed_growth_unjustified_additions( added: projected_declaration_refs(projections: projections) ) + let origin_refused = origin_refusals( + bridges: bridges, + evidence: evidence, + added: projected_declaration_refs(projections: projections) + ) if length(xs: unmappable) > 0 { SeedGrowthAdmissionRefused { unjustified_additions: unmappable, @@ -287,9 +292,9 @@ fn seed_growth_admit_change( unjustified_additions: unjustified, detail: "hand Rust item(s) lack SeedGrowthJustification row" } - } else if length(xs: origin_refusals(bridges: bridges, evidence: evidence, added: projected_declaration_refs(projections: projections))) > 0 { + } else if length(xs: origin_refused) > 0 { SeedGrowthAdmissionRefused { - unjustified_additions: origin_refusals(bridges: bridges, evidence: evidence, added: projected_declaration_refs(projections: projections)), + unjustified_additions: origin_refused, detail: "added hand Rust item(s) did not establish that the capability they realize was modeled independently of them" } } else {