From 67911faf8ca642194878ab7eaf0e55a7f7204347 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 21 Sep 2026 20:05:15 +0000 Subject: [PATCH 1/3] A node locus names its atom: the refused symbol reaches the text line The [native-verdict] chain rendered a resolver refusal's locus as '' while the persisted row carried the anchor atom (SubstrateInputsOnly). lens_verdict_node_occurrence_text now appends the atom identity for atom anchors; composite anchors render as before. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 677efd20a5e69f1c25e81c3c44da550117849a04) --- src/v2/std/lens_verdict.dag | 32 ++++++++++++++++--- .../native_refusal_detail_test.dag | 12 +++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/v2/std/lens_verdict.dag b/src/v2/std/lens_verdict.dag index 44ac62c3fc6..bcb9596b152 100644 --- a/src/v2/std/lens_verdict.dag +++ b/src/v2/std/lens_verdict.dag @@ -19,7 +19,9 @@ import v2.std.diagnostic { textual_whole_file_diagnostic, } import v2.std.integer { integer_int_to_decimal_string } -import v2.std.node { Node, Symbol } +import v2.std.node { + Node, Symbol, TypeNode, ComputationNode, Atom, Conj, Disj, Arrow, Cardinality, Instantiation +} import v2.std.qualified_name { qualified_name_to_dotted_string } import v2.std.text { String } @@ -94,13 +96,35 @@ fn lens_verdict_extent_text(e: Extent) -> String { // source graph that minted it; resolving it to a byte range needs that graph's SpanIndex, which // the stages past parse do not carry -- so this renders the identity the diagnostic HAS rather // than fabricating a file it does not. +// +// AN ATOM ANCHOR ALSO NAMES ITS SYMBOL. When the anchor is an atom -- a bare reference, the node a +// resolver refusal is located at -- its identity is in the node the diagnostic already carries, +// and the persisted row keeps it; rendering only the ordinal dropped the one word a reader needs +// ("which symbol was ambiguous?") on the text surface while the JSON row still had it. Composite +// anchors carry no single name and render as before. +fn lens_verdict_node_atom_suffix(node: Node) -> String { + match node.kind { + TypeNode { connective: c } => + match c { + Atom { identity: identity } => concat(" `", concat(symbol_lexeme(sym: identity), "`")) + Conj => "" + Disj => "" + Arrow => "" + Cardinality => "" + Instantiation => "" + } + ComputationNode { behavior: _ } => "" + } +} + fn lens_verdict_node_occurrence_text(node: Node) -> String { + let atom = lens_verdict_node_atom_suffix(node: node) match node.occurrence_id { - OccurrenceSynthetic => "" + OccurrenceSynthetic => concat("")) OccurrenceMinted { id: id } => - concat("")) + concat(""))) OccurrenceProjected { id: id, caused_by: _ } => - concat("")) + concat(""))) } } diff --git a/src/v2/test/claim/native_route/native_refusal_detail_test.dag b/src/v2/test/claim/native_route/native_refusal_detail_test.dag index 134a2d31215..6dd350d73b3 100644 --- a/src/v2/test/claim/native_route/native_refusal_detail_test.dag +++ b/src/v2/test/claim/native_route/native_refusal_detail_test.dag @@ -361,6 +361,18 @@ test fn operator_line_renders_the_source_occurrence_holds() -> Bool { } } +// AND THE OCCURRENCE NAMES ITS SYMBOL. The unbound refusal is anchored at the atom +// `ndp_absent_callee`; the persisted row carried that identity while the text line rendered only +// the ordinal, so a reader of the line could not tell which reference failed. A locus renderer +// that dropped the atom reds here. +test fn operator_line_names_the_refused_symbol_holds() -> Bool { + match ndp_unbound_refusal() { + Absent => false + Present { value: d } => + string_contains(s: lens_verdict_diagnostic_locus_text(d: diagnostics_fatal(d: d)), pattern: "`ndp_absent_callee`") + } +} + // THE DENOMINATOR: a module that resolves. Without it every claim above is satisfied by a route // that refuses everything for any reason. test fn clean_module_resolves_holds() -> Bool { From 59de2a1ce27002192563fb2cf435ac8ff15343f0 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 21 Sep 2026 20:25:57 +0000 Subject: [PATCH 2/3] Quote the locus atom through the JSON string quotation, not a raw backtick paste Side-chat hold on #12004: a symbol lexeme is any string, so a pasted backtick broke the quoting and a newline or ESC reached the line raw. The atom now renders through extdeps.languages.json.emit quote_json_string (RFC 8259: lossless, backslash and quote escaped, controls as visible escapes). Controls through lens_verdict_diagnostic_locus_text: ordinary identifier, backtick+backslash, newline/CR/ESC, composite anchor unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/std/lens_verdict.dag | 10 +++- .../native_refusal_detail_test.dag | 48 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/v2/std/lens_verdict.dag b/src/v2/std/lens_verdict.dag index bcb9596b152..822736ffd4d 100644 --- a/src/v2/std/lens_verdict.dag +++ b/src/v2/std/lens_verdict.dag @@ -19,6 +19,7 @@ import v2.std.diagnostic { textual_whole_file_diagnostic, } import v2.std.integer { integer_int_to_decimal_string } +import extdeps.languages.json.emit { quote_json_string } import v2.std.node { Node, Symbol, TypeNode, ComputationNode, Atom, Conj, Disj, Arrow, Cardinality, Instantiation } @@ -102,11 +103,18 @@ fn lens_verdict_extent_text(e: Extent) -> String { // and the persisted row keeps it; rendering only the ordinal dropped the one word a reader needs // ("which symbol was ambiguous?") on the text surface while the JSON row still had it. Composite // anchors carry no single name and render as before. +// +// THE SPELLING IS QUOTED BY ONE LOSSLESS AUTHORITY, NOT PASTED (side-chat hold on gunbc#12004). A +// symbol's lexeme is any string: pasted between delimiters, an embedded delimiter breaks the +// quoting and a newline or ESC goes onto the operator's line raw. `extdeps.languages.json.emit` +// `quote_json_string` is the repository's modeled RFC 8259 string quotation -- total, lossless and +// unambiguous (backslash and quote escaped, every control character a visible escape) -- so the +// atom renders as a JSON string literal and no second escaping rule is written here. fn lens_verdict_node_atom_suffix(node: Node) -> String { match node.kind { TypeNode { connective: c } => match c { - Atom { identity: identity } => concat(" `", concat(symbol_lexeme(sym: identity), "`")) + Atom { identity: identity } => concat(" ", quote_json_string(s: symbol_lexeme(sym: identity))) Conj => "" Disj => "" Arrow => "" diff --git a/src/v2/test/claim/native_route/native_refusal_detail_test.dag b/src/v2/test/claim/native_route/native_refusal_detail_test.dag index 6dd350d73b3..955a0cd1df5 100644 --- a/src/v2/test/claim/native_route/native_refusal_detail_test.dag +++ b/src/v2/test/claim/native_route/native_refusal_detail_test.dag @@ -45,12 +45,16 @@ import v2.std.diagnostic { SourcePortLocus, Textual, diagnostics_fatal, - diagnostics_fatal_reason + diagnostics_fatal_reason, + ExternalContractUnknown, + Unavailable, + node_locus } import v2.std.lens_verdict { lens_verdict_diagnostic_locus_text } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } -import v2.std.node { Symbol } +import v2.std.node { Symbol, Atom, Conj, TypeNode, node_synthetic } +import v2.std.compilers.lexing { symbol_intern_lexeme } import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.qualified_name { qualified_name_from_dotted_string } import v2.std.text { String } @@ -362,14 +366,14 @@ test fn operator_line_renders_the_source_occurrence_holds() -> Bool { } // AND THE OCCURRENCE NAMES ITS SYMBOL. The unbound refusal is anchored at the atom -// `ndp_absent_callee`; the persisted row carried that identity while the text line rendered only +// ndp_absent_callee; the persisted row carried that identity while the text line rendered only // the ordinal, so a reader of the line could not tell which reference failed. A locus renderer // that dropped the atom reds here. test fn operator_line_names_the_refused_symbol_holds() -> Bool { match ndp_unbound_refusal() { Absent => false Present { value: d } => - string_contains(s: lens_verdict_diagnostic_locus_text(d: diagnostics_fatal(d: d)), pattern: "`ndp_absent_callee`") + string_contains(s: lens_verdict_diagnostic_locus_text(d: diagnostics_fatal(d: d)), pattern: " \"ndp_absent_callee\">") } } @@ -386,3 +390,39 @@ test fn clean_module_resolves_holds() -> Bool { } } } + +// ── the atom spelling is QUOTED, losslessly, through the real locus renderer (side-chat hold on +// gunbc#12004). Each arm renders a node-anchored diagnostic whose anchor atom's lexeme carries the +// hostile characters, and asserts the EXACT text: a raw paste reds the escaping arms, and a +// renderer that dropped the name reds the ordinary one. + +fn atom_locus_text(lexeme: String) -> String { + lens_verdict_diagnostic_locus_text(d: Diagnostic { + reason: ^native_refusal_detail_quoting_probe, + at: node_locus(node: node_synthetic(kind: TypeNode { connective: Atom { identity: symbol_intern_lexeme(lexeme: lexeme) } }, children: [])), + correction: Unavailable { reason: ExternalContractUnknown } + }) +} + +test fn an_ordinary_atom_renders_readably_holds() -> Bool { + atom_locus_text(lexeme: "SubstrateInputsOnly") == "" +} + +test fn an_embedded_backtick_and_backslash_stay_unambiguous_holds() -> Bool { + atom_locus_text(lexeme: concat("left`right", concat(from_code_point(cp: 92), "x"))) + == concat(""))) +} + +test fn newline_cr_and_esc_become_visible_escapes_holds() -> Bool { + let bs = from_code_point(cp: 92) + atom_locus_text(lexeme: concat("a", concat(from_code_point(cp: 10), concat("b", concat(from_code_point(cp: 13), concat("c", concat(from_code_point(cp: 27), "d"))))))) + == concat("")))))) +} + +test fn a_composite_anchor_renders_unchanged_holds() -> Bool { + lens_verdict_diagnostic_locus_text(d: Diagnostic { + reason: ^native_refusal_detail_quoting_probe, + at: node_locus(node: node_synthetic(kind: TypeNode { connective: Conj }, children: [])), + correction: Unavailable { reason: ExternalContractUnknown } + }) == "" +} From 6b49f7005098d48b0b69b10a3094b98ee05d3bc2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Mon, 21 Sep 2026 21:02:52 +0000 Subject: [PATCH 3/3] Ask v2.std.grammar whether the anchor is an atom instead of re-destructuring it Review 69713: node_atom_identity_optional already owns the question. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/std/lens_verdict.dag | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/v2/std/lens_verdict.dag b/src/v2/std/lens_verdict.dag index 822736ffd4d..4d4432e97eb 100644 --- a/src/v2/std/lens_verdict.dag +++ b/src/v2/std/lens_verdict.dag @@ -20,9 +20,9 @@ import v2.std.diagnostic { } import v2.std.integer { integer_int_to_decimal_string } import extdeps.languages.json.emit { quote_json_string } -import v2.std.node { - Node, Symbol, TypeNode, ComputationNode, Atom, Conj, Disj, Arrow, Cardinality, Instantiation -} +import v2.std.node { Node, Symbol } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.optional { Absent, Present } import v2.std.qualified_name { qualified_name_to_dotted_string } import v2.std.text { String } @@ -102,7 +102,8 @@ fn lens_verdict_extent_text(e: Extent) -> String { // resolver refusal is located at -- its identity is in the node the diagnostic already carries, // and the persisted row keeps it; rendering only the ordinal dropped the one word a reader needs // ("which symbol was ambiguous?") on the text surface while the JSON row still had it. Composite -// anchors carry no single name and render as before. +// anchors carry no single name and render as before. Whether a node is an atom, and which, is +// `v2.std.grammar` `node_atom_identity_optional`'s question -- asked there, not re-destructured here. // // THE SPELLING IS QUOTED BY ONE LOSSLESS AUTHORITY, NOT PASTED (side-chat hold on gunbc#12004). A // symbol's lexeme is any string: pasted between delimiters, an embedded delimiter breaks the @@ -111,17 +112,9 @@ fn lens_verdict_extent_text(e: Extent) -> String { // unambiguous (backslash and quote escaped, every control character a visible escape) -- so the // atom renders as a JSON string literal and no second escaping rule is written here. fn lens_verdict_node_atom_suffix(node: Node) -> String { - match node.kind { - TypeNode { connective: c } => - match c { - Atom { identity: identity } => concat(" ", quote_json_string(s: symbol_lexeme(sym: identity))) - Conj => "" - Disj => "" - Arrow => "" - Cardinality => "" - Instantiation => "" - } - ComputationNode { behavior: _ } => "" + match node_atom_identity_optional(node: node) { + Present { value: identity } => concat(" ", quote_json_string(s: symbol_lexeme(sym: identity))) + Absent => "" } }