From f0872e1ed323ef1a20b0c852d0d80c6c8f1f9ec7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Tue, 13 Oct 2026 06:52:09 -0400 Subject: [PATCH 1/2] test(cppqualcheck): warm per-verb cache for repo-root arms; report call timeouts The repo-root arms each ran a cold --no-cache parse of the whole tree under a 30 s alarm, with stderr discarded, so a call killed on a slow sanitizer runner surfaced as a wrong count ("expected 32") rather than a timeout. - The repo-root arms share a per-run temp dir with one cache file per verb (--uses reads the rich artifact, --callers the lean one): two cold parses, four warm reads. Fixture arms keep --no-cache. The dir is removed on exit. - run() keeps the call's exit status and prints an explicit TIMEOUT (rc=142) or ERROR (rc=N) FAIL line naming the call; the gate fails on it, and a repo-root arm compares its count only when the call succeeded. - The binary flavour is detected once at the top (the section 9 nm probe, now shared); a sanitizer binary gets a 90 s per-call alarm instead of 30 s. No expected count or assertion changed. Co-Authored-By: Claude Opus 5.5 --- test/cppqualcheck.sh | 120 ++++++++++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 35 deletions(-) diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index b4f369f88..21f265f69 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -59,9 +59,42 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -d "$FIX" ] || { echo "no test/cppqualfix dir — fixture missing"; exit 2; } cd "$ROOT" -echo "cppqualcheck: BIN=$BIN CORPUS=test/cppqualfix + repo root" +# BINARY FLAVOUR, detected once (§9 reads it too). From the binary's UNDEFINED symbols: a -fsanitize=integer +# build imports __ubsan_handle_* from the runtime and an ASan build __asan_report_*; a plain one imports none. +# `strings` is the fallback for a toolchain without nm; if neither tool is present the flavour is UNKNOWN. +BINFLAV="unknown" +if command -v nm >/dev/null 2>&1; then + nm -u "$BIN" 2>/dev/null | grep -qE '__ubsan_handle|__asan_report' && BINFLAV="sanitizer" || BINFLAV="plain" +elif command -v strings >/dev/null 2>&1; then + strings -a "$BIN" 2>/dev/null | grep -q '__asan_annotation' && BINFLAV="sanitizer" || BINFLAV="plain" +fi -run(){ perl -e 'alarm 30; exec @ARGV' "$BIN" "$@" 2>/dev/null; } +# PER-CALL BUDGET. Every call runs under a perl alarm so a hang fails the gate instead of the CI job. 30 s for a +# plain binary; 90 s for a sanitizer one, which runs ~3x slower locally and slower still on a shared CI runner +# (the macOS ASan leg measured 16-30 s for ONE cold repo-root call, so a 30 s budget was a coin toss there). +ALARM=30 +[ "$BINFLAV" = sanitizer ] && ALARM=90 +echo "cppqualcheck: BIN=$BIN CORPUS=test/cppqualfix + repo root flavour=$BINFLAV alarm=${ALARM}s per call" + +# A call that is KILLED or FAILS prints nothing on stdout, and an empty output reads as a count of '' — which +# used to surface as "expected 32", a wrong-answer verdict for what was a timeout (CHECKLIST 14). So `run` +# keeps the call's exit status: any non-zero rc prints its own FAIL line naming the call — TIMEOUT for 142 +# (SIGALRM), ERROR otherwise — on fd 3 (the gate's real stdout, since `run` is nearly always inside a $( )), +# drops a marker the end-of-gate verdict reads (a subshell cannot set $fail), and returns the rc. +exec 3>&1 +RUNFAILED="$( mktemp )"; rm -f "$RUNFAILED" # a NAME only; the file exists iff some call failed +run(){ + local rc + perl -e 'alarm shift; exec @ARGV' "$ALARM" "$BIN" "$@" 2>/dev/null; rc=$? + [ "$rc" = 0 ] && return 0 + if [ "$rc" = 142 ]; then + printf ' FAIL TIMEOUT (rc=142, alarm %ss): ripwire %s\n' "$ALARM" "$*" >&3 + else + printf ' FAIL ERROR (rc=%s): ripwire %s\n' "$rc" "$*" >&3 + fi + : >"$RUNFAILED" + return "$rc" +} cnt(){ printf '%s' "$1" | grep -oE 'count="[0-9]+"' | head -1 | tr -dc 0-9; } # expect the `count="N"` of a fixture verb to equal a literal @@ -146,7 +179,7 @@ printf '%s' "$MAP" | grep -q 'n="callerCtor" amb=' \ # exclusion lives at capture time in ingest.cpp. Asserted PRE-resolution via the probe, because a cast ref # resolves to nothing and would otherwise vanish silently while still inflating every extraction count. if [ -x "$PROBE" ]; then - CASTREFS="$( perl -e 'alarm 30; exec @ARGV' "$PROBE" "$FIX" 2>/dev/null | grep -A1 'callerCasts' | grep 'calls:' )" + CASTREFS="$( perl -e 'alarm shift; exec @ARGV' "$ALARM" "$PROBE" "$FIX" 2>/dev/null | grep -A1 'callerCasts' | grep 'calls:' )" # VACUITY GUARD (L-6). "does not contain a cast keyword" is satisfied by the EMPTY STRING, so if the # probe's output format ever shifts under this grep the arm would pass while testing nothing. Assert the # POSITIVE first: callerCasts' four cast operands and its parameters are real refs that must be present. @@ -163,7 +196,7 @@ else echo " SKIP ripwire_probe not built at $PROBE — cast arm is --uses-only" fi for kw in static_cast reinterpret_cast const_cast dynamic_cast; do - RC="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$FIX" --uses="$kw" --no-cache >/dev/null 2>&1; echo $? )" + RC="$( perl -e 'alarm shift; exec @ARGV' "$ALARM" "$BIN" "$FIX" --uses="$kw" --no-cache >/dev/null 2>&1; echo $? )" [ "$RC" = 1 ] && ok "--uses=$kw refuses (no def, no use-sites) — the cast flood stayed out" \ || no "--uses=$kw expected refusal (exit 1), got exit $RC" done @@ -177,13 +210,30 @@ expect uses mutexFn 0 "most-vexing-parse: unfixable by any query widening, pi expect callers mutexFn 0 "most-vexing-parse: no caller, by construction" # ── §6 REPO-ROOT RECOVERY — the seams the defect was found on ─────────────────────────────────────────── +# ONE WARM CACHE PER VERB for the repo-root arms (§6 and §12f). Each was a separate --no-cache call, i.e. a full +# cold parse of the whole repo per arm — the cost that crossed the per-call alarm on the macOS ASan runner. None +# of these arms is ABOUT cold extraction (each pins what the resolver answers on the tree), so they share a +# per-run temp dir (CHECKLIST 12): the first call of a verb parses cold and writes its cache, the rest read it +# warm. ONE FILE PER VERB, not one in all: --uses reads the RICH artifact and --callers the lean one (doctor's +# rich_verbs=), and a single --cache= file handed to both is rewritten — a cold parse — on every switch +# (measured: 2.1 s cold, 0.5 s warm, back to 1.7 s cold on the next --callers). So two cold parses, four warm. +# The fixture arms keep --no-cache. The dir is removed on exit (the EXIT trap below and §7/§9's). +RCDIR="$( mktemp -d )"; trap 'rm -rf "$RCDIR"' EXIT +# rrun VAR --VERB=SYM… — one repo-root call on that verb's cache: stdout into VAR, returns the call's own rc +# (non-zero has already printed its TIMEOUT/ERROR line). An arm compares a count only when the call succeeded. +rrun(){ + local __out __rc __verb="${2%%=*}" + __out="$( run . "${@:2}" --cache="$RCDIR/${__verb#--}.cache" )"; __rc=$? + printf -v "$1" '%s' "$__out" + return "$__rc" +} # Literals from the W1-MEASURE census. These are the # flagship "did I break a caller?" sites: selectBaseline's second caller is a hard compile error under an # arity change and was never named; writeTally had a def and two call sites and reported ZERO callers. -US="$( run . --uses=selectBaseline --no-cache )" -{ [ "$( cnt "$US" )" = 2 ] && printf '%s' "$US" | grep -q 'mcpverbs.h'; } \ +rrun US --uses=selectBaseline \ +&& { { [ "$( cnt "$US" )" = 2 ] && printf '%s' "$US" | grep -q 'mcpverbs.h'; } \ && ok "repo: --uses=selectBaseline count=2 and names the mcpverbs.h qualified caller (was 1)" \ - || no "repo: --uses=selectBaseline expected 2 incl. mcpverbs.h, got '$( cnt "$US" )': $US" + || no "repo: --uses=selectBaseline expected 2 incl. mcpverbs.h, got '$( cnt "$US" )': $US"; } # 4 -> 5 when src/readability.h adopted docparse::detail::readWholeFile as the canonical whole-file read # (the feat/readability-lens round); 5 -> 6 when src/renamemine.h adopted the same one (feat/naming-calibration); # 6 -> 7 when src/commentcoherence.h adopted the same one (feat/comment-coherence); @@ -258,25 +308,29 @@ US="$( run . --uses=selectBaseline --no-cache )" # through the canonical helper — one new call site, same helper. # 31 -> 32 (lane/cr-qd-kinds-068): quality.h defectSitesOf reads each C++ / Python / Bash test-script file for the # defect-shape scanners through the canonical helper — one new call site, same helper. -[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 32 ] \ +rrun RW --uses=readWholeFile \ +&& { [ "$( cnt "$RW" )" = 32 ] \ && ok "repo: --uses=readWholeFile count=32 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ - || no "repo: --uses=readWholeFile expected 32" -[ "$( cnt "$( run . --callers=writeTally --no-cache )" )" = 1 ] \ + || no "repo: --uses=readWholeFile expected 32, got '$( cnt "$RW" )'"; } +rrun WC --callers=writeTally \ +&& { [ "$( cnt "$WC" )" = 1 ] \ && ok "repo: --callers=writeTally count=1 (was 0 — both template call sites are in writeDocDriftPage)" \ - || no "repo: --callers=writeTally expected 1" -[ "$( cnt "$( run . --uses=writeTally --no-cache )" )" = 2 ] \ + || no "repo: --callers=writeTally expected 1, got '$( cnt "$WC" )'"; } +rrun WU --uses=writeTally \ +&& { [ "$( cnt "$WU" )" = 2 ] \ && ok "repo: --uses=writeTally count=2 (the two explicit-template-argument call sites)" \ - || no "repo: --uses=writeTally expected 2" -[ "$( cnt "$( run . --callers=headSnapshotIngestMutex --no-cache )" )" = 0 ] \ + || no "repo: --uses=writeTally expected 2, got '$( cnt "$WU" )'"; } +rrun HM --callers=headSnapshotIngestMutex \ +&& { [ "$( cnt "$HM" )" = 0 ] \ && ok "repo: --callers=headSnapshotIngestMutex stays 0 — its 4 sites are most-vexing-parse declarations" \ - || no "repo: --callers=headSnapshotIngestMutex expected 0 (widening must NOT invent these edges)" + || no "repo: --callers=headSnapshotIngestMutex expected 0 (widening must NOT invent these edges), got '$( cnt "$HM" )'"; } # ── §7 --edit-check names a CROSS-FILE QUALIFIED caller on a contract change (plan §6) ────────────────── # The measured defect: `--edit-check=selectBaseline` on an arity change reported callers="1" and never named # the `rw::quality::selectBaseline( … )` caller in mcpverbs.h — a hard compile error under that change. # Same scenario, hermetically, in a private temp git repo (editcheckcheck.sh's mechanism). if command -v git >/dev/null 2>&1; then - WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT + WORK="$( mktemp -d )"; trap 'rm -rf "$WORK" "$RCDIR"' EXIT mkdir -p "$WORK/src" cat > "$WORK/src/a.cpp" <<'EOF' namespace app @@ -300,7 +354,7 @@ EOF ( cd "$WORK" && git init -q && git config user.email t@t && git config user.name t \ && git add -A && git commit -qm init >/dev/null 2>&1 ) - EC0="$( cd "$WORK" && perl -e 'alarm 30; exec @ARGV' "$BIN" . --edit-check=inner --no-cache 2>/dev/null )" + EC0="$( cd "$WORK" && perl -e 'alarm shift; exec @ARGV' "$ALARM" "$BIN" . --edit-check=inner --no-cache 2>/dev/null )" printf '%s' "$EC0" | grep -q 'status="unchanged"' && printf '%s' "$EC0" | grep -q 'callers="2"' \ && ok "--edit-check clean tree: callers=\"2\" — the qualified cross-file caller is counted (was 1)" \ || no "--edit-check clean tree: expected unchanged + callers=\"2\", got: $EC0" @@ -319,7 +373,7 @@ int nearUse( int a ) { return inner( a ); } } } EOF - EC1="$( cd "$WORK" && perl -e 'alarm 30; exec @ARGV' "$BIN" . --edit-check=inner --no-cache 2>/dev/null )" + EC1="$( cd "$WORK" && perl -e 'alarm shift; exec @ARGV' "$ALARM" "$BIN" . --edit-check=inner --no-cache 2>/dev/null )" ROWS="$( printf '%s' "$EC1" | grep -oE ']*/>' )" printf '%s' "$EC1" | grep -q 'status="contract-change"' && printf '%s' "$EC1" | grep -q 'params_was="1" params_now="2"' \ && ok "--edit-check arity change: contract-change, params 1 -> 2" \ @@ -417,7 +471,7 @@ fi # plain. What a plain rc=0 still buys: the frame parses, the scan terminates, and the document is # produced — everything except the sanitizer class. The abort evidence comes from CI, which runs this # gate under asan (.github/workflows/ci.yml, the G1 heavy-verb list). -TRDIR="$( mktemp -d )"; TRTXT="$( mktemp )"; trap 'rm -rf "$TRDIR" "$TRTXT" ${WORK:+"$WORK"}' EXIT +TRDIR="$( mktemp -d )"; TRTXT="$( mktemp )"; trap 'rm -rf "$TRDIR" "$TRTXT" "$RCDIR" ${WORK:+"$WORK"}' EXIT cat > "$TRDIR/edge.cpp" <<'EOF' namespace nsx { @@ -432,8 +486,8 @@ bool operator>( const S& a, const S& b ) { return a.v > b.v; } } EOF printf '#0 0x1 in nsx::ops::operator>(S const&, S const&) edge.cpp:8\n' > "$TRTXT" -TROUT="$( perl -e 'alarm 60; exec @ARGV' "$BIN" "$TRDIR" --from-trace="$TRTXT" --no-cache 2>/dev/null )" -TRC="$( perl -e 'alarm 60; exec @ARGV' "$BIN" "$TRDIR" --from-trace="$TRTXT" --no-cache >/dev/null 2>&1; echo $? )" +TROUT="$( perl -e 'alarm shift; exec @ARGV' "$(( ALARM * 2 ))" "$BIN" "$TRDIR" --from-trace="$TRTXT" --no-cache 2>/dev/null )" +TRC="$( perl -e 'alarm shift; exec @ARGV' "$(( ALARM * 2 ))" "$BIN" "$TRDIR" --from-trace="$TRTXT" --no-cache >/dev/null 2>&1; echo $? )" # (a) POSITIVE CONTROL first — without it, rc=0 proves nothing about the scan. # @@ -456,16 +510,10 @@ else no "positive control FAILED: the did not resolve by NAME, so the rc check below would be vacuous. Got: ${TRFRAME:-}" fi -# (b) FLAVOUR PROBE — disclose whether this binary can observe the class the arm is about. Detected from the -# binary's UNDEFINED symbols: a -fsanitize=integer build imports __ubsan_handle_* from the runtime, a -# plain one imports none. `strings` is the fallback for a toolchain without nm; if neither tool is -# present the flavour is reported UNKNOWN rather than guessed. -TRFLAV="unknown" -if command -v nm >/dev/null 2>&1; then - nm -u "$BIN" 2>/dev/null | grep -qE '__ubsan_handle|__asan_report' && TRFLAV="sanitizer" || TRFLAV="plain" -elif command -v strings >/dev/null 2>&1; then - strings -a "$BIN" 2>/dev/null | grep -q '__asan_annotation' && TRFLAV="sanitizer" || TRFLAV="plain" -fi +# (b) FLAVOUR PROBE — disclose whether this binary can observe the class the arm is about. BINFLAV is detected +# once at the top from the binary's UNDEFINED symbols (a -fsanitize=integer build imports __ubsan_handle_* +# from the runtime, a plain one imports none); if neither nm nor strings is present it is UNKNOWN, not guessed. +TRFLAV="$BINFLAV" case "$TRFLAV" in sanitizer) echo " NOTE §9 flavour: SANITIZER binary — this run CAN observe the integer-overflow class (rc=134 before the fix)" ;; plain) echo " NOTE §9 flavour: PLAIN binary — the wrap would execute SILENTLY here, so rc=0 below is NOT abort evidence." @@ -631,7 +679,7 @@ tmplexpect uses tqDepQualTmpl 1 "10. X::template f() resolves (was named \`te tmplexpect uses tqDeepDepTmpl 1 "11. X::Y::template f() resolves through the re-split" tmplexpect uses tqCommentDepTmpl 1 "15. X::template /* c */ f() — a comment between the keyword and the name" if [ -x "$PROBE" ]; then - TMPLREFS="$( perl -e 'alarm 30; exec @ARGV' "$PROBE" "$TMPLFIX" 2>/dev/null | grep -A1 ' tqCallDependentQualified ' | grep 'calls:' )" + TMPLREFS="$( perl -e 'alarm shift; exec @ARGV' "$ALARM" "$PROBE" "$TMPLFIX" 2>/dev/null | grep -A1 ' tqCallDependentQualified ' | grep 'calls:' )" if [ -z "$TMPLREFS" ]; then no "tmpl §12 probe: tqCallDependentQualified has no reference list — the name arm would be vacuous" else @@ -675,10 +723,10 @@ printf '%s' "$TMPLMAP" | grep -qE 'files=1 symbols=54 edges=19 shown=54 est_toke # the reader was the one spelled bare inside Reader::str. Presence guard first: the arm proves nothing # once the loader stops spelling the member form. if grep -q 'r\.pod<' src/gitoracle.h; then - POD="$( run . --callers=pod --no-cache )" - printf '%s' "$POD" | grep -qE ']*n="loadOracleCache"' \ + rrun POD --callers=pod \ + && { printf '%s' "$POD" | grep -qE ']*n="loadOracleCache"' \ && ok "repo: --callers=pod names loadOracleCache (was only Reader::str — the r.pod() sites minted nothing)" \ - || no "repo: --callers=pod does not name loadOracleCache. Rows: $( printf '%s' "$POD" | grep -oE ']*>' | tr '\n' ' ' )" + || no "repo: --callers=pod does not name loadOracleCache. Rows: $( printf '%s' "$POD" | grep -oE ']*>' | tr '\n' ' ' )"; } else no "repo: src/gitoracle.h no longer spells r.pod<…>() — re-point this arm at a live member-template call site" fi @@ -726,5 +774,7 @@ else no "xmllint not on PATH — the G4 arm cannot run; install libxml2/xmllint (a gate that cannot run must say so)" fi +# A call `run` saw fail (TIMEOUT/ERROR, already printed) fails the gate even when its arm's own check could not. +[ -e "$RUNFAILED" ] && { fail=1; rm -f "$RUNFAILED"; } [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 00c30a6f6f91baf9807a72d19b1201883eed3711 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Tue, 13 Oct 2026 06:52:09 -0400 Subject: [PATCH 2/2] docs: follow-ups to the Python import capture move (#382 review) - src/ingest_importcap.h, src/graph.h: comments named the deleted capturePythonImportBinds; they now name emitCapturedPythonImportBind. Comment-only. - test/pyimportprecisecheck.sh: the "vocabulary is compiled in" arm grepped the bare capture names, which the Go tags.scm comment already carried, so it passed on a binary without the Python captures. It now greps the Python patterns themselves. - CHANGELOG: the #382 entry states that record content and the extraction version are unchanged while the order of a file's bind records differs; drop a doubled blank line. - queries/python/tags.scm: "C++ imposes" reads as the language; name the C++ side (ingest_importcap.h). - CONTRIBUTING: family table row 1 lists Python as reading @import.path; row 2 names the Python bind producer that replaced the deleted one. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++++---- CONTRIBUTING.md | 4 ++-- queries/python/tags.scm | 5 +++-- src/graph.h | 4 ++-- src/ingest_importcap.h | 2 +- test/pyimportprecisecheck.sh | 6 +++++- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4049e8c6d..7796ead93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,10 @@ The Python walk is removed. Captured targets and bindings retain the same contai imports at 255/256 containers remain, and a real import at 257 is cut and disclosed. An import-free deep container, including a top-level import followed by a deep `pass`, no longer produces `extract-partial` and may be cached. Disclosure now reports a captured import actually cut, rather than an empty container. -No persisted record or extraction-version change is intended; the Python import gate checks -fresh/warm output and, when `RIPWIRE_BASE_BIN` is supplied, acceptance of a pristine-main cache -without rewriting it. - +There is no record content or extraction-version change; the order of a file's bind records +differs, and no reader depends on it (a cache written by the previous release gives identical +answers). The Python import gate checks fresh/warm output and, when `RIPWIRE_BASE_BIN` is supplied, +acceptance of a pristine-main cache without rewriting it. ## [0.6.7] — 2026-10-11 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 343a1ff0f..889931733 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -556,8 +556,8 @@ review will credit it, but it is never a condition for merging the language. | Family | Shared mechanism today | State | Claimable slices | |---|---|---|---| -| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS and Go read the capture; Python is in review (#382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | -| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: `capturePythonImportBinds` (`ingest_relations.h`), `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | +| 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go and Python read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | +| 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: Python's `@import.names` / `@import.alias` captures, read in `captureTagsFacts` (`ingest_sidecap.h`) and turned into binds by the Python-only `emitCapturedPythonImportBind` (`ingest_importcap.h`); `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | | 3. Types | `captureTagsFacts` (`ingest_sidecap.h`) reads `@definition.*` captures for every language. | shared, but language decisions are `Lang::` branches inside the loop: 36 non-comment `Lang::` mentions, 15 of them Elixir. | **3a** #374: one constexpr per-language hook table that `captureTagsFacts` calls once per capture, one language per commit, Elixir first. M. | | 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins have their own `captureRubyMixinBases`. | **4a** Ruby `include`/`extend`/`prepend` as captures read by the shared path, retiring `captureRubyMixinBases`: S–M, Ruby (offered on #339). **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first. | | 5. Callables | Definitions come from `tags.scm`, read by `captureTagsFacts`. | partly. Keyword-headed definitions keep keyword tables in C++: `elixirFunctionKeyword` and `elixirMetadataAttribute` (`ingest_elixir.h`) repeat `macroBodyKeyword` (`ingest_relations.h`), 0.82 and 0.96 similar by `--clones`. | **5a** #355: the tags pass evaluates `#eq?`/`#any-of?`/`#not-any-of?`, then Elixir's table moves into its `tags.scm`. M; Elixir, then Clojure. | diff --git a/queries/python/tags.scm b/queries/python/tags.scm index f6808d491..b2a7b9122 100644 --- a/queries/python/tags.scm +++ b/queries/python/tags.scm @@ -67,8 +67,9 @@ ; Shared import vocabulary (#358). The dependency target is still ONE per statement: the first ; name of a plain import (including its alias spelling), or the module_name of a from-import. -; The child anchor captures only the first plain-import name. C++ imposes the old container -; reach/depth; statement patterns remain unanchored so conditional and function imports keep coverage. +; The child anchor captures only the first plain-import name. The C++ side (ingest_importcap.h) +; imposes the old container reach/depth; statement patterns remain unanchored so conditional and +; function imports keep coverage. (import_statement . name: (_) @import.path) (import_from_statement module_name: (_) @import.path) diff --git a/src/graph.h b/src/graph.h index a191f5bd7..285e20d26 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2620,8 +2620,8 @@ inline JsImportTables buildJsImportTables( const IngestResult& ing, const WsIncl // (3) a split with one evidenced arm is kept whole, false arms included; // (4) an object handed to the caller with no mention of its class in the file (an unannotated parameter, dependency // injection, a factory return) no longer reaches the in-repo method by name; the call is declined and counted; -// (5) Python `from m import Cls as Alias` records no original name (capturePythonImportBinds keeps the module only), -// so an aliased class is no evidence until the extractor records it (a parser-version change); likewise a TS +// (5) Python `from m import Cls as Alias` records no original name (emitCapturedPythonImportBind keeps the module +// only), so an aliased class is no evidence until the extractor records it (a parser-version change); likewise a TS // namespace import used as `ns.Cls.m()` and a CJS `const X = require( … )` that the export table does not pin; // (6) languages without a table keep the ladder unchanged, each for the reason externalnames.h records: Java, // Kotlin, C#, Swift, Rust and ObjC (the extractor records no declared parameter or local type there — an ObjC diff --git a/src/ingest_importcap.h b/src/ingest_importcap.h index 0beb5185c..31a5dcdbb 100644 --- a/src/ingest_importcap.h +++ b/src/ingest_importcap.h @@ -347,7 +347,7 @@ void emitCapturedPythonImportBind( TSNode namesNode, TSNode aliasNode, std::uint // // SEVEN PARAMETERS, and that is the house shape rather than an accident. --quality-delta's `params` check // bars 5, and this is over it; the siblings it sits between take 5 (captureBases, captureFields, -// capturePythonImportBinds) and 9 (captureIncludes — the very function this round moved C-family OUT of), +// emitCapturedPythonImportBind above) and 9 (captureIncludes — the very function this round moved C-family OUT of), // with the two per-file drivers at 10. Four of the seven are the sink set (includes, refs, shortfall) plus // the file id every capture records carry. Collapsing them into a context struct would take this one call // under the bar while leaving every sibling over it, i.e. a local abstraction that makes the family harder diff --git a/test/pyimportprecisecheck.sh b/test/pyimportprecisecheck.sh index 24f4e8970..355a28c70 100755 --- a/test/pyimportprecisecheck.sh +++ b/test/pyimportprecisecheck.sh @@ -144,7 +144,11 @@ printf 'import a as aa\nfrom pkg.mod import run as r\n' >"$PC/typing.pyi" printf 'x = (import unreachable)\ndef okay():\n import a\n' >"$PC/broken.py" # This inspects the EMBEDDED vocabulary; --match alone also matches on a pre-change binary. -if grep -qa 'import\.names' "$BIN" && grep -qa 'import\.alias' "$BIN"; then +# The needles are the PYTHON PATTERNS themselves, not the bare capture names: v0.6.7's binary already +# carries `@import.names` and `@import.alias` in the Go tags.scm comment that explains their absence there, +# so a bare-name grep passed on a binary without the Python captures (review N2 on #382). Measured: both +# needles are absent from the v0.6.7 main binary (0e0f67c7) and the v0.6.5 release asset; present here. +if grep -qaF 'name: (dotted_name) @import.names' "$BIN" && grep -qaF 'alias: (identifier) @import.alias' "$BIN"; then ok '#358: Python import.names and import.alias vocabulary is compiled in' else no '#358: Python import.names/import.alias vocabulary is absent from this binary'