From f47581a43ee665f09b8c74994ba5ed76d7863356 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:32:42 +0800 Subject: [PATCH 01/22] test: reproduce Elixir quote live-hole and accounting gaps --- test/elixircheck.sh | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 76fb9e660..6bafcd10e 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -33,6 +33,66 @@ assert 'phantom' not in syms, 'quoted definition became an indexed symbol' assert 'nonexistent' not in calls('quoted'), 'quoted call site became a call edge' print(' PASS Elixir definitions, guards, macros, local/remote calls, pipes and negatives') PY + +# Issue #357: quoted syntax is inert, but unquote expressions are evaluated while +# constructing it. Keep this fixture separate from the golden corpus so the gate +# contrasts the call sites and the declined count directly. +mkdir "$TMP/inert" +cat > "$TMP/inert/quoted.ex" <<'EXINERT' +defmodule InertFixture do + def inert(x), do: x + def bar(x), do: x + def splice(x), do: [x] + def after_call(x), do: x + def quote(x), do: x + def comment(x), do: x + + def run(x) do + quote do + inert(x) + foo(unquote(bar(x))) + [unquote_splicing(splice(x))] + end + after_call(x) + InertFixture.quote(x) + InertFixture.comment(x) + end + + def options(x) do + quote bind_quoted: [item: bar(x)] do + inert(item) + end + end + + def nested(x) do + quote do + unquote(quote do + inert(x) + unquote(bar(x)) + end) + end + end +end +EXINERT +"$BIN" "$TMP/inert" --no-cache > "$TMP/inert.xml" +python3 - "$TMP/inert.xml" <<'PYINERT' +import re, sys, xml.etree.ElementTree as ET +data = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(data).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert 'inert/1' not in calls('run/1'), 'ordinary quoted call became an edge' +assert 'bar/1' in calls('run/1'), 'unquote(bar(x)) lost its live call edge' +assert 'splice/1' in calls('run/1'), 'unquote_splicing(splice(x)) lost its live call edge' +assert 'after_call/1' in calls('run/1'), 'call after quote was suppressed' +assert {'quote/1', 'comment/1'} <= calls('run/1'), 'ordinary named functions were treated as special forms' +assert 'bar/1' in calls('options/1'), 'bind_quoted option expression was suppressed' +assert 'inert/1' not in calls('options/1'), 'bind_quoted quote body became live' +assert 'bar/1' in calls('nested/1'), 'inner unquote in nested quote lost its live call edge' +assert 'inert/1' not in calls('nested/1'), 'nested quote body became live' +match = re.search(r'\bdeclined=(\d+)\b', data) +assert match and int(match.group(1)) >= 3, 'inert-region calls vanished from declined accounting' +print(' PASS inert quote bodies, live holes, near misses, boundaries, and declined count') +PYINERT for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From b5cacea5adcda5bf3505774e001285bfaa3b57ef Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:03:15 +0800 Subject: [PATCH 02/22] fix: preserve Elixir unquote calls and count quoted call sites --- docs/ARCHITECTURE.md | 10 ++++++---- queries/elixir/tags.scm | 4 +++- src/graph.h | 12 +++++++++++- src/graphlegend.h | 8 +++++++- src/ingest.cpp | 1 + src/ingest_cache.h | 4 +++- src/ingest_elixir.h | 33 ++++++++++++++++++++------------- src/ingest_inert.h | 32 ++++++++++++++++++++++++++++++++ src/ingest_model.h | 4 ++-- src/ingest_sidecap.h | 19 +++++++++++++++++++ src/main.cpp | 6 +++--- src/model.h | 8 +++++--- src/pincensus.h | 9 ++++----- src/quality.h | 2 +- src/serialize.h | 6 +++++- src/verbs_navigate.h | 5 +++-- test/elixircheck.sh | 25 +++++++++++++++++++++++++ 17 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 src/ingest_inert.h diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80c7fd288..189ab0fd5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -165,7 +165,11 @@ metadata do not become executable calls. Alias/import/require/use and behaviour module dependencies resolved through declared module identities, regardless of umbrella/file layout. `@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`. -**Static limits:** quoted AST and macro-generated definitions are not expanded. `use` records the +**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call +candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; +`--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and +`unquote_splicing(...)` are live, as are expressions in `bind_quoted:` options. Nested quotes +become inert again inside a live hole. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call @@ -196,9 +200,7 @@ that function — the underscore rule is for unused variables, and a bare `_seed (`src/ingest_elixir.h`); and a call that omits a defaulted argument reaches the bodyless head that evaluates the default beside the clauses, so `--path` and `--impact` see the default expression's calls from that caller, while a call that supplies the argument reaches the clauses alone -(`src/elixir_resolve.h`). One gap stays open: executable `unquote(...)` and `bind_quoted:` expressions -under `quote` are omitted with the rest of the quoted-AST filter, so a helper called only from inside -an `unquote` has no caller edge from its macro. +(`src/elixir_resolve.h`). `test/elixircheck.sh`, `test/eliximportcheck.sh` and `test/elixirsemanticcheck.sh` cover extraction, metrics, exact target selection against decoys, lexical boundaries, contracts, CLI/MCP use-site parity, diff --git a/queries/elixir/tags.scm b/queries/elixir/tags.scm index 03f8c89e6..26a90a60d 100644 --- a/queries/elixir/tags.scm +++ b/queries/elixir/tags.scm @@ -1,5 +1,7 @@ ; Elixir definitions are ordinary calls in the grammar. captureTagsFacts applies -; the keyword/head/quote filters in ingest_elixir.h; tags predicates are not evaluated. +; the keyword/head filters in ingest_elixir.h and the shared nearest-boundary inert +; walk in ingest_inert.h; tags predicates are not evaluated. Capturing every call as +; @inert/@live would still need C++ head checks and enlarge this hot capture stream. ; Anchors restrict definition names to the FIRST argument (never a call's body). (call target: (identifier) (arguments . [(alias) (dot) (atom)] @name)) @definition.module diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..a720ec728 100644 --- a/src/graph.h +++ b/src/graph.h @@ -129,6 +129,7 @@ struct Graph // stores what those calls could equally have meant ONCE PER DISTINCT candidate list (internDeclinedList): list k is declinedListCand[ off[k], off[k+1] ), // named by declinedListCallCount[k] calls — what the callers and impact answers read to count, once per call, the declines that could have meant THEIR symbols. std::vector declinedOut; + std::size_t inertDeclinedCalls = 0; // quoted call sites counted in declinedOut, with no candidate list std::vector declinedListOff{ 0u }; // the leading offset lives here, so a default Graph is already a valid zero-list CSR std::vector declinedListCand; std::vector declinedListCallCount; @@ -269,7 +270,7 @@ inline bool namespaceCompatible( RefRole role, SymKind kind ) noexcept return kind == SymKind::Macro; } case RefRole::Call: case RefRole::Read: case RefRole::Write: case RefRole::Import: - case RefRole::Value: case RefRole::Through: // never in the call loop; graph.h valueRefIndex narrows by kind itself + case RefRole::Value: case RefRole::Through: case RefRole::Inert: // Inert exits before the name ladder { return true; // un-narrowed — see the doctrine above; a NEW role is a -Werror=switch decision here } @@ -4056,6 +4057,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null disposition = CallDisposition::FileScope; // no caller node for an edge; the use-site index still lists the site continue; } + if( r.role == RefRole::Inert ) + { + // The syntax is a call candidate, but quoted AST cannot execute it. Count the declined + // site on its caller; do not intern target candidates, since it could reach none of them. + ++g.declinedOut[ r.fromSymbol ]; + ++g.inertDeclinedCalls; + disposition = CallDisposition::Declined; + continue; + } const auto it = byName.find( r.calleeName ); cand.clear(); diff --git a/src/graphlegend.h b/src/graphlegend.h index b35e988e2..e5110f774 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -598,6 +598,9 @@ inline constexpr const char* kDeclinedCallsLegend = // answer on a tree the gate never touched keeps its bytes. inline constexpr const char* kDeclinedCallsGateClause = "It also counts a call named like a builtin-type method (dict.get, list.append) whose bound definitions' classes the caller's file never names. "; +inline constexpr const char* kInertDeclinedCallsClause = + "In the callees form it also counts ordinary Elixir calls written inside quote AST outside unquote/unquote_splicing holes. " + "These sites cannot reach a target, so they contribute no caller or impact declined_calls= candidate list. "; inline const char* declinedCallsLegend( bool on ) noexcept { return on ? kDeclinedCallsLegend : ""; } inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDeclinedCallsGateClause : ""; } // --test-gate's own short form: the same attribute and unit, sized for a verb whose legend has an absolute byte budget @@ -605,7 +608,10 @@ inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDe inline constexpr const char* kDeclinedCallsTestGateLegend = "declined_calls=K (absent when 0): K call SITES the resolver declined to bind that could have reached the change or its radius (the map's declined=); a test behind one is in no row here. "; // The clause and, where the gate declined in this graph, its gate sentence — the one spelling every declined_calls= emitter uses. -inline std::string declinedCallsLegendWithGate( bool on, bool gateDeclined ) { return std::string( declinedCallsLegend( on ) ) + declinedCallsGateLegend( on && gateDeclined ); } +inline std::string declinedCallsLegendWithGate( bool on, bool gateDeclined, bool inertDeclined = false ) +{ + return std::string( declinedCallsLegend( on ) ) + declinedCallsGateLegend( on && gateDeclined ) + ( on && inertDeclined ? kInertDeclinedCallsClause : "" ); +} // ── reference-as-value round (src/valuerefs.h, src/ingest_valuerefs.h) ───────────────────────────────────────── // Each clause rides ONLY a document that carries the attribute or rows it defines (value_refs= / ), so an answer diff --git a/src/ingest.cpp b/src/ingest.cpp index 840d28e7f..ee0ef7005 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -192,6 +192,7 @@ extern "C" #include "ingest_docs.h" #include "ingest_names.h" #include "ingest_binds.h" +#include "ingest_inert.h" #include "ingest_elixir.h" #include "ingest_importcap.h" #include "ingest_valuerefs.h" diff --git a/src/ingest_cache.h b/src/ingest_cache.h index e13079f71..a5f6cebb3 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -302,7 +302,9 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 143; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change + // 145 = issue #357 lane: Elixir quote live holes and counted inert + // call candidates. 144 is main's rich-family tag; train renumbers. // 143 = 2026-10-04 (train 25 review fixes): two extraction changes — // a declaration named like the global object (`var self = this`, a // parameter `window`) is now a JsShadow binding, and a value-reference diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 028b5aefb..45aab8a65 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -156,13 +156,11 @@ bool elixirMetadataAttribute( std::string_view name ) noexcept /// references; retain executable defaults and attributes while excluding declaration heads and patterns. bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std::string_view src ) noexcept { - // Quoted syntax and module attributes (notably @spec/@type) are not runtime call sites. + // Module attributes (notably @spec/@type) are not runtime call sites. + // The shared inert-region decision runs after this capture's other syntax filters, + // so quoted definition heads and special forms never inflate declined=. for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { - if( elixirTarget( parent, src ) == "quote" ) - { - return false; - } if( std::strcmp( ts_node_type( parent ), "unary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "@" ) { if( elixirMetadataAttribute( elixirAttribute( parent, src ) ) @@ -215,8 +213,9 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: } } constexpr std::string_view special[] = { "defimpl", "defstruct", "defexception", "defoverridable", "alias", "import", "require", "use", - "quote", "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; + "unquote", "unquote_splicing", "case", "cond", "for", "if", "unless", "with", "receive", "try" }; if( elixirFunctionKeyword( target ) || elixirModuleKeyword( target ) + || ( target == "quote" && !ts_node_is_null( elixirBody( role, src ) ) ) || std::find( std::begin( special ), std::end( special ), target ) != std::end( special ) ) { return false; @@ -301,13 +300,21 @@ std::uint32_t elixirVisibilityEnd( TSNode scope, std::string_view src ) noexcept return end; } -bool elixirQuoted( TSNode node, std::string_view src ) noexcept +bool elixirInert( TSNode node, std::string_view src ) noexcept { - for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + return inInertRegion( node, [ & ]( TSNode ancestor, TSNode site ) noexcept { - if( elixirTarget( p, src ) == "quote" ) { return true; } - } - return false; + const auto target = elixirTarget( ancestor, src ); + if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) ) + { + return InertBoundary::Live; + } + if( target == "quote" && elixirContains( elixirBody( ancestor, src ), site ) ) + { + return InertBoundary::Inert; + } + return InertBoundary::None; + } ); } std::string elixirFunctionName( std::string_view name, std::uint32_t arity ) @@ -590,8 +597,8 @@ struct ElixirContext std::uint32_t size = 0; const char* name = ts_query_capture_name_for_id( query, capture.index, &size ); const std::string_view label( name, size ); - if( label == "elixir.context" && !elixirQuoted( capture.node, src ) ) { calls.push_back( capture.node ); } - if( label == "reference.bare" && !elixirQuoted( capture.node, src ) ) { identifiers.push_back( capture.node ); } + if( label == "elixir.context" && !elixirInert( capture.node, src ) ) { calls.push_back( capture.node ); } + if( label == "reference.bare" && !elixirInert( capture.node, src ) ) { identifiers.push_back( capture.node ); } } } std::sort( calls.begin(), calls.end(), []( TSNode a, TSNode b ) { return ts_node_start_byte( a ) < ts_node_start_byte( b ); } ); diff --git a/src/ingest_inert.h b/src/ingest_inert.h new file mode 100644 index 000000000..b2e8211c9 --- /dev/null +++ b/src/ingest_inert.h @@ -0,0 +1,32 @@ +#pragma once +#if !defined( RIPWIRE_INGEST_TU ) +#error "ingest_inert.h is a section of ingest.cpp; include it only there" +#endif + +namespace rw +{ +namespace +{ + +// The nearest boundary wins: a live hole re-enters an inert quote, and a nested +// quote inside that hole becomes inert again. Language syntax stays in the +// boundary callback; this walk is shared by every tags and context capture. +enum class InertBoundary : std::uint8_t { None, Inert, Live }; + +template +bool inInertRegion( TSNode node, BoundaryOf boundaryOf ) noexcept +{ + for( TSNode ancestor = ts_node_parent( node ); !ts_node_is_null( ancestor ); ancestor = ts_node_parent( ancestor ) ) + { + switch( boundaryOf( ancestor, node ) ) + { + case InertBoundary::Inert: return true; + case InertBoundary::Live: return false; + case InertBoundary::None: break; + } + } + return false; +} + +} // namespace +} // namespace rw diff --git a/src/ingest_model.h b/src/ingest_model.h index 13ad31101..daea95700 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -329,7 +329,7 @@ inline void mintModuleScopeOwners( IngestResult& result, std::vector& ra { continue; } - if( r.role != RefRole::Call && r.role != RefRole::Macro ) + if( r.role != RefRole::Call && r.role != RefRole::Macro && r.role != RefRole::Inert ) { continue; } @@ -928,7 +928,7 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, // one that does not, so one reference's spelling would depend on an unrelated fact about its file; // and --affected's import tier and graph.h's Binding tables both already read that kNoNode. const bool refTakesModuleScope = !r.isInherit && !r.isDocLink && !r.isCompose - && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); ref.fromSymbol = refTakesModuleScope ? refSweep.find( r.fileId, r.startByte ) : refSweep.findOwnedDef( r.fileId, r.startByte ); } diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 0b3ebb9a7..fa691e607 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1909,6 +1909,25 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t { continue; } + if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) + { + // Keep ordinary call candidates as declined facts, never edges. Definition heads and + // special forms have already failed elixirKeepCapture; bare names require lexical + // classification and are not counted as calls merely because they are identifiers. + if( isRef && refCapSv == "reference.call" ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( roleNode ); + r.line = nameRow + 1; + r.lang = Lang::Elixir; + r.role = RefRole::Inert; + const auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); + r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); + refs.push_back( std::move( r ) ); + } + continue; + } if( le.lang == Lang::Elixir && refCapSv == "reference.bare" ) { // Bare pipe targets have their own capture; every other bare name needs lexical variable exclusion. diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..304abc703 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1917,7 +1917,7 @@ int runDefaultMap( const MainDispatch& d ) { return {}; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); return measured.isWhole ? std::string( measured.bytes ) : std::string(); }; @@ -1930,7 +1930,7 @@ int runDefaultMap( const MainDispatch& d ) DISCLOSE( maxTokensFit, rw::MapAnnotations::MaxTokensFit::DisclosureWhy::ProbeUnmeasured, "runDefaultMap: open_memstream failed for the --max-tokens fit probe — the map is emitted unshaped and its ceiling unverified" ); return 0; } - serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( m, ing, rank, g.outOff, g.outTargets, k, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, /*outEstTokens=*/nullptr, extraPayloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); const rw::MemoryStreamBytes measured = probe.finish(); if( !measured.isWhole ) { @@ -2686,7 +2686,7 @@ int runDefaultMap( const MainDispatch& d ) } else { - serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls ); + serialize( out, ing, rank, g.outOff, g.outTargets, mapTopK, cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, cfg.stable, mapProvPtr, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, mapAutoOrder, &mapEstTokens, payloadTokens, mapAnn, /*statsFirstScreen=*/false, mapRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, g.gateDeclinedCalls, g.inertDeclinedCalls ); } } else diff --git a/src/model.h b/src/model.h index cb58e3082..fcea0d0b7 100644 --- a/src/model.h +++ b/src/model.h @@ -370,16 +370,18 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a // may-call clue); never a use site, never in the CSR. -enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through }; +// Inert — an ordinary Elixir call candidate inside quote's AST body, outside an unquote live hole. +// It remains a site for declined/report accounting, but can never bind or mint a call edge. +enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Inert }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). -inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Through ) + 1; +inline constexpr std::size_t kRefRoleCount = static_cast( RefRole::Inert ) + 1; static_assert( enumCountIsExact(), "kRefRoleCount must name the LAST RefRole enumerator — move it with the append" ); static_assert( sizeof( RefRole ) == 1, "RefRole must be a single byte (SoA-friendly, smallest int that fits)" ); // the terse `role=` attribute string for the use-site index — a declarative table indexed by the enum, in enum // order. The static_assert is the guard a switch's -Werror=switch used to be: a NEW role without a spelling is a // build error, never a silent fallback. -inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through" }; +inline constexpr const char* kRefRoleTagTable[] = { "call", "read", "write", "import", "extends", "macro", "type", "value", "through", "inert" }; static_assert( std::size( kRefRoleTagTable ) == kRefRoleCount, "kRefRoleTagTable: one spelling per RefRole, in enum order" ); inline const char* refRoleTag( RefRole r ) noexcept { diff --git a/src/pincensus.h b/src/pincensus.h index 5add98daf..5b70acf1a 100644 --- a/src/pincensus.h +++ b/src/pincensus.h @@ -115,10 +115,9 @@ enum class CallDisposition : std::uint8_t OtherRoot = 5, // multi-root only: every compatible definition lives in ANOTHER root and no include/import // reaches it — external to this root, which is what that root's solo run would say QualifiedExternal = 6, // a Rust `Scope::name()` call no in-repo member of `Scope` can answer (the H4 W3 guard) - Declined = 7, // tier 3: two or more same-language candidates, none in the caller's file or directory, none - // pinned by a qualifier or a receiver rule; or the builtin-method name gate (graph.h - // BuiltinMethodGate) admitted none of a builtin-type method name's candidates — header - // declined=, answers' declined_calls= + Declined = 7, // tier 3: ambiguous calls with no safe binding; builtin-method name gate refusals; or + // inert quoted call candidates. No edge; all count in header declined=. An inert site + // contributes to its caller's declinedOut, but names no possible target list. FileScope = 8, // a call outside every symbol with no caller node to hang an edge on. Since issue #60 // ingest_model.h mintModuleScopeOwners gives every such call a module-scope owner over // exactly isResolvableCallReference's population below, so on a code corpus this bucket @@ -147,7 +146,7 @@ static_assert( std::string_view( kCallDispositionNames[ std::size_t( CallDisposi // re-derivation, so the two cannot disagree about what a call is — only about what happened to one. inline bool isResolvableCallReference( const Reference& r ) noexcept { - return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro ); + return !r.isInherit && !r.isDocLink && !r.isCompose && ( r.role == RefRole::Call || r.role == RefRole::Macro || r.role == RefRole::Inert ); } // Per-row provenance bits — every narrowing stage that FIRED on this site, not just the deciding one. A diff --git a/src/quality.h b/src/quality.h index 5961d1b00..17d56115f 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 143; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..3f83380e9 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2290,6 +2290,8 @@ inline constexpr const char* kDeclinedMapLegend = // only to a map where the gate declined at least one call, so a map the gate never touched keeps its bytes. inline constexpr const char* kDeclinedGateMapLegend = ""; +inline constexpr const char* kInertDeclinedMapLegend = + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged @@ -2658,7 +2660,8 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::vector* declinedOut = nullptr, // of those, the calls the builtin-method name gate declined (graph.h g.gateDeclinedCalls) → the // kDeclinedGateMapLegend clause, absent when zero. - std::size_t gateDeclinedCalls = 0 ) + std::size_t gateDeclinedCalls = 0, + std::size_t inertDeclinedCalls = 0 ) { const std::size_t* changedCount = ann.changedCount; const std::string* mapAtStamp = ann.atStamp; @@ -2888,6 +2891,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::size_t declinedTotal = counterTotal( declinedOut ); // calls tier 3 declined: no edge, and no guess legend += declinedTotal > 0 ? kDeclinedMapLegend : ""; // charged to the map that carries declined= legend += gateDeclinedCalls > 0 ? kDeclinedGateMapLegend : ""; // only where the builtin-method gate declined a call + legend += inertDeclinedCalls > 0 ? kInertDeclinedMapLegend : ""; // only where an Elixir quote declined a call candidate // C1 DRIFT FIX (Round C lane B, found by re-reading this header's own output). `precise=` means "how many // out-edges a SCIP index PINNED", and the emitter's own comment below says it is "emitted ONLY under // --scip". Both were true when outProv held only {0, 1}. A4-R5 then added value 2 (an FFI binding edge) diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 62d43e5d4..bbc7384d9 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -222,7 +222,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), - rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls= + rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0, + !wantCallers && g.inertDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( chRows.declinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + rw::crossKindLegend( !chRows.crossKind.empty() ) ).c_str(), // hono-07: likewise for cross_kind= @@ -2708,7 +2709,7 @@ std::optional runAround( const MainDispatch& d ) aroundAnn.seed = { ing.symbols[ focus ].name, cfg.aroundDepth, cfg.aroundFanout, definitionCountOfName( ing, focus ), eg.fanoutCut, eg.depthTruncated }; aroundAnn.seed.unprovenDefs = arUnprovenDefs; // H1: unproven_defs= beside defs= on the root, its clause in the map legend - serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut ); + serialize( stdout, ing, rank, g.outOff, g.outTargets, int( eg.nodes.size() ), cfg.mostImportantLast, cfg.metrics, fanInPtr, &g.ambOut, false, g.outProv.empty() ? nullptr : &g.outProv, cboPtr, testedPtr, lcom4Ptr, ampPtr, &g.unresolvedOut, g.bindLabel.empty() ? nullptr : &g.bindLabel, /*autoOrder=*/false, /*outEstTokens=*/nullptr, aroundCompose.tokens + aroundRoutes.tokens + wrap.tokens, aroundAnn, /*statsFirstScreen=*/false, aroundRootArg, &g.locPinOut, g.externalCalls, &g.declinedOut, 0, g.inertDeclinedCalls ); if( !g.composeEdges.empty() ) { diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 6bafcd10e..2dc904bac 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -93,6 +93,31 @@ match = re.search(r'\bdeclined=(\d+)\b', data) assert match and int(match.group(1)) >= 3, 'inert-region calls vanished from declined accounting' print(' PASS inert quote bodies, live holes, near misses, boundaries, and declined count') PYINERT +mkdir "$TMP/inert-control" +python3 - "$TMP/inert/quoted.ex" "$TMP/inert-control/quoted.ex" <<'PYINERTCONTROL' +import pathlib, sys +original = pathlib.Path(sys.argv[1]).read_text() +lines = original.splitlines(keepends=True) +mutated = 0 +for i, line in enumerate(lines): + if line.strip() in {'inert(x)', 'inert(item)'}: + lines[i] = line[:len(line) - len(line.lstrip())] + 'x\n' + mutated += 1 + elif line.strip() == 'foo(unquote(bar(x)))': + lines[i] = line.replace('foo(unquote(bar(x)))', 'unquote(bar(x))') + mutated += 1 +assert mutated == 4, f'control did not remove all four inert call sites: {mutated}' +pathlib.Path(sys.argv[2]).write_text(''.join(lines)) +PYINERTCONTROL +"$BIN" "$TMP/inert-control" --no-cache > "$TMP/inert-control.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-control.xml" <<'PYINERTCOUNT' +import re, sys +def declined(path): + match = re.search(r'\bdeclined=(\d+)\b', open(path).read()) + return int(match.group(1)) if match else 0 +assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) +print(' PASS four inert call sites move declined accounting by four') +PYINERTCOUNT for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From d2870fa4763e7fc59539b9ff9d6b1e47ff19fb4c Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:03:35 +0800 Subject: [PATCH 03/22] test: re-pin extraction scheme tripwire for issue 357 --- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 2192e4759..1f78abd6d 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -32240f4552ee695a54f885409b67c60585d6fc7c1f3cffc362313f6c772dafcc +a4e37a094bcb61237281c6d9203daed614cfbef2e15e81b9c484c5bef4735858 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index eaf642263..bb5e04406 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-05, issue #357 lane: RE-DERIVED with UPDATE_GOLDEN=1. kParserVer 143 -> 145 for +# Elixir quote live holes and counted inert call candidates (144 is main's rich-family tag; +# train renumbers). quality.h's parser mirror moved with it. kQSnapCacheScheme stays 17: +# Snapshot meaning is unchanged and its key carries the extraction identity. # 2026-10-04, train 25 review fixes: RE-DERIVED with UPDATE_GOLDEN=1 (hash 32240f4552…2dafcc). kParserVer 141 -> 143 (two # extraction changes: a declaration named like the JS global object is a JsShadow binding; value-reference slot text # is cut on a UTF-8 boundary and JS string keys are capped) with quality.h's mirror; 143 because 141's full-use file From 0cb0a8269617a0968d7daf8866a3027cb35bc24b Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:12:27 +0800 Subject: [PATCH 04/22] test: cover Elixir quote options before enabling live holes --- test/elixircheck.sh | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 2dc904bac..cdd81ea66 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -118,6 +118,43 @@ def declined(path): assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) print(' PASS four inert call sites move declined accounting by four') PYINERTCOUNT +# quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). +mkdir "$TMP/quote-options" +cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' +defmodule QuoteOptions do + def bar(x), do: x + def splice(x), do: [x] + def bound(x) do + quote bind_quoted: [x: x] do + unquote(bar(x)) + [unquote_splicing(splice(x))] + end + end + def disabled(x) do + quote unquote: false, do: unquote(bar(x)) + end + def enabled(x) do + quote bind_quoted: [x: x], unquote: true do + unquote(bar(x)) + end + end +end +EXOPTIONS +"$BIN" "$TMP/quote-options" --no-cache > "$TMP/options.xml" +python3 - "$TMP/options.xml" <<'PYOPTIONS' +import sys, xml.etree.ElementTree as ET +syms = {s.get('n'): s for s in ET.parse(sys.argv[1]).iter('s')} +def calls(name): return {c.get('n') for c in syms[name].iter('c')} +assert not calls('bound/1'), 'bind_quoted incorrectly activates unquote/splicing' +assert not calls('disabled/1'), 'unquote: false incorrectly activates unquote' +assert calls('enabled/1') == {'bar/1'}, 'explicit unquote: true failed to restore the live hole' +print(' PASS disabled unquote, bind_quoted default, and explicit re-enable') +PYOPTIONS +for n in a b c; do "$BIN" "$TMP/inert" > "$TMP/inert-$n.xml"; done +cmp "$TMP/inert.xml" "$TMP/inert-a.xml" +cmp "$TMP/inert-a.xml" "$TMP/inert-b.xml" +cmp "$TMP/inert-b.xml" "$TMP/inert-c.xml" +echo ' PASS inert count and live edges survive cold/warm round trips' for n in a b c; do "$BIN" "$TMP/fix" > "$TMP/$n.xml"; done cmp "$TMP/map.xml" "$TMP/a.xml" cmp "$TMP/a.xml" "$TMP/b.xml" From aa0361d63e462ed6e2df3865b8014596419c1783 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:14:00 +0800 Subject: [PATCH 05/22] test: expose inert declines in default map and callees output --- test/elixircheck.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index cdd81ea66..ccba85744 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -118,6 +118,21 @@ def declined(path): assert declined(sys.argv[1]) >= declined(sys.argv[2]) + 4, (declined(sys.argv[1]), declined(sys.argv[2])) print(' PASS four inert call sites move declined accounting by four') PYINERTCOUNT +"$BIN" "$TMP/inert" --callees=run/1 --no-cache > "$TMP/inert-callees.xml" +"$BIN" "$TMP/inert" --callers=inert/1 --no-cache > "$TMP/inert-callers.xml" +python3 - "$TMP/inert.xml" "$TMP/inert-callees.xml" "$TMP/inert-callers.xml" <<'PYINERTVERBS' +import sys, xml.etree.ElementTree as ET +map_text, callees_text, callers_text = [open(p).read() for p in sys.argv[1:]] +assert 'including inert Elixir quote sites' in map_text, 'compact map misdescribes declined sites' +callees = ET.fromstring(callees_text) +assert callees.get('declined_calls') == '2', callees.attrib +assert 'including inert Elixir quote sites' in callees_text, 'compact callees misdescribes declined sites' +callers = ET.fromstring(callers_text) +assert callers.get('count') == '0', callers.attrib +assert 'declined_calls' not in callers.attrib, 'inert sites became possible callers' +print(' PASS declined map/callees accounting, compact readings, and no possible callers') +PYINERTVERBS + # quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). mkdir "$TMP/quote-options" cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' From f3f94d71a8f5a69981b11ed8ff1f43e754b63131 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:14:00 +0800 Subject: [PATCH 06/22] fix: honor quote options and explain inert declines in compact output --- docs/ARCHITECTURE.md | 6 ++++-- src/compactlegend.h | 14 +++++++++++++- src/graphlegend.h | 4 ++-- src/ingest_elixir.h | 23 ++++++++++++++++++++++- src/serialize.h | 2 +- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 189ab0fd5..afabf222b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -168,8 +168,10 @@ module dependencies resolved through declared module identities, regardless of u **Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and -`unquote_splicing(...)` are live, as are expressions in `bind_quoted:` options. Nested quotes -become inert again inside a live hole. `use` records the +`unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options. +`bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and +`unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. +Nested quotes become inert again inside a live hole. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..61e3bd156 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1788,7 +1788,19 @@ inline std::string compactLegendText( const CompactLegendSpec& spec, std::string for( const std::uint16_t i : compactPresentTerms( head, doc, spec.key ) ) { out += ' '; - out.append( compactReading( i ).reading ); + const auto& term = compactReading( i ); + if( term.attr == "declined_calls" && doc.find( "ordinary Elixir calls written inside quote AST" ) != std::string_view::npos ) + { + out += "declined_calls=K: K call sites without edges, including inert Elixir quote sites outside enabled unquote/unquote_splicing holes; inert sites have no target candidates or uses rows"; + } + else if( term.attr == "declined" && doc.find( "ordinary-Elixir-call-sites-inside-quote-AST" ) != std::string_view::npos ) + { + out += "declined=K: K call sites without edges, including inert Elixir quote sites outside enabled unquote/unquote_splicing holes"; + } + else + { + out.append( term.reading ); + } out += '.'; } // #60: the module-scope owner is the one kind that arrives under a dozen different spellings — diff --git a/src/graphlegend.h b/src/graphlegend.h index e5110f774..bc20789ae 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -599,8 +599,8 @@ inline constexpr const char* kDeclinedCallsLegend = inline constexpr const char* kDeclinedCallsGateClause = "It also counts a call named like a builtin-type method (dict.get, list.append) whose bound definitions' classes the caller's file never names. "; inline constexpr const char* kInertDeclinedCallsClause = - "In the callees form it also counts ordinary Elixir calls written inside quote AST outside unquote/unquote_splicing holes. " - "These sites cannot reach a target, so they contribute no caller or impact declined_calls= candidate list. "; + "In the callees form it also counts ordinary Elixir calls written inside quote AST outside enabled unquote/unquote_splicing holes. " + "These sites have no uses rows or target candidates, so they contribute no caller or impact declined_calls= candidate list. "; inline const char* declinedCallsLegend( bool on ) noexcept { return on ? kDeclinedCallsLegend : ""; } inline const char* declinedCallsGateLegend( bool on ) noexcept { return on ? kDeclinedCallsGateClause : ""; } // --test-gate's own short form: the same attribute and unit, sized for a verb whose legend has an absolute byte budget diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 45aab8a65..c331cd978 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -300,12 +300,33 @@ std::uint32_t elixirVisibilityEnd( TSNode scope, std::string_view src ) noexcept return end; } +// An unquote belongs to its nearest quote body. bind_quoted disables holes by +// default; an explicit literal unquote option takes precedence. Unknown options +// cannot prove a live hole, so retain the inert call candidate instead. +bool elixirLiveHole( TSNode hole, std::string_view src ) noexcept +{ + for( TSNode parent = ts_node_parent( hole ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) + { + if( elixirTarget( parent, src ) != "quote" || !elixirContains( elixirBody( parent, src ), hole ) ) + { + continue; + } + const TSNode option = elixirKeywordValue( parent, "unquote:", src ); + if( !ts_node_is_null( option ) ) + { + return nodeTextOf( option, src ) == "true"; + } + return ts_node_is_null( elixirKeywordValue( parent, "bind_quoted:", src ) ); + } + return false; +} + bool elixirInert( TSNode node, std::string_view src ) noexcept { return inInertRegion( node, [ & ]( TSNode ancestor, TSNode site ) noexcept { const auto target = elixirTarget( ancestor, src ); - if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) ) + if( ( target == "unquote" || target == "unquote_splicing" ) && elixirContains( elixirFirstArgument( ancestor ), site ) && elixirLiveHole( ancestor, src ) ) { return InertBoundary::Live; } diff --git a/src/serialize.h b/src/serialize.h index 3f83380e9..82743cf5a 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2291,7 +2291,7 @@ inline constexpr const char* kDeclinedMapLegend = inline constexpr const char* kDeclinedGateMapLegend = ""; inline constexpr const char* kInertDeclinedMapLegend = - ""; + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged From 474d9c0514028e6ee7127dc4d135dfbd80ec92c1 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:30:56 +0800 Subject: [PATCH 07/22] test: reproduce non-Elixir CDATA legend marker collision --- test/elixircheck.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index ccba85744..2ed67f34f 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -133,6 +133,28 @@ assert 'declined_calls' not in callers.attrib, 'inert sites became possible call print(' PASS declined map/callees accounting, compact readings, and no possible callers') PYINERTVERBS +# A non-Elixir body may quote our legend markers as source text. Only real +# comments outside CDATA may select the inert-decline reading. +mkdir -p "$TMP/marker/src" "$TMP/marker/a" "$TMP/marker/b" +cat > "$TMP/marker/src/run.cpp" <<'CPPMARKER' +int run() { + const char* marker = R"marker()marker"; + const char* clause = "ordinary Elixir calls written inside quote AST"; + return duplicate(); +} +CPPMARKER +printf 'int duplicate() { return 1; }\n' > "$TMP/marker/a/a.cpp" +printf 'int duplicate() { return 2; }\n' > "$TMP/marker/b/b.cpp" +"$BIN" "$TMP/marker" --pack-top-n=3 --no-cache > "$TMP/marker.xml" +python3 - "$TMP/marker.xml" <<'PYMARKER' +import sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +ET.fromstring(text) +assert 'declined=1' in text and ' "$TMP/quote-options/options.ex" <<'EXOPTIONS' From 3dca9ba47c03f79f39f11db067a47ca750b1f4c4 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:30:57 +0800 Subject: [PATCH 08/22] fix: select inert decline readings only from emitted XML comments --- src/compactlegend.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 61e3bd156..1eee1eee4 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -1789,11 +1789,11 @@ inline std::string compactLegendText( const CompactLegendSpec& spec, std::string { out += ' '; const auto& term = compactReading( i ); - if( term.attr == "declined_calls" && doc.find( "ordinary Elixir calls written inside quote AST" ) != std::string_view::npos ) + if( term.attr == "declined_calls" && compactCommentOpenedBy( doc, ""; inline constexpr const char* kInertDeclinedMapLegend = - ""; + ""; // #157: the default map's own nest-refused disclosure — before this, a refused file's absence carried no signal // on the map's own header at all, only in the skipped verb's own report (if a reader thought to ask). Charged diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 915e3ff7d..1b098c66d 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -185,8 +185,8 @@ PYINERTVERBS mkdir -p "$TMP/marker/src" "$TMP/marker/a" "$TMP/marker/b" cat > "$TMP/marker/src/run.cpp" <<'CPPMARKER' int run() { - const char* marker = R"marker()marker"; - const char* clause = "ordinary Elixir calls written inside quote AST"; + const char* marker = R"marker()marker"; + const char* clause = "ordinary Elixir call candidates in inert quote AST"; return duplicate(); } CPPMARKER From 04bdf4d4e62016450f9bd79d1ea80e1a58b01a1d Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:42:29 +0800 Subject: [PATCH 14/22] test(elixir): reproduce inert operator and capture accounting loss --- test/elixircheck.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index 1b098c66d..d7794fb0e 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -202,6 +202,33 @@ assert 'including inert Elixir quote sites' not in text, 'C++ source text change print(' PASS non-Elixir CDATA markers cannot select the inert-decline reading') PYMARKER +# Operator calls and named function captures are reference candidates too. Bare +# variable-shaped AST names are not counted merely for being identifiers. +mkdir "$TMP/inert-kinds" +cat > "$TMP/inert-kinds/count.ex" <<'EXKINDS' +defmodule InertKinds do + def bar(x), do: x + def run(x) do + quote do + bar(x) + 1 + 2 + &bar/1 + x + end + end +end +EXKINDS +"$BIN" "$TMP/inert-kinds" --no-cache > "$TMP/inert-kinds.xml" +python3 - "$TMP/inert-kinds.xml" <<'PYKINDS' +import re, sys, xml.etree.ElementTree as ET +text = open(sys.argv[1]).read() +syms = {s.get('n'): s for s in ET.fromstring(text).iter('s')} +assert not list(syms['run/1'].iter('c')), 'inert operator/capture became an edge' +match = re.search(r'\bdeclined=(\d+)\b', text) +assert match and int(match.group(1)) == 3, 'inert operator/named capture references disappeared or variable AST inflated counting' +print(' PASS inert calls, operators and named captures counted; variable AST excluded') +PYKINDS + # quote options control whether unquote is a live hole (Kernel.SpecialForms.quote/2). mkdir "$TMP/quote-options" cat > "$TMP/quote-options/options.ex" <<'EXOPTIONS' From 16284d0fa4686fcea8ee1167a38f81b960e83701 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:42:55 +0800 Subject: [PATCH 15/22] fix(ingest): count inert operator and named-capture references --- src/ingest_sidecap.h | 41 ++++++++++++++++++++++------------------- src/model.h | 2 +- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index fa691e607..8d3d09448 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1909,25 +1909,6 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t { continue; } - if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) - { - // Keep ordinary call candidates as declined facts, never edges. Definition heads and - // special forms have already failed elixirKeepCapture; bare names require lexical - // classification and are not counted as calls merely because they are identifiers. - if( isRef && refCapSv == "reference.call" ) - { - RawRef r; - r.fileId = fileId; - r.startByte = ts_node_start_byte( roleNode ); - r.line = nameRow + 1; - r.lang = Lang::Elixir; - r.role = RefRole::Inert; - const auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); - r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); - refs.push_back( std::move( r ) ); - } - continue; - } if( le.lang == Lang::Elixir && refCapSv == "reference.bare" ) { // Bare pipe targets have their own capture; every other bare name needs lexical variable exclusion. @@ -1943,6 +1924,28 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t if( nameTxt == "/" && elixirNodeIs( parent, "unary_operator" ) && nodeFieldText( parent, NodeField::Operator, src ) == "&" ) { continue; } } + if( le.lang == Lang::Elixir && elixirInert( roleNode, src ) ) + { + // Keep call/operator/named-capture candidates as declined facts, never edges. Heads, + // special forms and non-call operators already failed their syntax filters. Bare AST + // names remain variable-shaped syntax unless they explicitly name a function capture. + if( isRef && ( refCapSv == "reference.call" || refCapSv == "reference.operator" + || ( refCapSv == "reference.bare" && elixir.namedCapture( nameNode ) ) ) ) + { + RawRef r; + r.fileId = fileId; + r.startByte = ts_node_start_byte( roleNode ); + r.line = nameRow + 1; + r.lang = Lang::Elixir; + r.role = RefRole::Inert; + auto [ count, known ] = elixirCallArity( roleNode, nameNode, src ); + if( refCapSv == "reference.operator" ) { count = elixirNodeIs( roleNode, "binary_operator" ) ? 2 : 1; known = true; } + r.name = known ? elixirFunctionName( nameTxt, count ) : std::string( nameTxt ); + refs.push_back( std::move( r ) ); + } + continue; + } + if( isDef ) { if( le.lang == Lang::Elixir && defCapSv == "definition.attribute" ) { kind = SymKind::Var; } diff --git a/src/model.h b/src/model.h index c173ad138..8c2084fe9 100644 --- a/src/model.h +++ b/src/model.h @@ -370,7 +370,7 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a // may-call clue); never a use site, never in the CSR. -// Inert — an ordinary Elixir call candidate whose enclosing quote AST remains inert after live-hole re-entry. +// Inert — an Elixir call/operator/named-capture candidate whose enclosing quote AST remains inert after live-hole re-entry. // It remains a site for declined/report accounting, but can never bind or mint a call edge. enum class RefRole : std::uint8_t { Call, Read, Write, Import, Extends, Macro, Type, Value, Through, Inert }; // The number of RefRole enumerators — the bound readRef validates a cached role byte against (see kSymKindCount). From fff9fa7f170060ff414a1943de10ef4178e6ccb7 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:43:25 +0800 Subject: [PATCH 16/22] docs: define inert reference counting coverage --- docs/ARCHITECTURE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 73bad2baf..05ea7fe74 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -165,9 +165,10 @@ metadata do not become executable calls. Alias/import/require/use and behaviour module dependencies resolved through declared module identities, regardless of umbrella/file layout. `@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`. -**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call -candidates inside a `quote` body mint no edges and are counted in the map's `declined=` gauge; -`--callees` counts those sites on their enclosing function. Calls inside `unquote(...)` and +**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call, +operator and named-function-capture candidates inside a `quote` body mint no edges and are counted +in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function. +Bare variable-shaped AST identifiers are not counted merely for being identifiers. Calls inside `unquote(...)` and `unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options. `bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and `unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. From 4bfb96d75011fdaa09a92cc8091763a34eeda562 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:47:36 +0800 Subject: [PATCH 17/22] test(elixir): reproduce quoted module stealing live-hole scope --- test/elixircheck.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index d7794fb0e..c1bb108be 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -81,6 +81,33 @@ assert not calls('disabled/1'), 'disabled declaration-head hole became live' print(' PASS live holes in quoted declaration heads/metadata, bare calls and variable negatives') PYHEADHOLES +# A module declaration which is itself quoted must not change the lexical +# module of an evaluated hole. A same-file namesake makes wrong-scope fallback visible. +mkdir "$TMP/module-hole" +cat > "$TMP/module-hole/modules.ex" <<'EXMODULEHOLE' +defmodule Real do + def bar(x), do: x + def run(x) do + quote do + defmodule Phantom do + unquote(bar(x)) + end + end + end +end +defmodule Other do + def bar(x), do: x +end +EXMODULEHOLE +"$BIN" "$TMP/module-hole" --no-cache > "$TMP/module-hole.xml" +python3 - "$TMP/module-hole.xml" <<'PYMODULEHOLE' +import sys, xml.etree.ElementTree as ET +syms = {(s.get('n'), s.get('sc', '')): s for s in ET.parse(sys.argv[1]).iter('s')} +assert 'bar/1' in {c.get('n') for c in syms['run/1', 'Real'].iter('c')}, 'inert module ancestor stole the live-hole lexical scope' +assert not any(n == 'Phantom' for n, sc in syms), 'inert module became a definition' +print(' PASS inert module declarations do not shadow live-hole lexical scope') +PYMODULEHOLE + # Issue #357: quoted syntax is inert, but unquote expressions are evaluated while # constructing it. Keep this fixture separate from the golden corpus so the gate # contrasts the call sites and the declined count directly. From 75a06708cbd516177b99d4ead7aafc456b0077dc Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:47:37 +0800 Subject: [PATCH 18/22] fix(elixir): skip inert declarations when finding live-hole module scope --- docs/ARCHITECTURE.md | 2 +- src/ingest_elixir.h | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 05ea7fe74..7c3bfcfac 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -173,7 +173,7 @@ Bare variable-shaped AST identifiers are not counted merely for being identifier `bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and `unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole. Nested quotes become inert again inside a live hole; an inner quote that is itself inert -cannot execute its own holes in the current stage. Quoted declaration heads and metadata +cannot execute its own holes in the current stage. Quoted module declarations do not change the lexical module of evaluated holes. Quoted declaration heads and metadata do not suppress evaluated hole arguments; bound variables in those arguments remain variables. `use` records the dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions therefore need an explicit source definition to appear. A call that only an injected import could diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 1ec9cef37..4ab76702e 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -378,8 +378,10 @@ struct ElixirContext } for( TSNode p = ts_node_parent( node ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) { - if( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + if( ( elixirModuleKeyword( elixirTarget( p, src ) ) || elixirTarget( p, src ) == "defimpl" ) + && !elixirInert( p, src ) ) { + // Quoted module declarations are AST, not the lexical module of an evaluated hole. return moduleName( p, depth + 1 ); } } From 7e834936881134f50d9d9bab743e0630142fb80a Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:48:07 +0800 Subject: [PATCH 19/22] test(elixir): pin live-hole callee identity across quoted modules --- test/elixircheck.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/elixircheck.sh b/test/elixircheck.sh index c1bb108be..4f424d177 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -107,6 +107,13 @@ assert 'bar/1' in {c.get('n') for c in syms['run/1', 'Real'].iter('c')}, 'inert assert not any(n == 'Phantom' for n, sc in syms), 'inert module became a definition' print(' PASS inert module declarations do not shadow live-hole lexical scope') PYMODULEHOLE +"$BIN" "$TMP/module-hole" --callees=run/1 --no-cache > "$TMP/module-hole-callees.xml" +python3 - "$TMP/module-hole-callees.xml" <<'PYMODULETARGET' +import sys, xml.etree.ElementTree as ET +rows = {(s.get('n'), s.get('p')) for s in ET.parse(sys.argv[1]).iter('s')} +assert rows == {('bar/1', 'modules.ex:2')}, f'live hole bound to the wrong module: {rows}' +print(' PASS live hole reaches Real.bar/1, not the same-file Other.bar/1') +PYMODULETARGET # Issue #357: quoted syntax is inert, but unquote expressions are evaluated while # constructing it. Keep this fixture separate from the golden corpus so the gate From 0167bdd85327a2332305478a321f8909b6e85a1e Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:04:52 +0800 Subject: [PATCH 20/22] chore: account for inert-region ingest section in optimization triage --- scripts/optremarks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/optremarks.py b/scripts/optremarks.py index 316ebac38..3785977e9 100755 --- a/scripts/optremarks.py +++ b/scripts/optremarks.py @@ -112,6 +112,8 @@ "per-node tree-sitter walk — is covered by ingest_binds.h and ingest_relations.h, which run for every grammar." ), ( "src/ingest_elixir.h", "Elixir-specific capture helpers: per node, but for one grammar with a small corpus share. Same argument as ingest_jsimports.h above." ), + ( "src/ingest_inert.h", + "The shared inert-boundary walk currently runs only for Elixir quote captures and context. Like ingest_elixir.h, its remarks cover one grammar, not the general corpus." ), # ── per-DIRECTIVE, where every hot ingest section above is per-node or per-file ──────────────────── ( "src/ingest_importcap.h", "the shared @import.path capture vocabulary (issue #358), carrying the C family for now. It is called from ONE place in ingest_sidecap.h and runs once per captured " From bc53df27f755d3934a7750bdc5ed770472180667 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:07:30 +0800 Subject: [PATCH 21/22] docs: regenerate shifted graph seed showcase blocks --- docs/captures/COMMANDS_showcase_2026-09-14.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..29f1a2f9b 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3921,29 +3921,29 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:5565` +## `./build/ripwire . --at=src/graph.h:5577 --legend=full` *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* ````` - - + + ````` -## `./build/ripwire . --callers=@src/graph.h:5565` +## `./build/ripwire . --callers=@src/graph.h:5577 --legend=full` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - + - - + + From b479b4f83d806f5aa65c37c3d62f846361b40335 Mon Sep 17 00:00:00 2001 From: LaimaWu <317139567+LaimaWu@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:14:44 +0800 Subject: [PATCH 22/22] docs: synchronize navigation captures and generated command reference --- docs/COMMANDS.md | 6 +++--- docs/captures/COMMANDS_showcase_2026-09-14.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..0e298cca2 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -3350,11 +3350,11 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:5565 +$ ./build/ripwire . --at=src/graph.h:5577 --legend=full - - + + ``` diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 29f1a2f9b..ff096c728 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3951,7 +3951,7 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 ````` -## `./build/ripwire . --at=src/graph.h:999999` +## `./build/ripwire . --at=src/graph.h:999999 --legend=full` *A seed past the end of the file — the refusal shape for a faulted location.* @@ -3964,7 +3964,7 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 stderr: ````` -ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 6763 lines — the seed asked for line 999999) +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 9428 lines — the seed asked for line 999999) ````` ## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"`