diff --git a/CHANGELOG.md b/CHANGELOG.md index 837945ff5..e4ed3f07e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,55 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — `--dead-code` no longer claims a confidence its evidence cannot support + +Every `--dead-code` root carried `confidence="high"`, hardcoded: nothing in the candidate loop — internal +linkage plus zero indexed callers — varied it, so the attribute was a constant wearing the shape of a +finding. The claim is also the one most likely to be acted on destructively, since the row an agent deletes +on a false "high" is live code reached by a virtual call, a reflection hook or a macro-generated caller, +none of which a name-based graph can see. The attribute is **removed** rather than replaced by a derived +number: there is no per-finding signal to derive one from, and `evidence="internal-linkage+zero-callers"` +already states the one thing the code actually knows. The full and compact legends, `--help` for +`--dead-code` and for `--safe-delete`'s `dead_code_candidate=`, and four skills that asserted the +confidence in prose all say the same thing now. `test/deadprecisioncheck.sh` asserts the attribute is +absent — the arm that used to pin the bug. + +### Fixed — `reuse-decline` fired on clone groups `duplication` already exempts + +`reportReusedClones` (kind `new-clone-of-reused-helper`) reads the same clone vectors as its sibling +`reportNewClones` (kind `duplication`) but carried neither of that sibling's two demotions: the +all-test-script skip, which exempts a group of sibling gate scripts repeating the house harness +boilerplate by convention, and the idiom→minor demotion. So a shell gate script cloning the house test +helper was exempt as `duplication` and gating as `reuse-decline`, from the same input — and that shape is +where the kind's one real-history firing came from. Both demotions are now applied identically in both +reporters. + +### Fixed — a Java or Kotlin `import` is a dependency edge, not a call + +`queries/java/tags.scm` and `queries/kotlin/tags.scm` captured an `import_declaration`/`import_header` as +`@reference.call`. That was inert while nothing owned a reference written outside every named definition; +once a top-level statement gained a `` owner (#60), an import line minted a real caller edge +and inflated `--callers=`/`--impact=` fan-in by one phantom caller per importing file. Both queries now +capture `@reference.import`, which routes to the existing `RefRole::Import` the C++ `using ns::name;` form +already used: the import stays fully visible on `--uses` as a `role="import"` use-site, and is excluded from +the call-graph CSR. No new C++ — the two query files are the whole change. `kParserVer` moves 118 → 119, so +any cache written by an earlier binary is refused and reparsed. Every other indexed language was checked: +Java and Kotlin were the only two that captured an import-shaped construct as a call. + +A .kt file has no executable top level, so with this the Kotlin fixture correctly carries **no** module-scope +owner at all; `test/kotlincheck.sh` §1a pins that whole chain — no owner minted, the import still visible +with `role="import"`, and `square`'s fan-in counting real functions only. + +### Fixed — unseeded `--slice-flow=both` now says that it adds nothing the flat rows do not + +Unioned over a function's whole variable inventory, `--slice-flow=both`'s reach is byte-identical to the +union of the flat rows, which holds by construction: every flow row at depth ≥ 1 lands on an occurrence of +some other sliceable local, and that is already a row of *that* local's flat slice. Seeded with `--at=`, the +same analysis adds real reach. The unseeded case is not refused — the answer it returns is correct, just no +more informative — it is **disclosed**: the `` root carries `flow_redundant="1"` exactly when +`--slice-flow=both` runs with no `--at=` seed, never on `back`/`fwd` alone and never on a seeded run, with +one line in the legend pointing at the seed. + ### Fixed — a call written outside every named function now has a caller, so `callers`, `impact`, `affected` and `test-gate` stop answering one short (#60) A reference was attributed to the innermost definition whose span contains it, so a call written where no @@ -301,6 +350,68 @@ the arm compares normally. When an arm is still refused, the root carries `reaso debug trace. Both `ok=` postures and `reason=` are defined in the legend. Gates: `test/scoutheadconflictcheck.sh` arms T9(a)–(e), `test/mergescoutcheck.sh`. (CodeRabbit review on #295) +### Fixed — sixteen gates now share one GNU/BSD `stat` compat helper instead of a per-gate copy + +`stat -f` is GNU coreutils' filesystem-stat flag, not BSD's format-string flag, so it succeeds with junk +instead of failing — a caller-local `stat -f ... || stat -c ...` one-liner never reaches its own fallback on +Linux. Sixteen gates each hand-rolled the same detect-once-and-redefine fix independently: +`cachehashcheck.sh`, `cachesplitcheck.sh`, `clonecachecheck.sh`, `codexpromptroutecheck.sh`, +`evictioncheck.sh`, `g1freshcheck.sh`, `headsnapcachecheck.sh`, `mcpeditmodecheck.sh`, +`portablecachecheck.sh`, `prcontextcheck.sh`, `qsnapcachecheck.sh`, `qsnapprefetchcheck.sh`, +`statgatecheck.sh`, `cacheisolationcheck.sh`, `qsnapproducercheck.sh`, `sidecarsymlinkcheck.sh` and +`tempfilesymlinkcheck.sh` all now source the new shared `test/lib/statcompat.sh` instead — one place defines +the GNU-vs-BSD `stat` compat logic, not seventeen. Centralised by **@s0undt3ch** in #298. + +### Fixed — a gate that builds a throwaway git repository could be aimed at the caller's repository instead + +`git -C DIR` changes the working directory; it does not override the environment, and `GIT_DIR`, +`GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`, +`GIT_ALTERNATE_OBJECT_DIRECTORIES` and `GIT_PREFIX` all outrank it. A gate that builds a fixture repo and +asks it a question therefore answered from somebody else's repository whenever one of those was exported — +by a git hook running the suite, a CI job, or a `git rebase` running the suite per commit — and then +passed or failed on data it never +selected. Measured at the reported call shape: with `GIT_DIR` set, `git -C "$REPO" init` created no `.git` +under `$REPO` at all, the gate's own commit landed **in the ambient repository**, and `git -C "$REPO" +rev-parse HEAD` read that foreign sha back, with the gate still reporting ALL PASS. The clearing joins the +agent-home names in the shared `test/lib/clean-env.sh` rather than becoming a 156th call-site copy: **155 +gates** build a repo and were exposed, and the two that had already found this independently +(`dispatchordercheck.sh`, `pagingsweepcheck.sh`) had hand-rolled partial lists that each missed names the +other had. New gate `test/gitenvhermeticcheck.sh` proves the defect is live on this git, proves the helper +fixes it, pins the variable list against the helper, and sweeps the tree for a repo-building gate that does +not source it — with a control that strips the source line from a real gate and requires the sweep to flag +the copy. (CodeRabbit review on the train-13 branch) + +### Fixed — a gate that varies `HOME=` per invocation could still leak into an ambiently-set agent-home variable + +`CODEX_HOME`/`AGENTS_HOME`/`HERMES_HOME`/`CLAUDE_CONFIG_DIR`/`RIPWIRE_DATA_HOME` override the default an +agent's tools derive from `HOME`, so a gate that only sets `HOME=` per invocation is not actually sandboxed +on a machine where any of these is already exported ambiently. `codexpromptroutecheck.sh`, +`claudeconfigdircheck.sh`, `skillinstallcheck.sh` and `hermesinstallcheck.sh` now source the new shared +`test/lib/clean-env.sh` before varying `HOME=`, closing that leak in each. `claudeconfigdircheck.sh` — the +gate that exists specifically to test `CLAUDE_CONFIG_DIR` relocation — was the one this hit hardest: with +`CLAUDE_CONFIG_DIR` exported ambiently (a developer whose real Claude Code config is relocated, exactly the +case this gate tests for), its "unset" baseline arm wrote real files into that directory and then failed +comparing against its own contaminated baseline. Found and closed by **@s0undt3ch** in #298; carrying the +same shape through the rest of the suite closed six more — `hookcheck.sh`, `routehookcheck.sh`, +`agenttablecheck.sh`, `codexinstallhonestycheck.sh`, `meterdisclosurecheck.sh` and `releaseinstallcheck.sh`. + +### Added — the advertised MCP verb roster is pinned by deriving it, so a count-preserving rename cannot slip through + +`test/mcpverbscheck.sh` checked that `tools/list` advertises the expected *number* of verbs, which a rename +that swaps one name for another passes unchanged — and a renamed verb is a silently broken contract for every +agent that had wired up the old name. The gate now derives the roster from a live `tools/list` call and +compares the **names**, all 33 of them. Proved against the mutation it exists for: planting an added verb, a +removed verb and a count-preserving rename in the served stanza all redden the new arm, and the rename case +leaves the old count-only check green — which is the gap. Contributed by **@pt-act** in #291. + +### Fixed — two documentation claims that described behaviour the binary does not have + +`--expand=SYM` on an ambiguous name was documented without the scoping the disclosed `topk_default="0"` +actually applies, and the directory-of-repos row described a guard that does not exist, where the real +behaviour is a silent merge. Both corrected in `README.md` and in the `ripwire-navigate` and `ripwire-orient` +skills, against a built binary rather than from reading the source. Contributed by **@llvm-x86** in #302, +who built the Flask fixture that found them. + ### Fixed — an ambiguous `--expand` buried its body behind the ranked map, and the escape hatch was stderr-only Reported by @mariadb-KyleHutchinson in #289: `--expand=SYM` on a name matching more than one definition, in a diff --git a/README.md b/README.md index fdfa6b211..cdd766c03 100644 --- a/README.md +++ b/README.md @@ -682,7 +682,16 @@ to do with it.** Two shapes get the most out of it, and one gets nothing.
The three controls that decide what it costs — a budget (--token-budget / --top-k), routing (--help-task), and the skills that teach an agent when not to reach for it -1. **A budget.** `--token-budget=N` caps the bundle; `--top-k=N` caps the rows. Unbudgeted `--for` returns +1. **A budget.** `--token-budget=N` caps the bundle; `--top-k=N` caps the rows **of the default map, + `--query`, `--format=candidates`, `--recall` and `--graph-query` — it does not shape plain `--for`**. + On `--for`, a *positive, explicit* `--top-k` is read by nothing: the run prints a stderr note + (`--top-k is not read by --for`) and still emits the full bundle. Two neighbours of that shape are + different and neither warns: `--for --format=candidates --top-k=N` *does* consume the flag (the + candidate export composes with it and caps the rows), and `--for --top-k=0` is refused outright by the + payload-only guard (`--top-k=0` needs a payload verb), not warned-and-emitted. Narrow plain `--for` + with its own arguments instead — `--signatures-only` + (drop the auto-bodies), `--token-budget=N` (shapes the bundle to fit), `--detail=N` (full bodies for + just the top N). Unbudgeted `--for` returns a rich terminal bundle by design — right when it ends the question, wasteful when it does not. 2. **Routing.** `ripwire . --help-task=""` names the ONE command the task actually wants, and abstains when the evidence is thin. It is advice, it never runs anything. An answer of "just grep @@ -692,6 +701,27 @@ to do with it.** Two shapes get the most out of it, and one gets nothing. time — including the times it should not.
+
+The invocations first-time users get wrong — WRONG → RIGHT, each row a real failure reported from the field + +| WRONG | RIGHT | why | +|---|---|---| +| `ripwire . --for="…" --top-k=5` | `ripwire . --for="…" --signatures-only` (or `--token-budget=N`, `--detail=N`) | `--top-k` is inert on `--for`: the run warns on stderr and emits the full bundle anyway, so the agent *believes* it narrowed the output and did not. | +| `ripwire . --query="…"` as the default lens | `ripwire . --for="…"` | `--query` is the raw BM25 ranking — the binary's own help calls it debug and says "use --for". It is the right tool for hunting a vocabulary, the wrong default for a task. | +| `ripwire . --expand=SYM` where SYM is an **ambiguous** bare name | `ripwire . --expand=SYM --top-k=0` (or name it exactly: `--expand=FILE:NAME`) | A multi-match name keeps the ranked map — there IS something to disambiguate — so ~9K est_tokens of map ride along with the bodies. An **unambiguous** single match already defaults to `--top-k=0` on its own (disclosed as `topk_default="0"`); no flag needed there. | +| `--callers=` expecting routes | find the URL in the project's own docs (e.g. a feature map), then `--expand` the handler | Framework route handlers have no callers in the graph — the decorator reaches them, not project code. Empty `--callers` on a handler is the design, not a bug. | +| `ripwire …` (ONE root that happens to contain checkouts) | `cd` into ONE checkout first | Nothing refuses this: the crawl silently walks the nested repos and merges them into one corpus, so you pay for a map of everything and rank across unrelated codebases. Distinct from the real multi-root feature, which is N **explicit** positional roots (`ripwire dir1 dir2 … `, 2–16 checkouts merged on purpose). | + +**What ripwire does not replace** — reach for `grep`/`read` here even when a verb looks close: + +| still use grep/read for | why | +|---|---| +| route → handler lookup from a URL | ripwire ranks symbols, not URLs; it does not know your routes | +| templates, i18n catalogs, SQL migrations | not call-graph territory | +| one exact string in one file you can already name | `--grep=TERM` works, but `rg` is fine too — and the map is a fixed cost you did not need | +| UX flow through frontend event handlers | the JS is in the graph, but the flow needs line context, not a ranking | +
+
What is measured and what is not — these are single-agent figures; that the saving grows with the number of cold orientations is pre-registered and unrun, not a published result @@ -1818,9 +1848,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-645 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +646 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **645 gate scripts** and is the authoritative list; +`test/regression.sh` names **646 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2027,7 +2057,7 @@ a runtime call with no syntax to read, so a Lua corpus reports no inheritance ed implied), **Dart** (`.dart` — classes, mixins, extensions, enums, typedefs, functions, methods, getters/setters; `recv.m()`, `recv?.m()` and cascade `..m()` invocations are edges. Two stated floors: named constructors and factories index under the CLASS name, so `C()`, `C.seeded()` and `factory C.fromA()` are overloads of `C`; and `noSuchMethod` dynamic dispatch names its callee at run time. The grammar makes a function body a SIBLING of its signature rather than a child, so the definition span is extended through it at capture time — without that, every call in a body attributes to the enclosing class), **Elixir** (`.ex`/`.exs` — nested modules, structs, protocols and implementations, functions, macros, guards, delegates, types, callbacks, attributes and literal ExUnit tests; module/name/arity resolution with lexical aliases, filtered imports, default arguments, captures and pipes; see the -[static-analysis limits](docs/ARCHITECTURE.md#elixir-extraction)), **Kotlin** (`.kt` — classes, objects, companion objects, interfaces, enum classes and functions, extension functions included; bare and navigation calls, constructor delegation and imports are edges. Kotlin and Java share one call graph, and a call reaches the other language only when its own defines no candidate of that name, so adding `.kt` files never moves a Java edge. Stated floors: an explicit receiver (`A.f()`) does not narrow candidates; a multiplatform `expect`/`actual` type pair is two candidates; `.kts` is not indexed; and a file nesting string templates past 128 levels is refused and listed by `--skipped` — see the [Kotlin limits](docs/ARCHITECTURE.md#kotlin-extraction)), **GDScript** (`.gd` — a Godot file is a class body: `class_name` names it and its file-scope `func`/`var` are its members, with inner classes, constants, enums and their members, signals and call edges; `preload`/`load` produce no dependency edge yet, and `.tscn`/`.tres`/`.gdshader` are not indexed — see the [GDScript notes](docs/ARCHITECTURE.md#gdscript-extraction)), Bash, Go, Rust, Swift, C#, JSON + TOML + YAML (config keys — a +[static-analysis limits](docs/ARCHITECTURE.md#elixir-extraction)), **Kotlin** (`.kt` — classes, objects, companion objects, interfaces, enum classes and functions, extension functions included; bare and navigation calls and constructor delegation are call-graph edges; an import is a dependency edge only (role="import" on `--uses`, never a `--callers`/`--impact` edge — T13/fix3, 2026-09-20). Kotlin and Java share one call graph, and a call reaches the other language only when its own defines no candidate of that name, so adding `.kt` files never moves a Java edge. Stated floors: an explicit receiver (`A.f()`) does not narrow candidates; a multiplatform `expect`/`actual` type pair is two candidates; `.kts` is not indexed; and a file nesting string templates past 128 levels is refused and listed by `--skipped` — see the [Kotlin limits](docs/ARCHITECTURE.md#kotlin-extraction)), **GDScript** (`.gd` — a Godot file is a class body: `class_name` names it and its file-scope `func`/`var` are its members, with inner classes, constants, enums and their members, signals and call edges; `preload`/`load` produce no dependency edge yet, and `.tscn`/`.tres`/`.gdshader` are not indexed — see the [GDScript notes](docs/ARCHITECTURE.md#gdscript-extraction)), Bash, Go, Rust, Swift, C#, JSON + TOML + YAML (config keys — a `[tool.ruff.lint]` table is one symbol under its full dotted name, and `pyproject.toml`/`Cargo.toml`/CI workflows become greppable), and **Markdown** (`.md`/`.markdown` — the DOC tier: every heading, ATX or setext, is a section symbol whose span runs to the next diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 312691a10..45546e862 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -2765,7 +2765,7 @@ $ ./build/ripwire . --owners ### `--dead-code[=DIR]` -**Answers:** list internal functions with no caller anywhere in the indexed tree high-confidence internal source functions with no caller in the indexed tree; +**Answers:** list internal functions with no caller anywhere in the indexed tree internal source functions with no caller found in the indexed tree — a name-based graph reading, not a confidence score (dynamic dispatch, reflection and macro-generated callers are invisible to it); =DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed. A symbol whose definition is produced by a SELF-REGISTERING test/benchmark macro is never reported: doctest TEST_CASE/ TEST_CASE_FIXTURE/SCENARIO, gtest TEST/TEST_F/TEST_P, Catch2, Google Benchmark — a static initializer registers them, so a name-based call graph cannot see the caller and every one of them would be a false positive. Extend the list for your own framework with `.ripwire_config`'s one key, `register_macros = NAME[, NAME...]` (one directive per line, # comments). The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0. Exempt from dead-code only — such a symbol still participates in clone detection. A LEADING ./ anchors DIR at the repo ROOT (=./src matches only the top-level src/ subtree); a bare name (=src) matches that component ANYWHERE in the tree, including nested (test/fixture/src/…) @@ -2786,6 +2786,7 @@ $ ./build/ripwire . --dead-code=src **Caveats (stated by the binary):** +- list internal functions with no caller anywhere in the indexed tree internal source functions with no caller found in the indexed tree — a name-based graph reading, not a confidence score (dynamic dispatch, reflection and macro-generated callers are invisible to it); - =DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed. - The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0. @@ -3218,7 +3219,7 @@ $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply ### `--safe-delete=SYM` -**Answers:** "can I delete this?" — one call joining callers, blast radius, tests and history "can I delete this?" — ONE call composing signals the tool already computes for one already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=), every --uses read/write/import/call/extends site (uses=), how much of the blast radius the tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own high-confidence shape at defs=1 (dead_code_candidate=). +**Answers:** "can I delete this?" — one call joining callers, blast radius, tests and history "can I delete this?" — ONE call composing signals the tool already computes for one already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=), every --uses read/write/import/call/extends site (uses=), how much of the blast radius the tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own zero-caller/internal-linkage shape at defs=1 (dead_code_candidate=). ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict. @@ -3279,7 +3280,7 @@ $ ./build/ripwire . --slice=rankGraphTeleport **Answers:** (with --slice) follow the value flow transitively — back, forward, or in both directions TRANSITIVE cross-statement data-flow slice (modifies --slice=SYM:VAR; -refused alone or on the bare inventory — a flow needs a seed variable). Follows VALUE FLOW over reaching-definition def-use edges — a use of v reaches the last def of v in source order before it — by bounded BFS from the seed variable, the ARISE paper's own slicer semantics (arXiv:2605.03117: seed + direction, bounded BFS, stops at the function boundary; the inter-procedural half stays with --callers/--impact by the paper's own design). back = statements whose values feed the seed; fwd = statements the seed's value reaches; both = the union. Flow rows are : v= the variable at that step, d= BFS depth (seed rows are depth 0), f= the line the step was reached from. steps= counts flow rows; depth= states the bound in force. LIMITS (in the legend too): name-based, no alias analysis, line-granular ROWS (a multi-statement line merges) over statement-anchored CHAINING (a multi-LINE statement chains as ONE unit), a shadowed name's bindings walk separately (never into each other's block), data dependence only — no control dependence (the guard deciding whether a def executes is never a row). +refused alone or on the bare inventory — a flow needs a seed variable). Follows VALUE FLOW over reaching-definition def-use edges — a use of v reaches the last def of v in source order before it — by bounded BFS from the seed variable, the ARISE paper's own slicer semantics (arXiv:2605.03117: seed + direction, bounded BFS, stops at the function boundary; the inter-procedural half stays with --callers/--impact by the paper's own design). back = statements whose values feed the seed; fwd = statements the seed's value reaches; both = the union. Flow rows are : v= the variable at that step, d= BFS depth (seed rows are depth 0), f= the line the step was reached from. steps= counts flow rows; depth= states the bound in force. LIMITS (in the legend too): name-based, no alias analysis, line-granular ROWS (a multi-statement line merges) over statement-anchored CHAINING (a multi-LINE statement chains as ONE unit), a shadowed name's bindings walk separately (never into each other's block), data dependence only — no control dependence (the guard deciding whether a def executes is never a row). both= is UNSEEDED-REDUNDANT: unioned over a function's whole variable inventory, an unseeded both reaches no line the flat rows (bare --slice=SYM lists them) do not — disclosed as flow_redundant="1", never gated. Seed it (--at=FILE:LINE) for flow to add real reach beyond the flat inventory (measured, docs/research). **Try it** @@ -3309,6 +3310,7 @@ $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd **Caveats (stated by the binary):** - refused alone or on the bare inventory — a flow needs a seed variable). +- both= is UNSEEDED-REDUNDANT: unioned over a function's whole variable inventory, an unseeded both reaches no line the flat rows (bare --slice=SYM lists them) do not — disclosed as flow_redundant="1", never gated. ### `--slice-depth=N` @@ -3349,7 +3351,7 @@ $ ./build/ripwire . --at=src/graph.h:3954 ``` -**Shaped by:** `--slice` +**Shaped by:** `--slice`, `--slice-flow` **Caveats (stated by the binary):** diff --git a/docs/EVALS.md b/docs/EVALS.md index 5f5d7d878..33d6e1ff0 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 645 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 646 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5837,7 +5837,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **645 gate scripts**, all of which exist on disk. +naming **646 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6849,7 +6849,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 645. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 646. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LINEAGE.md b/docs/LINEAGE.md index b0245d459..2526d511f 100644 --- a/docs/LINEAGE.md +++ b/docs/LINEAGE.md @@ -242,7 +242,7 @@ shipped target links, and it is named in the near-miss paragraph below rather th | [SpotBugs](https://github.com/spotbugs/spotbugs) | Confidence is a disclosed axis SEPARATE from severity: "how sure am I" is answered independently of "how bad is it", so a consumer can filter on either without the other moving. Ripwire's delta: the axis landed on the CALL GRAPH, not on the linter — `--lint`'s built-ins carry no `sev=` at all by design, and a tree-sitter match has no resolver ambiguity, so a confidence number there would be invented rather than measured. | `prov="split"`, the third value of the existing per-edge provenance attribute, emitted on each arm of a k-way pick the resolver could not decide and keyed from the same predicate that increments `ambOut` (`src/graph.h`, `src/serialize.h`); gate `test/resolverhonestycheck.sh` | | [graphify](https://github.com/Graphify-Labs/graphify) | Edge confidence is not a number in a footer — it is drawn: a guessed edge must LOOK different from a resolved one in the rendered graph, every time, not only in the text. Ripwire's delta: dashed rather than faded, because a faded solid line reads as a DISTANT edge rather than an uncertain one. | `--html`'s two-pass edge paint and the `"a":1` flag in LINKS (`src/htmlexport.h`), with a counted legend clause; gate `test/htmlexport.sh`. Before this the renderer contained zero occurrences of amb/confidence/dashed/opacity — ripwire computed `amb=` and discarded it at render time | | [gortex](https://github.com/zzet/gortex) | Mine the repository's OWN history, not the last N months of wall-clock time. A window anchored to `now` is right for a live checkout and wrong for every pinned corpus, archived release and eval — and eval corpora are pinned by definition. Measured: on rocksdb at `0e2801ac` (HEAD dated 2024-10-17) gortex mined 9,854 co-change edges where ripwire's `--cochange` returned zero rows and exited 1 claiming "git unavailable / no history", which was false on both counts. | The default history window anchors on HEAD's committer date across all five miner construction sites and six window labels (`src/gitmine.h`, `src/main.cpp`, `src/verbs_report.h`, `src/mcpverbs.h`); an explicit `--since` is never re-anchored, so it is the opt-out. Gate `test/sincewindowcheck.sh` arm 4 | -| [vulture](https://github.com/jendrikseipp/vulture) | Attach a CONFIDENCE tier (60-100%) to each dead-code finding instead of one undifferentiated list, so the near-certain findings can be triaged separately from the merely-plausible ones. Ripwire's delta: `--dead-code`'s `confidence="high"` is currently a FIXED literal on every finding, not tiers — tiering by ripwire's own evidence (linkage + in-degree + amb on incoming edges), explicitly NOT vulture's self-described-rough 60/90/100 numbers, is planned and not yet shipped. | the fixed `confidence="high"` literal on every `--dead-code` finding (`src/main.cpp`) | +| [vulture](https://github.com/jendrikseipp/vulture) | Attach a CONFIDENCE tier (60-100%) to each dead-code finding instead of one undifferentiated list, so the near-certain findings can be triaged separately from the merely-plausible ones. Ripwire's delta: `--dead-code` used to stamp a FIXED `confidence="high"` literal on every finding — a claim the candidate loop had no per-finding signal (no ambiguity count, no dispatch/reflection flag) to back. Removed rather than tiered (T13/fix1, 2026-09-20): the root carries no `confidence=` at all now, only `evidence="internal-linkage+zero-callers"`, the one thing the code actually knows. Tiering by ripwire's own evidence, explicitly NOT vulture's self-described-rough 60/90/100 numbers, remains unshipped. | `evidence=` on every `--dead-code` finding, no `confidence=` (`src/verbs_quality.h`) | | [codesight](https://github.com/Houseofmvps/codesight) | A per-facet write budget sized to roughly one screen of prose (~300 tokens per generated article) generalises to a SHARE floor: the least a document can be given and still be worth serving when one fixed budget is divided among several competing documents — distinct from the absolute last-resort minimum, which is a different, smaller number. | `kRecallShareFloorBytes = 900` in the water-filling recall budget (`src/recall.h`) | | [cocoindex-code](https://github.com/cocoindex-io/cocoindex-code) | Staleness is not only "does the index still describe the tree" — already solved here — it is also "can THIS BINARY read this artifact": a foreign format, parser, or architecture version has to refuse cleanly and NAME why, not misread silently. | `--doctor` check 7's `` row and the named `CacheReject` refusal enum (`src/ingest_cache.h`). **Second lesson, same repo, taken 2026-09-06:** when the question names a file, that file's own declaration/definition partner is the answer far more often than the largest thing that transitively depends on it — measured, cocoindex returned `db/wal_manager.h` as result #1 in 55 B where `--situ` missed it entirely in 3,104 B. Now surfaced ahead of the transitive list in `--situ`. **codanna** reached the same incremental-as-a-contract framing independently and is credited here rather than given a duplicate row, because the lesson and the artifact it points at are the same one | diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index cd0d456fc..c82f54b26 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1123,7 +1123,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["645 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["646 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1147,7 +1147,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "645", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "646", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1397,7 +1397,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["183 long flags · 34 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["645 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["646 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/queries/java/tags.scm b/queries/java/tags.scm index 01f30a4aa..9157b3ee2 100644 --- a/queries/java/tags.scm +++ b/queries/java/tags.scm @@ -7,7 +7,9 @@ ; - type declarations: class / interface / enum → def nodes (the containers) ; - method + constructor declarations → the def nodes calls resolve TO ; - method invocations + object creations → the call references (edges) -; - imports → reference edges to the imported name's final segment +; - imports → IMPORT reference edges (role="import", @reference.import) to the imported name's +; final segment — a dependency edge, not a call (T13/fix3: was @reference.call, which double- +; counted --callers=/--impact= fan-in by one phantom "caller" per importing file) ; ; Deliberately NOT captured (noise): fields, local variables, annotations, `@name` on ; every type mention. Only defs + calls + imports become graph nodes/edges — matching @@ -71,20 +73,19 @@ type: (scoped_type_identifier (type_identifier) @name .)) @reference.call -; import a.b.C; — the last scoped-identifier segment is the imported name. +; import a.b.C; — the last scoped-identifier segment is the imported name. T13/fix3: an import is a +; DEPENDENCY edge, not a call — @reference.import (not @reference.call) so ingest_sidecap.h's generic +; "reference.import" check routes it to RefRole::Import, which graph.h's isResolvableCallReference +; (Call+Macro only) excludes from the call-graph CSR: it still rides --uses as a role="import" use-site +; (same shape C++'s `using ns::name;` already gets), but --callers/--impact fan-in no longer count it. ; -; #60 CONSEQUENCE, recorded here beside the choice that causes it (train-12, 2026-09-20). This capture is -; @reference.call, not @reference.import, DELIBERATELY: an import is a dependency edge to the imported -; name, and spelling it as a call is what puts it in the graph. An import sits outside every named -; definition, so since #60 it is owned by the file's synthetic module-scope owner and therefore mints a -; CALLER edge: in test/kotlinfix, --callers=square went 1 -> 2, the second caller being Greeter.kt's -; `import com.example.util.square`. Java behaves identically and is inert only because a fixture's -; `import java.util.List` names nothing defined in-tree. Java and Kotlin are the only two languages where -; this happens; every other module-scope owner in this tree owns real top-level executable code. -; The mis-description is in this rule, not in #60, and it predates it: on 755f9026 the same reference -; already read ``. Changing it is an extraction change (kParserVer) and -; an owner call on whether an import is a caller — open, not taken here. test/kotlincheck.sh §1a pins the -; behaviour so it cannot go silent. +; This answers the owner call train-12 left open beside this rule. #60 gave every file a synthetic +; module-scope owner, and an import sits outside every named definition, so as a @reference.call it +; minted a CALLER edge: in test/kotlinfix, --callers=square read 1 -> 2, the second "caller" being +; Greeter.kt's `import com.example.util.square`. Java behaved identically and was inert only because a +; fixture's `import java.util.List` names nothing defined in-tree. Java and Kotlin were the only two +; languages with this shape; every other module-scope owner in this tree owns real top-level executable +; code. test/kotlincheck.sh §1a pins the behaviour so it cannot go silent again. (import_declaration (scoped_identifier - name: (identifier) @name)) @reference.call + name: (identifier) @name)) @reference.import diff --git a/queries/kotlin/tags.scm b/queries/kotlin/tags.scm index 179bfca0c..8f1ad9f03 100644 --- a/queries/kotlin/tags.scm +++ b/queries/kotlin/tags.scm @@ -15,9 +15,13 @@ ; split into function/method, since the grammar gives no structural way to tell them apart ; without a scope walk (that walk is ingest_sidecap.h's job, not the tags pass's). ; - call expressions (bare and via navigation) → the call references (edges). -; - imports → reference edges to the imported name's final segment, mirroring Java's import rule -; exactly (including its capture kind: @reference.call, not @reference.import — matched for -; consistency with the one other JVM-family query in this tree). +; - imports → IMPORT reference edges (role="import", @reference.import) to the imported name's +; final segment — a dependency edge, not a call. T13/fix3 (2026-09-20): this used to spell +; @reference.call "for consistency with the one other JVM-family query in this tree" — that +; consistency was with a defect, not a virtue: an import is not a call in either language, and +; with #60's owner an import line got a real caller edge, double-counting +; --callers=/--impact= fan-in by one phantom "caller" per importing file. Both queries now agree +; on @reference.import instead. ; ; Deliberately NOT captured (first cut, disclosed floor, not an oversight): ; - property_declaration (`val`/`var`, class or top-level): Kotlin's constant-vs-mutable @@ -84,4 +88,4 @@ ; test/kotlincheck.sh §1a pins it so it cannot go silent. (import_header (identifier - (simple_identifier) @name .)) @reference.call + (simple_identifier) @name .)) @reference.import diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index 3b30f422e..bc2a23fb1 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -188,8 +188,8 @@ emits a flat `` top-K — identity + score + signatu same grammar as `--edit-check`/`--around`/`--lego`). The removal-side sibling of step 10: composes 1-hop `callers=`, the transitive `--impact` blast radius (`impact_reaches=`), every `--uses` read/write/import/call/extends site (`uses=`), how much of that radius the `tested=` lens covers - (`radius_tested=`/`radius_untested=`), and `--dead-code`'s own high-confidence shape at `defs=1` - (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `uses-exist` / + (`radius_tested=`/`radius_untested=`), and `--dead-code`'s own zero-caller/internal-linkage shape at + `defs=1` (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `uses-exist` / `untested-radius` — never a go/no-go verdict; `radius_untested=` equal to `impact_reaches=` is the strongest signal ("nothing downstream is test-covered"). `ambiguous_callers=`/per-row `amb="1"` disclose the same call-graph resolution limit `--edit-check`'s `incompatible=` and `--for`'s `amb=` already carry. diff --git a/skills/ripwire-fresh-eyes/SKILL.md b/skills/ripwire-fresh-eyes/SKILL.md index d28f2b887..216bfd084 100644 --- a/skills/ripwire-fresh-eyes/SKILL.md +++ b/skills/ripwire-fresh-eyes/SKILL.md @@ -175,12 +175,13 @@ sections your question needs. and any row carries `gating="1"`, exit 0 clean or `dialect="0"`, exit 1 only when FILE could not be read. 2. **Dead code** — `ripwire --dead-code[=DIR] --legend=compact` - `` — - source-defined free functions with explicit internal linkage and **in-degree 0** in the indexed call - graph. `--dead-code=DIR` scopes to a sub-tree. Methods, declarations, headers, and external-linkage entry - points are excluded, so `count="0"` means "no high-confidence candidates," not "no dead code." Name-based - and tree-local — **verify before deleting**: confirm against the compiler's unused-symbol diagnostics or a - linker map; ripwire narrows the field, the toolchain proves it. + `` — no `confidence=` + attribute (removed 2026-09-20: it was a fixed literal, not a derived signal) — source-defined free + functions with explicit internal linkage and **in-degree 0** in the indexed call graph. + `--dead-code=DIR` scopes to a sub-tree. Methods, declarations, headers, and external-linkage entry + points are excluded, so `count="0"` means "no candidates met the evidence rule," not "no dead code." + Name-based and tree-local — **verify before deleting**: confirm against the compiler's unused-symbol + diagnostics or a linker map; ripwire narrows the field, the toolchain proves it. 3. **Duplicate bodies** — `ripwire --clones --legend=compact` ` --hotspots --legend=compact` — files ranked by `score = churn × ccx`; `top=` names the gnarliest function. Plan edits around this list. -**6. Budget it** if the map is large — `--max-tokens=8000` or `--top-k=50`. +**6. Budget it** if the map is large — `--max-tokens=8000` or `--top-k=50`. Both shape the default map; +a positive, explicit `--top-k` is inert on plain `--for` (it warns on stderr and emits the full bundle) — +bound a `--for` call with `--signatures-only`, `--token-budget=N` or `--detail=N` instead. ## Orienting N agents at once, not yourself — `--partition=N` diff --git a/skills/ripwire-quality-bar/quality-metrics.md b/skills/ripwire-quality-bar/quality-metrics.md index 287ec3859..10bd139ea 100644 --- a/skills/ripwire-quality-bar/quality-metrics.md +++ b/skills/ripwire-quality-bar/quality-metrics.md @@ -53,7 +53,7 @@ defect- and vulnerability-prone files in the empirical literature. Push hardest | **Change-amplification `amp`** | `|direct callers| + |co-change partners|` — "editing this historically touches N places" in one number. Granularity note: callers are symbol-level, co-change is file-level — read it as a blast-radius proxy, not a pure per-symbol count. | high `amp` ⇒ change carefully | `--metrics amp=`, `--for` | add tests before touching a high-`amp` symbol; don't refactor it and three callers in the same diff | | **Dependency cycles / god-files** | tangled, hard to test in isolation. VALIDATED — defects concentrate in cyclic-dependent components. | 0 cycles; no 100-fan-in header | `--deps`, `--report` | break the cycle (introduce an interface); split the god-file | | **External dependency surface** | supply-chain + build weight + reader-learning tax | add only when it earns it | `--external-surface`, `--uses` import role | reuse an in-tree dep first; justify each new import | -| **Dead code** | latent risk + reader tax | 0 newly-orphaned | `--dead-code` | review high-confidence internal zero-caller candidates; verify before deletion | +| **Dead code** | latent risk + reader tax | 0 newly-orphaned | `--dead-code` | review internal zero-caller candidates (`evidence=`); verify before deletion | | **Bus factor** | knowledge concentration (`bf=1` = one owner) | flag `bf=1` on code you touch | `--owners` | add a second reviewer; leave a doc/comment trail | | **Untested integration seams** | cross-module calls no test reaches | 0 new uncovered seams | `--seams`, `--affected` | add a test across the seam you introduced | | **Resolution ambiguity `amb=`** | the map's OWN honesty signal — K calls the resolver guessed | verify high-`amb` in source | header `ambiguous=`, per-symbol `amb=` | read the source before trusting a high-`amb` edge | diff --git a/src/cli.h b/src/cli.h index 7596ae8ce..7500a4443 100644 --- a/src/cli.h +++ b/src/cli.h @@ -297,7 +297,7 @@ struct Config bool compress = false; // --compress: strip comments + blank runs from --expand/--outline body output (P2-B) bool baseline = false; // --baseline: write .ripwire_arch_baseline sidecar (accept current debt), exit 0 bool baselineUpdate = false; // --baseline-update: merge current violations into baseline (accept new debt), exit 0 - bool deadCode = false; // --dead-code[=DIR]: internal source functions with zero indexed callers (high-confidence candidates) + bool deadCode = false; // --dead-code[=DIR]: internal source functions with zero indexed callers (a name-based graph reading, not a confidence score) std::string_view deadCodeDir; // --dead-code=DIR: restrict scan to DIR — matched as WHOLE path components (a dir, a nested dir, or a filename); a filter naming nothing indexed REFUSES (§P0.3). Leading ./ anchors DIR at the repo root instead of matching that component anywhere (§A10.6) bool qualityBaseline = false; // --quality-baseline: snapshot ccx/clones/dead to .ripwire_quality_baseline bool allowDirty = false; // --allow-dirty: let --quality-baseline pin on a tree that DIFFERS from HEAD (H11) — the sidecar is @@ -327,7 +327,7 @@ struct Config std::string_view safeDeleteSym; // --safe-delete=SYM: "can I delete this?" composed from signals the tool already // computes — 1-hop callers, the transitive --impact blast radius, every --uses // read/write/import/call/extends site, how much of the radius the tested= lens - // covers, and --dead-code's own high-confidence shape at defs=1. FACTS only: + // covers, and --dead-code's own zero-caller/internal-linkage shape at defs=1. FACTS only: // risk= names what was found (none-found/uses-exist/untested-radius), never a // go/no-go verdict. file:name disambiguates like --around/--lego. std::string_view sliceSpec; // --slice=SYM[:VAR] (lane/paper-slice): NAME-BASED intra-procedural def-use @@ -1609,7 +1609,7 @@ inline constexpr char kHelpHead[] = " An @FILE:LINE seed rebinds to the innermost enclosing definition (sym= names it) and\n" " analyses exactly that definition's file\n" " --dead-code[=DIR] list internal functions with no caller anywhere in the indexed tree\n" - " high-confidence internal source functions with no caller in the indexed tree; =DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed.\n" + " internal source functions with no caller found in the indexed tree — a name-based graph reading, not a confidence score (dynamic dispatch, reflection and macro-generated callers are invisible to it); =DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed.\n" " A symbol whose definition is produced by a SELF-REGISTERING test/benchmark macro is never reported: doctest TEST_CASE/\n" " TEST_CASE_FIXTURE/SCENARIO, gtest TEST/TEST_F/TEST_P, Catch2, Google Benchmark — a static initializer registers them, so a\n" " name-based call graph cannot see the caller and every one of them would be a false positive. Extend the list for your own\n" @@ -1801,7 +1801,7 @@ inline constexpr char kHelpHead[] = " already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=),\n" " every --uses read/write/import/call/extends site (uses=), how much of the blast radius the\n" " tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own\n" - " high-confidence shape at defs=1 (dead_code_candidate=). ambiguous_callers= names callers\n" + " zero-caller/internal-linkage shape at defs=1 (dead_code_candidate=). ambiguous_callers= names callers\n" " whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of\n" " proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND\n" " zero uses), untested-radius (a radius exists and none of it is test-covered), or\n" @@ -1867,6 +1867,10 @@ inline constexpr char kHelpHead[] = " CHAINING (a multi-LINE statement chains as ONE unit), a shadowed name's bindings walk\n" " separately (never into each other's block), data dependence only — no control\n" " dependence (the guard deciding whether a def executes is never a row).\n" + " both= is UNSEEDED-REDUNDANT: unioned over a function's whole variable inventory, an\n" + " unseeded both reaches no line the flat rows (bare --slice=SYM lists them) do not —\n" + " disclosed as flow_redundant=\"1\", never gated. Seed it (--at=FILE:LINE) for flow to\n" + " add real reach beyond the flat inventory (measured, docs/research).\n" " --slice-depth=N (with --slice-flow) bound the walk at N hops; 1..32, default 8\n" " the --slice-flow BFS depth bound, 1..32 (default 8, always disclosed as depth= on the\n" " root). A bound that cuts a live frontier is disclosed as flow_truncated=\"1\" — a short\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index 9b77a0f01..4c4cc9b9d 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -138,7 +138,7 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { "hotspots", "hotspots", "maintenance pain = churn x ccx over window=: ; unranked_*= no churn/complexity" }, { "clones", "clones", "similar normalized-token bodies: of ; dup_loc=/dup_pct=" }, { "deps", "deps", "file-to-file include/import view, heaviest cone first: , , , " }, - { "dead-code", "dead-code", "high-confidence dead functions (internal linkage, no caller in the index): ; filter= path component" }, + { "dead-code", "dead-code", "internal-linkage functions with no caller found in the index (not a confidence score): ; filter= path component" }, { "lint", "lint", "AST-only checks, facts not gates: of " }, { "lintcatalog", "lint-catalog", "the built-in lint rule registry: " }, { "external-surface", "external-surface", "names used but never defined in the index: " }, @@ -799,8 +799,9 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "changed", "changed=: the files/symbols argument as given", false, "affected", MapHeaderRead::No, {}, "affected" }, // connect: src/mcpverbs.h packConnect { "terminals", "terminals=/groups=/edges=: task symbols resolved, connected groups (g), e edges printed", false, "connect", MapHeaderRead::No, {}, "connect" }, - // dead-code: src/verbs_quality.h - { "confidence", "confidence=high count=: every row met the evidence= rule; count= the rows, a floor", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, + // dead-code: src/verbs_quality.h — T13/fix1: confidence="high" removed (a claim the code did not + // support); count= still needs its own reading now that the confidence row is gone. + { "count", "count=N: candidates meeting evidence= (a floor)", false, "dead-code", MapHeaderRead::No, {}, "dead-code" }, // doc-drift: src/docdrift.h { "drift", "drift=/dated=: anchors that no longer hold / anchors skipped as dated (unverifiable by date)", false, "doc-drift", MapHeaderRead::No, {}, "doc-drift" }, // edit-check: src/editcheck.h diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 582b643ea..4eff95028 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -249,7 +249,21 @@ constexpr std::uint32_t kCacheVersion = 24; // 24: RawRef gains `viaAr // (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 = 118; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 119; // bump on any grammar/.scm/extraction change + // 119 = 2026-09-20 (T13/fix3): queries/java/tags.scm + // and queries/kotlin/tags.scm's import captures were + // @reference.call — an import is a dependency edge, + // not a call, and since #60 gave top-level statements + // a owner, an import line got a REAL + // caller edge: --callers=/--impact= fan-in on a + // Java/Kotlin symbol was inflated by one phantom + // "caller" per importing file. Both now spell + // @reference.import (RefRole::Import), the same + // mechanism C++'s `using ns::name;` already uses — + // still a role="import" --uses site, never a call- + // graph edge (graph.h isResolvableCallReference is + // Call+Macro only). A cache written before this + // double-counts every JVM import as a caller. // 118 = 2026-09-19 (CodeRabbit follow-up, thread // 4053600599: isJsxIntrinsicTagIdentifier // (src/ingest_names.h) tested `!isUppercase`, which kept diff --git a/src/quality.h b/src/quality.h index ceb7387ac..934107cbc 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1989,7 +1989,11 @@ 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 = 24; // MUST equal ingest.cpp's kCacheVersion (gated) -constexpr std::uint32_t kIngestParserVerMirror = 118; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 119; // MUST equal ingest.cpp's kParserVer (gated) + // 119 = 2026-09-20 (T13/fix3): queries/java + queries/kotlin + // tags.scm import captures moved @reference.call -> + // @reference.import (RefRole::Import) — an import is a + // dependency edge, not a call-graph edge. // 118 = 2026-09-19 (CodeRabbit follow-up, thread 4053600599: // isJsxIntrinsicTagIdentifier now tests ASCII-lowercase // directly, so `_Widget`/`$Widget` are components, not @@ -7684,15 +7688,36 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap { const auto* ro = g.inEdges.rowOffsets(); gtl::btree_map reuseSeen; // clone-group count is not strictly bounded by symbol count, so keep the unbounded sorted map here - const auto reportReusedClones = [ & ]( const std::vector& cgs ) - { - for( const CloneGroup& cg : cgs ) + // T13/fix2: reportReusedClones read the SAME clone vectors as reportNewClones (duplication, above) + // but carried neither of its two demotions — the all-test-script skip and the idiom→minor demotion — + // so a clone group entirely composed of test SCRIPTS (sibling shell gates repeating the house + // mcp_call()-style boilerplate by convention) fired this kind even though `duplication` exempts the + // identical shape. Both demotions now mirror reportNewClones exactly: the skip continues outright + // (never a row), and the idiom verdict rides the row the same way (vd.demoted / cloneIdiomName). + const auto reportReusedClones = [ & ]( const std::vector& cgs, const std::vector& vx ) + { + for( std::size_t ci = 0; ci < cgs.size(); ++ci ) { + const CloneGroup& cg = cgs[ci]; + const CloneIdiomVerdict& vd = vx[ci]; const std::uint64_t h = cloneGroupHash( cg, ing, root ); if( std::binary_search( base.cloneGroups.begin(), base.cloneGroups.end(), h ) ) { continue; // group already existed → not new } + // B10.1a (mirrors reportNewClones): a clone group ENTIRELY composed of test-SCRIPT members + // is fixture-class noise — sibling shell test scripts repeat near-identical setup/ok/no + // boilerplate by convention (see isTestScriptPath). Exempt only when every member is a test + // script, so a real src/ ↔ test-script clone (still worth a look) is unaffected. + bool allTestScript = !cg.members.empty(); + for( NodeId m : cg.members ) + { + if( m >= ing.symbols.size() || !isTestScriptPath( rootRelPath( ing, ing.symbols[m].fileId ) ) ) { allTestScript = false; break; } + } + if( allTestScript ) + { + continue; + } // r27 SUSPICION-C FIX — "the reused helper is preexisting BY CONSTRUCTION" was asserted here // (and in fbc527e's commit message) but never ENFORCED: fan-in ≥ 3 is trivially reached by a // brand-new helper that three brand-new call sites use, so an all-new blob of code duplicating @@ -7748,12 +7773,13 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap joined += " | "; } } - regs.push_back( { "new-clone-of-reused-helper", joined, 0, maxFanin, h, false, {}, cloneGroupIsNew( cg ) } ); // now = the eroded helper's fan-in; ack identity = the member-set hash + regs.push_back( { "new-clone-of-reused-helper", joined, 0, maxFanin, h, vd.demoted, + std::string( cloneIdiomName( vd.idiom ) ), cloneGroupIsNew( cg ) } ); // now = the eroded helper's fan-in; ack identity = the member-set hash stampCloneLoc( cg ); } }; - reportReusedClones( exactClones ); - reportReusedClones( type3Clones ); + reportReusedClones( exactClones, exactIdioms ); + reportReusedClones( type3Clones, type3Idioms ); } std::sort( regs.begin(), regs.end(), []( const Regression& a, const Regression& b ) diff --git a/src/slice.h b/src/slice.h index aa97ac1e5..cf85f7a7c 100644 --- a/src/slice.h +++ b/src/slice.h @@ -3058,7 +3058,8 @@ inline std::string sliceLegendText( const SliceEmitOpts& opts ) "(-=none) per reach=cfg (flow-sensitive; C-family, Python) | linear (source order; JS/TS, Go, Java, Rust). Inventory " ", vars=count. " "bindings=shadow count; preproc_rows=lines dropped under #if 0; seed/var_from/seed_vars/seed=1 = line-seed disclosure. " - "Flow rows add v=variable d=depth f=from-line; steps=flow rows, depth=bound, flow_truncated=1 bounded not complete. " + "Flow rows add v=variable d=depth f=from-line; steps=flow rows, depth=bound, flow_truncated=1 bounded not complete, " + "flow_redundant=1 (both=, unseeded only) reaches no line the flat inventory does not — seed via --at=FILE:LINE for real reach. " "Limits: a write hidden behind a call (receiver mutation, by-ref/out-param, macro) rows as a use; no alias analysis; " "the statement is the unit (nested bodies/?:/short-circuit fold, goto untracked); no control dependence; block " "scopes separated; C-family #if 0 dropped, other #if kept+flagged. Full legend: omit " @@ -3134,7 +3135,8 @@ inline std::string sliceLegendText( const SliceEmitOpts& opts ) "cause is limit (2): receiver mutation leaves no def to anchor on — read the rows, not just the count. EXTRA LIMITS: " "rows are line-granular (a multi-statement line merges and may over-connect) while chaining is statement-anchored (a " "statement spanning lines chains as ONE unit keyed on its first line); data dependence only — no control dependence: " - "the guard (if/loop) deciding whether a def executes is never a row. -->"; + "the guard (if/loop) deciding whether a def executes is never a row. " + "flow_redundant=\"1\" (unseeded both=): adds no line beyond the flat inventory; seed --at= for real gain. -->"; } } // H1: the residue clause, in BOTH dialects and as its own comment — opened `