diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80c7fd288..7c3bfcfac 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -165,7 +165,16 @@ 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, +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. +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 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 answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call @@ -196,9 +205,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/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 51b6240f2..ff096c728 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.* ````` - + - + - - + + @@ -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)"` diff --git a/queries/elixir/tags.scm b/queries/elixir/tags.scm index 03f8c89e6..88794a9be 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 inert-region +; 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/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 " diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..4c64cc753 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" && 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 @@ -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 76fb9e660..4f424d177 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -33,6 +33,289 @@ 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 + +# Live-hole arguments execute even when the enclosing quoted syntax is a +# declaration head or metadata. Outer declaration/pattern filters must stop here. +mkdir "$TMP/head-holes" +cat > "$TMP/head-holes/heads.ex" <<'EXHEADHOLES' +defmodule HeadHole do + def bar(x), do: x + def seed(), do: :generated + defmacro generated(x) do + quote do + def unquote(bar(x))(arg), do: arg + end + end + defmacro typed(x) do + quote do + @spec unquote(bar(x)) + end + end + defmacro bare() do + quote do + def unquote(seed)(arg), do: arg + end + end + defmacro bound_name(seed) do + quote do + def unquote(seed)(arg), do: arg + end + end + defmacro disabled(x) do + quote unquote: false do + def unquote(bar(x))(arg), do: arg + end + end +end +EXHEADHOLES +"$BIN" "$TMP/head-holes" --no-cache > "$TMP/head-holes.xml" +python3 - "$TMP/head-holes.xml" <<'PYHEADHOLES' +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 calls('generated/1') == {'bar/1'}, 'quoted declaration head dropped the live-hole call' +assert calls('typed/1') == {'bar/1'}, 'quoted metadata dropped the live-hole call' +assert calls('bare/0') == {'seed/0'}, 'quoted declaration head mistook a live bare call for a pattern' +assert not calls('bound_name/1'), 'bound live-hole variable became a zero-arity call' +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 +"$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 +# 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 +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 +"$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 + +# 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 call candidates in inert 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/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' +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 + def nested_inert(x) do + quote do + quote do + unquote(bar(x)) + end + end + end + def nested_disabled(x) do + quote unquote: false do + unquote(quote do + unquote(bar(x)) + end) + 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' +assert not calls('nested_inert/1'), 'unquote in an inert nested quote became live' +assert not calls('nested_disabled/1'), 'disabled outer hole evaluated its nested quote' +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" 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