diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd030ae65..7e28982ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -312,11 +312,23 @@ jobs: $0 ~ "^## \\[" ver "\\]" { on = 1; print; next } on && /^## \[/ { exit } on { print } - ' CHANGELOG.md > dist/RELEASE_NOTES.md - if [ ! -s dist/RELEASE_NOTES.md ]; then + ' CHANGELOG.md > dist/RELEASE_SECTION.md + if [ ! -s dist/RELEASE_SECTION.md ]; then echo "CHANGELOG.md has no '## [$ver]' section — write it before tagging $tag" >&2 exit 1 fi + # The release body is the hand-written summary at the top of the section, up to the line + # ``, then a link to the whole section: the whole section can pass + # GitHub's 125,000-character release-body limit (0.6.10's is 170 KB). A section with no such line is + # published whole. The link names the commit this job checked out (github.sha), the one the section was + # read from, so a manual run from a ref other than the tag still links the text it summarised; on a tag + # push that commit is the tag's. + if grep -qx '' dist/RELEASE_SECTION.md; then + sed '/^$/,$d' dist/RELEASE_SECTION.md > dist/RELEASE_NOTES.md + printf '\n---\n\nEvery entry of this release, with its measurement and its floors, is in [CHANGELOG.md](https://github.com/%s/blob/%s/CHANGELOG.md).\n' "${{ github.repository }}" "${{ github.sha }}" >> dist/RELEASE_NOTES.md + else + cp dist/RELEASE_SECTION.md dist/RELEASE_NOTES.md + fi wc -l dist/RELEASE_NOTES.md - uses: softprops/action-gh-release@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3f21d02..ab1ca7a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,830 @@ Entries for the next release are files under [`changelog.d/`](changelog.d/README stays this one paragraph. At release, the step in CONTRIBUTING.md ("Cutting a release") folds the files in under the new version's heading. +## [0.6.10] — 2026-11-08 + +Seventy-one merged pull requests since 0.6.9 (#433 to #570). What a user sees (each entry below states its own measurement and its floors): +- More complete answers: on our fixed in-sample questions (the same 163 everyday, 39 multi-file and 56 large-repository questions, commands and grader as the README answers table, one call each, no cache), everyday answers complete 129 → 132 of 163 (27 partial, 4 none), multi-file 12 → 19 of 39, large repositories 23 → 30 of 56; required items delivered 1,142 → 1,152, 84 → 114 and 148 → 164; no everyday answer stated something wrong (read in source). Answers are larger: everyday answers total 2.37 MB against 1.95 MB (median 10.7 KB against 10.6 KB), multi-file 722 KB against 536 KB, large repositories 1.97 MB against 1.38 MB, so bytes per complete answer rise from 15.1 to 18.0 KB (everyday) and from 60 to 66 KB (large repositories). One everyday question that read complete in 0.6.9 reads none in 0.6.10 (textual-09; hono-17 stays complete under the rule for exports flagged as public API; the README footnote says why). A grader input lost in a cleanup was restored (GRADER-FIX-1107), so the 0.6.9 and 0.6.10 columns are graded by the same scripts. In-sample questions, so progress between releases, not held-out performance; measured on main just before #570 (the `--for` anchor-choice change), which these figures do not include. +- Faster and lighter, as percent changes (each figure is that pull request's own measurement against the build just before it, on one machine; the changes were not measured together, and absolute seconds and megabytes come with the 0.7.0 table, measured on one other machine): + - Warm `--deps` and `--for` on large trees use 35 to 67 % less CPU: vscode `--deps` -67 %, cockroach `--for` -57 %, envoy `--deps` -35 % (#542); a warm TypeScript `--deps` uses a further 22 % less CPU on vscode (#554). Cold runs gain less: instructions -14 % on vscode, -12 % on cockroach, -4 % on envoy. + - Peak memory: on macOS the peak footprint of an index is about halved, -49 % on envoy, -47 % on cockroach, -48 % on vscode (#497); a warm run's peak is 30 to 40 % lower (-37 % on envoy, -40 % on terraform, -36 % on cpython) and sits at or under the cold run's (#556); the include closure of a large tree is held in an eighth of the memory, vscode's 32.5 MB of sorted vectors becoming 3.74 MB (-88 %, #505). + - A file the nesting guard refuses no longer makes every warm run rewrite the whole cache: on envoy a 146 MB write per run is gone (#553; user CPU about 10 % lower, inside that measurement's noise). +- Fewer false edges and more honest answers: `--arch`, `--deps`, `--cochange`, `--impact` and `--export=cc.json` stop reporting coupling from edges the index never had; `--deps` stops calling every deep graph `tangled`; a "not found" from an index the memory guard cut no longer reads as proof of absence; `unused` is printed only when the scan can prove it; a `--path` chain stitched from calls bound by name alone is no longer printed as the plain answer; a Rust prelude call, a macro with no in-tree binding, and a Python call to a builtin or third-party name no longer bind a same-named definition; `--for` reads `confidence="high"` on a name-exact route only when the anchor heads the rows, and hedges a second-hop caller reached only by name. +- `--for` reaches more of what a task needs: registration rows and the registry key to what is registered under it (Python signals, TypeScript providers, computed-key registrars and their readers); the sites of a literal, a `--flag` and its default, a table or registration identifier; the family of an add-a-new-X task and the manifest lines that list a file; the doc sections for a prose question about the docs; re-export hops, alias chains and namespace re-exports; both sides of a missing `path from A to B`; the callers of the one function a sentence asks about; the implementations that can run at an interface call; the match arm that holds a literal; the one namesake a path word picks, while a test-only anchor among several no longer commits the route; Cargo `[[bin]]` and package.json `bin` / `main` entries; and ``, the shown files' other symbols. +- New and changed verbs: `--path` and `--connect` hops print the call site, the call and the callee's signature; `--impact` sums up the importers a cut listing hides and `--impact=` lists a file's importers; `--defs` prints an alias's target and `--uses` lists the sites written through an alias; reST and Sphinx documents become real units and `--recall` serves the best-matching part of an HTML page; `ripwire wrap claude` routes edits through a ranged Read and Claude Code's native Edit. +- Language specifics: C++ pointers to member functions, `using` and namespace aliases and function-pointer typedefs; C / C++ quote includes resolve at the include roots a build passes; Rust `crate::`, `super::`, `self::` and `mod x;` resolve in the includer's own crate; Python `import a.b as c`, `from`-imported submodule calls and src layouts; TS/JS `run=` runs from the package directory. +- Contributors: a committed filename can no longer be read as commit metadata in `git log` framing (@pt-act, #433, #469); a TS/JS test in a subdirectory package gets a runnable `run=` hint (@llvm-x86, with @rainhuang0220's #330: #335, #442) and `--for` shows a Cargo `[[bin]]` and a package.json `bin` / `main` entry (@llvm-x86: #459, #460, folded into #511); langfacts slice 6c, Windows warm runs that leave the cache directory's ACL alone and shell test scripts named `test-*.sh` read as tests (@lennix1337: #471, #468, #496); an aliased Python `import a.b as c` is a file edge again, and C / C++ include roots with the foreign-qualifier veto (@jflowers: #530, #524, #526, #478); the installer hardening list of #293 (@s0undt3ch: #482); reports that led to fixes: reST and Sphinx documents (@muayyad-alsadi, #409), `--for` on a file name (@pguerrerolinares, #466), the router skill's language list (@gucliti, #521). +- Upgrade: caches rebuild once (`kParserVer` 301, `kCacheVersion` 49); quality snapshots of the older scheme are not used (scheme 25). +- Thanks: @pt-act (#433, #469), @llvm-x86 (#442, #459, #460), @lennix1337 (#436, #447, #471, #472), @jflowers (#478, #524, #526, #530), @s0undt3ch (#293), @muayyad-alsadi (#409), @pguerrerolinares (#466), @gucliti (#521) and @rainhuang0220 (#330). + + +### Fixed — TS/JS `run=` runs from the package directory (#335, #442, thanks @llvm-x86) + +A TS/JS test in a subdirectory package got a `run=` that could not run. The command was root-relative. Run from the root, +the runner found no package config. Run from the package, the path was root-relative. Now the command is a subshell, +`(cd && )`, and the path after the `cd` is package-relative. The subshell keeps the `cd` from outliving the command, so a shell that keeps its directory between calls (an agent's) starts the next row at the root again. A multi-root run has no `root=` and keeps the plain command. Paste it from the shell at the root. + +A bracketed path, such as a Next.js route (`app/[slug]/page.test.ts`), had `--` before it. vitest reads `--` as the end +of its options and drops the file filter, so the row ran the whole suite. vitest and jest now get one quoted path. + +Two paths get no `run=` now. A path that starts with `-` gets no `run=`, because the runner could read it as an option. To run such a file by hand, use `node ./-suite.test.mjs`: on Node 26.9 `node --test` drops the `./` and exits 1 with `bad option`. Node's test runner globs its +file arguments, so a bracketed path under `node --test` matches no file and the runner exits 1. Both rows print `run_unknown="1"`. + +jest reads its argument as a regular expression, so a path under a package below `root=` has every regex byte escaped with a backslash, a dot included (`a.test.js` would also match `aXtest.js`). + +A package at `root=` keeps its command. A path there that is not shell-safe loses the `--` and has its regex bytes escaped. For `node --test`, a glob byte in the path gives `run_unknown="1"`. + +Limit: a jest package at `root=` keeps a plain dot as a regex dot when the path is shell-safe, so its filter may also match a sibling such as `route/test.ts`. vitest and jest match the path as a substring, so `src/lib.test.ts` also runs `deep/src/lib.test.ts`: a run can include extra files, never fewer. + +A vitest or jest command now needs a file the runner's default include collects: a name ending in `.test.` or `.spec.`, or, for jest only, a file under a `__tests__/` directory (jest matches that glob against the whole path, so one above the package counts too; thanks CodeRabbit). `x.test.helper.ts` (both runners) and a vitest `__tests__/x.ts` got a command that exits 1 with "No test files found". They are now `run_unknown="1"`. The rule follows `defaultVitestFileName` from #330 (thanks @rainhuang0220, #330). Limits: a `vitest.config.*` or `jest.config.*` `include`/`testMatch` is not read, so a config that narrows the default still gets a command; and jest 29's default lacks the `.mjs`/`.cjs`/`.mts`/`.cts` extensions that jest 30 collects, so a jest 29 package gets a command for such a file that exits 1. + +### Added — reST and Sphinx documents become real units (#409, thanks @muayyad-alsadi) + +`.rst` no longer goes through the markdown grammar. A hand-written parser reads titles in any punctuation, with the level +taken from first-seen order, and makes `.. option::`, `.. setting::`, `.. function::`, `.. class::`, `.. method::`, +`.. confval::` and their siblings into units named by their target. `.. _label:` is a unit too, and `:ref:` and `:doc:` are +link edges. A `.txt` is indexed when the nearest Sphinx `conf.py` declares it in `source_suffix`, which brings in django's +679 documents; a `.txt` anywhere else stays unindexed and stays in `unindexed=`. Past 20,000 units or 64 levels of nesting +a file keeps the units it has and reports `why="extract-partial"` in `--skipped`. + +On a code question, the `--for` tail lists code files before doc files. A question that asks for doc text keeps the old +interleaved order. When the index holds a doc file, the legend says which order the tail used. A reST directive or markdown +heading that shares a function's name is still listed as a second definition by `--callers`, `--impact` and `--safe-delete`. +`--path` starts and ends only at the code. + +### Fixed — a committed filename can no longer be read as commit metadata (`git log` framing) (#433, thanks @pt-act) + +Every `git log --name-only` consumer in the history miners framed its stream with a line-based reader and a +`__C__` marker matched as a line prefix, so a repository containing a file whose name begins with the marker +bytes had that file read as commit metadata. Measured on a hostile fixture (files named `__C__`, +`__C__x|1694000000.cpp`, `__C__ 1694000000.cpp`, `ab.cpp`, `q"uote.cpp`, `sp ace.cpp`, `back\slash.cpp`, +`pi|pe.cpp`, `--dashdash.cpp`): `--owners` analysed 8 of 11 files, five of them owned by an author email that +appears in no commit, one file's top owner flipped to a different real author, and the newline-named file lost +its churn entirely. + +One reader now parses the stream for all five consumers (`gitLogFileSets`, `gitLogNameOnlyRaw`, +`gitFileCommitCountsInDayWindow`, `gitLogDecayedFileMining`, `gitFileAuthors`): +`git log -z --name-only --format=%x00__C__%x00%ae%x00%at%x00%ct`, with the marker a whole-field sentinel a path +can never spell and one field of lookahead so a merge's prefix empty is not mistaken for a record boundary. +Any POSIX-legal filename — newlines, quotes, backslashes, pipes, leading dashes, the marker itself — +round-trips byte-exact, and ownership answers with the author, never the committer. On trees with ordinary +names the answers are byte-identical to before, pinned by a golden recorded from the pre-change binary over +`--owners`, `--hotspots` and `--cochange`. + +The history-walk cache's `kQChurnCacheScheme` moves 3 → 4: a blob written by a pre-lane binary holds the old +reader's stream, which for the repositories this fix targets is the corrupted one, and no key component can +tell that blob from a fresh read. Every repository's history is walked once more after the upgrade; on trees +with ordinary names the walk produces the same stream, so the cost is one cold walk and no changed answer. + +**The walkers' stop contract rides the field read.** The line readers this replaces checked the abandon flag +before every line; the first cut of the field reader checked it only between fields, so a stream whose fields +never arrive — a git answering in its default format, or a shim streaming bare lines — held the read inside one +unterminated field until end-of-stream instead of stopping at the next unit. The flag is now polled once per +byte, and the memory-guard fixture that measures it refuses in about a second where the narrowed read blocked +for the whole stream. + +**One case is disclosed, not fixed:** a merge whose FIRST combined path is a file literally named `__C__` +still reads as pathless — its prefix empty is followed by the marker-spelled path, which the boundary rule +cannot tell from a real record boundary, so that merge's files lose their attribution. Its next three paths +are read as a phantom record's email and epochs (which never parse, so ownership skips it); a fifth and later +combined path rides that phantom at epoch 0, which the co-change sets count as one more commit and +`--rank-by=churn-decay` weights as if it were HEAD's. The full fix is a format sentinel change, owned by a +follow-on spec. `test/gitframingcheck.sh` pins both the fix and this collision's exact observable, including +where the two churn miners disagree by design. + +### Fixed — Windows: warm runs no longer rewrite the cache directory's ACL + +- The per-user cache directory ladder skips `chmod(0700)` when `lstat` already reports an owner-only directory. On Win32 that `chmod` re-applies a protected DACL with inheritable ACEs, which Windows propagates through every file under the cache: with 38,000 files in the cache a warm run took ~2,024 ms instead of ~10.6 ms (p50 of 3, same revision, only this guard changed). A permissive directory is still repaired. Regression: `test/cache_ladder_windows.py` and the Windows arm of `test/cacheisolationcheck.sh` (#436, thanks @lennix1337). + +### Changed — Slice 6c: language receiver and comment rules moved to LangFacts columns (#447, thanks @lennix1337) + +Move the language predicates `implicitReceiverLang`, `memberNeverReachesFreeFunction`, +`moduleCallReachesFunctionsOnly`, `classNameReceiverLang`, and `langUsesHashComment` +into `constexpr` columns of `struct LangFacts` (`implicitReceiver`, `memberNeverReachesFreeFunction`, +`moduleCallReachesFunctionsOnly`, `classNameReceiver`, and `hashComment`) in `src/langfacts.h`. +Call sites in `src/receiverevidence.h`, `src/graph.h`, and `src/quality.h` now read `langFacts( l )` +directly, eliminating Type-2 clone group 99 (`langUsesHashComment` / `classNameReceiverLang`) from +`--clones` output and lowering the shared-file `== Lang::` ratchet ceiling in `test/langidentity/ratchet.tsv` +from 333 to 326. `isJsTsLitRecv` remains in `src/model.h` as it partitions `RecvKind` rather than `Lang`. +Output across the repository is verified byte-identical with 0 quality-delta regressions. + +### Changed — unused-in-file fix round 1: `unused` is printed only when the scan can prove it + +The `--for` "unused in file" table reads every text file under the root, indexed or not (`.vue`, `.svelte`, `.ipp`, Procfile, templates, `vendor/`, `dist/`), and reads `unknown why=unseen` when a file cannot be read. `#` opens a comment only where the language facts say so (Python, shell, Ruby), never in Rust attributes, C/C++ directives or JS/TS private fields. One-character names are scanned. A distribution's public API (a Python package with a build table, a published package.json, `export type *`) reads `why=api`, and the legend no longer says `never live`. File-path routes, `__init_subclass__` and metaclass registries, joined string names, dynamic directory imports and a stem aimed at the target module read `unknown`; the short-stem length rule is gone. + +Fix round 2 keeps to the same rule, `unused` is printed only on a proven shape. A C or C++ call no longer reads as a prototype. Non-ASCII names are one name. `.mdx` and `.txt` are read; prose docs are the one kind that is not, and the legend says so. A directory that cannot be entered counts as unseen, and `unseen_by=` names the limit that fired. Binaries count toward no cap. `* ` and `// ` line heads are comments only where the language has them. A module object indexed or passed along, a default export with an importer, a stem joined to anything computed, a multi-line decorator or attribute, a runtime-called name, and a module nothing imports (`why=unimported`, Python, TS, JS) all read `unknown`. + +### Added — `--for` shows a Cargo `[[bin]]` entry and the `main` it names (#450) + +- `--for` manifest entry points now read Cargo.toml `[[bin]]` tables (group `bin`), resolving each bin path to its `main` function; an omitted `path` follows Cargo's default (`src/main.rs` for the package's own name, `src/bin/.rs` otherwise), and a crate with no `[[bin]]` (and no `autobins = false`) lists its own bin at `src/main.rs` when the task names the package or asks about running a binary (run, bin, binary, cli, command, executable, launch, main, entry). A `path` with `../` or Windows `\` separators names the file it points at. Not read: the binaries Cargo auto-discovers (`src/bin/*.rs`, and `src/main.rs` beside declared `[[bin]]` tables) and `src/bin//main.rs` as a default path. Gate coverage in `test/epmanifestcheck.sh` (issue #450). Thanks to @llvm-x86 for the reader, the fixtures and the Cargo inference cases. + +### Added — `--for` shows a package.json `bin` / `main` entry and the file it names (#451) + +- `--for` manifest entry points now read the top-level `bin` and `main` fields of `package.json`, and resolve each to the file it names (its first exported function, else the file's module body); a bin is named as npm installs it (`@acme/tool` installs `tool`), and a string `bin` in a package with no `name` is no entry, as npm drops it; a `main` that names no exact file is looked up as Node loads it (`.js`, `.ts`, `.mjs`, `.cjs`, then `index.js` / `index.ts` in that directory; `"lib/"` with a trailing slash is the directory), while a `bin` names its file exactly; package.json is read for a `.js` or `.ts` row; a `package.json` inside `node_modules` is a dependency's and is not read. Gate coverage in `test/epmanifestcheck.sh` (issue #451). Thanks to @llvm-x86 for the reader and the fixtures. +- The Python, Cargo and package.json readers share one format table; Cargo and package.json targets go through one file-path resolver, and the pyproject and Cargo readers share one set of TOML helpers (headers, keys, string values, multi-line strings, so a pyproject `description = """` that spells a table is a value); a Cargo or package.json group is a manifest key, so no code is scanned for it as a loader literal, and the legend says so. + +### Fixed — a shell test script named `test-*.sh` or `*-test.sh` reads as a test (#466, #472, thanks @lennix1337) + +`isTestPath`, the test-file question every verb shares, did not read `test-document-remind.sh` as a test, so `--for` ranked the test sibling beside the script it tests and `--ignore-tests` kept it. @lennix1337 found it in #466 and sent the fix in #472; the `src/filter.h` change lands under their authorship, and the narrowing below is ours. + +A false match here hides real source from every verb, and the bare hyphen forms (`test-*`, `*-test.*`, `*-spec.*`) also matched shipped files such as `pod-spec.ts`, `ab-test.ts`, `self-test.c`, `test-and-set.c` and zone.js's `async-test.ts`. So the hyphen forms are an allowlist: `test-*` and `*-test.*` apply to shell and batch scripts (`.sh .bash .zsh .ps1 .bat`), and `.e2e-spec.` and `.perf-spec.` apply in any extension. Of the 102 corpus code files the bare forms newly read as tests, the allowlist keeps 73, including every `.e2e-spec.` and `.perf-spec.` file and the shell scripts; the other 29 keep their previous reading as source. That includes real test files such as a bare `foo-spec.ts`, which stay visible: a missed test only stays on screen, a false test hides source. Underscore forms (`ab_test.ts`) already read as tests before this change and are untouched. Gate: `test/testpathhyphencheck.sh`. + +The mention-anchoring half of #472 is not part of this change: FOR-FILE-ANCHOR already anchors the bare filename and stem cases. + +### Fixed — an aliased Python `import a.b as c` is a file edge again (thanks @jflowers, #478) + +`import pkg.mod as pm` recorded the Include target `pkg.mod as pm`, the whole aliased clause, and no resolver finds a module spelled that way. So every aliased plain import added nothing to the file graph: `--deps` listed the target with no edge behind it, `--arch` Ca/Ce, cycles and `--impact` never saw the dependency, and a bare call that only the import could disambiguate was declined (`declined_calls=`) instead of bound. The target is now the module itself (`pkg.mod`), the same one `import pkg.mod` records; the alias still binds `pm` as before. `from a.b import x as y` was never affected. A multi-clause `import a as b, c` still records only its first clause as an Include, as every multi-clause import does. + +### Fixed — Python file edges: stdlib-named siblings, src layouts, and directories the crawl denylist removes (#478) + +Reported by @jflowers, who built an `ast` ground truth of pip's whole import graph and named the causes. + +Inside a package, an absolute `import logging` no longer binds a sibling `logging.py`, in the file graph or the call graph; Python 3 has no implicit relative import. (A stray `__init__.py` at the repo root makes the root itself a package and does not trigger this: its `a.py -> logging.py` edge stays.) A plain script directory keeps its sibling, because it is `sys.path[0]` when the script runs. An absolute import now also resolves under a `src` directory beside the importer or any ancestor, and under the source root of the importer's own package chain (`lib/mypkg/a.py` importing `mypkg.b`). Those layout roots are a fallback: the repo root and a plain script directory are probed first and win over a namesake under `src`, and two layout roots that name one module degrade to no edge (the base resolver had no layout roots, so this is the new rule). A `src` that holds an `__init__.py`, or sits inside a package, is a package and not a source root. A stdlib-named first segment (`logging`, `json`, from the CPython 3.13 `sys.stdlib_module_names`) does not bind through a layout root, because a repo file there shadows the stdlib only when that directory comes first on the path; a root that holds the stdlib itself (CPython's `Lib/`) is exempt. A package that imports ITSELF is decided at its own chain root first, with no stdlib guard and no repo-root namesake in the way: `src/email/sender.py` with `from email.templates import t` binds `src/email/templates.py`, and `src/foo/b.py` beside a root `foo/` binds `src/foo`. Any other importer of that name (`src/api/routes.py`, `tests/`) stays unresolved, because the run mode decides whether the stdlib or `src/` comes first on the path and the resolver cannot know it. `__main__` is guarded like a stdlib name. A file outside those shapes importing a package under a root that is not `src` stays unresolved. In a multi-root run, the call graph refuses every hit in a root other than the importer's own (the file graph always did), so a root that happens to be labelled `src` cannot bind another root's importer. The one base-true edge this also removes: with `mypkg` and `tests` as two roots, `import mypkg; mypkg.ff()` from `tests/` goes from 1 caller to 0, matching the file graph. Over `ast` truth, exact directories Ca/Ce: textual 86/3 to 98/98, flake8 62/25 to 100/100, pydantic 97/97 to 100/100 with 52 false edges gone. + +Limits. A script inside a package (`pkg/__init__.py` beside `pkg/tool.py`, run as `python pkg/tool.py`, with `import helper`) puts its own directory first on `sys.path`, but the import resolver cannot tell a script from a module, so it loses that true sibling edge with no hedge in the answer: in one fixture `--callers=run` goes from 2 callers to 0, `--impact=run` from 3 reached files to 0, and the importer count from 2 to 0. The commonest form is a `src/__init__.py` with `python src/train.py` doing `from utils import x` (`--impact` importers 1 to 0; `--callers` hedged). The corpus counted three of these over about 6,000 package files, but the corpus is library-heavy, so application repos with training or CLI scripts inside a package will lose more. A namespace package (no `__init__.py`) cannot be told from a script directory and keeps its sibling edge. The older suffix binding is unchanged: a plain `src/logging.py` or a unique `tools/logging.py` still binds `import logging; logging.info()` as a firm call edge from `pkg/`, `tools/` and `tests/` callers (3 callers in one fixture) while `--arch` and `--deps` show no edge. A non-self importer of a stdlib-named package under `src/` (`src/api/routes.py` doing `from email.sender import send`) is dropped from the file graph, and the answer does not say so. + +`--skipped` now rows the directories counted in `pruned_dirs=` (up to its 500-row ceiling per class; `rows_capped="1"` says a cut and the count stays exact): `why="pruned-package"` when a build-output name (`build`, `dist`, `out`, `target`, `captures`) holds its own `__init__.py`, else `why="pruned-dir"`. `--arch` and `--deps` carry `` naming the real packages, present only when one was pruned, up to 8 with `count=` and `next="--skipped"` for the rest. + +### Changed — an over-budget `--for` page sheds optional row attributes instead of missing the budget (#520) + +When a `--for --token-budget=N --limit=…` page still exceeds `N` at its one-row floor, the row drops `cx=`, `ccx=`, `layer=` and its own `next=`, then `sc=` if needed, and the root says `attrs_shed="1"` (defined in the page's legend). The row and what names the answer (`n=`, `p=`, `l=`, `r=`, the signature) always ship; a page that already fits is byte-identical. + +### Fixed — the router skill no longer tells agents that PHP, Kotlin or Elixir are unsupported, and `--help` lists Clojure (#523, thanks @gucliti) + +Fixes #521, reported by @gucliti. `skills/ripwire-router/SKILL.md` said ripwire parses 15 languages "— nothing else" and +named PHP and Kotlin as examples of languages that get a thin or empty map. `ripwire --help` lists 27, so an agent that +read the skill on a PHP, Kotlin, Lua, Elixir, Dart, Clojure, GDScript, TOML, YAML or Markdown repo was sent to plain `rg` +exactly where ripwire works. @gucliti measured it on an Elixir/OTP project: `--skipped` showed all 126 `.ex` files +indexed (3185 symbols), `--for` returned ranked symbols with correct call edges, and `--recall` ranked the repo's ADR docs +by section. + +The "Wrong tool for the job" paragraph now names `--help`'s Languages line as the source of truth, lists the families +compactly, and uses languages that really are unindexed as its examples (Haskell, OCaml, Scala, Zig); the map's `unindexed=` +header names the skipped extensions. `docs/LOCALS_INDEXING.md` lost a stale "all 14 languages" count. `--help` itself had +drifted: Clojure (`.clj/.cljs/.cljc/.bb`) has been indexed since September but was missing from its Languages line. It is +listed now. + +New gate `test/skilllangdriftcheck.sh` checks both directions. It fails if `--help`'s Languages line and the crawler's +extension table (`src/ingest_crawl.h`) disagree on a language. It also fails if a skill, doc or hook calls a listed +language unsupported ("another language (PHP, ...)", "PHP is not supported", "no PHP support", "ripwire does not yet +parse PHP", "won't index", "Unsupported: ...") or restates a closed list ("nothing else", "and nothing more", "supports +only ...") that omits one. A closed list is judged on its own run of names, not on the paragraph around it. It is red on +the old router paragraph and on eleven reworded claims, and green on the new paragraph. A fake doc that calls Haskell, +OCaml, Scala and Zig unsupported passes, and so do ordinary sentences such as "files in an unsupported language are +counted in unindexed=". A per-feature sentence that only looks like a claim ("Per-language rules cover C++, Python, Go, +Rust; no other language has the gate") is exempted with `` on its line. Limit: it reads +these prose patterns, not every possible wording; "X and Y only." and "for a PHP repo, use rg" still pass. + +### Changed — warm `--deps` and `--for` on large trees use 1.5-3x less CPU (#542) + +The by-name call resolver asked, for every candidate definition of a called name, facts that depend only on the call: the receiver's root name, whether it is `this`/`super`, the caller's class, and for JS/TS the `require()` alias and the path it joins to. They are now read once per call and tested per candidate, and the "is this owner a class" table is one byte per symbol instead of a lookup. Warm runs (cache hit) measured with user CPU (instructions retired in brackets): vscode `--deps` -67 % (-70 %), cockroach `--for` -57 % (-60 %), envoy `--deps` -35 % (-36 %). Cold runs (no cache) gain less: instructions -14 % on vscode, -12 % on cockroach and -4 % on envoy. Peak footprint is unchanged in each of those runs. The new storage is one byte per symbol plus one small per-call struct. The measurement is one machine and three repos; wall time was noisy on a shared machine, so the figures above are CPU and instruction counts, not wall time. + +Answers are unchanged where it was checked: 321 in-sample `--for` calls and 12 base-versus-new output pairs on those three repos (9 warm, 3 cold) are byte-identical, as are 92 caller and callee outputs on a crafted Python, JS, TS, Go and Java tree. A module alias is proved by its own call's `require()` path, never by a neighbouring call's (a new case in `test/tsimportprecisecheck.sh`). + +### Changed — a `--for` task with no table or registration word still reaches a registration row + +The table block that ends a `--for` answer (``) used to need a word such as register, table, default or handler in the task. A bare subject (`new_boxed_executor for HashJoinNode`, `light.turn_on hue async_turn_on`) has none, so the block never ran. Now such a task runs the same walk over its backticked spans and identifier-shaped words, but lists only strong rows: an argument of a registration call (`reg=`), a top-level macro or call row (`tbl="item"`), or a row of a macro table (`for_all_params! {`, `$macro! {`). A registration counts only when the site is a direct argument of the call (`register(handler)`; not `register(wrap(handler))`, `register(table[handler])` or a site inside an object literal). A dict, list, `__all__`, `#define` or const-array row, and a site that is no row, is weak: counted in `weak=N`, never listed; with no strong row the answer is unchanged. A task with no identifier-shaped word and no backticked span scans nothing, and neither does a no-cue anchor that is one plain lower-case word (`register`, `open`, `index`: no `_`, `::`, `.`, digit or capital, backticked or not), and a walk that lists no strong row adds no `` note either. The legend clause that says this rides only on such an answer. + +The callee names that count as a registration are narrower: `use` is no adding verb (it is the prefix of hooks such as `useRouter`), the noun after an adding verb must be the name's last word (`addEventHandler` is one, `addCommandLineOption` is not), and `reg` is a verb only as the last word or right before route, command or handler (`async_reg`, `reg_handler`; not `reg_value` or `reg_field`, a hardware register). The table legend is shorter: it no longer lists the vocabulary or the ranking of generated files, says the registration call is looked for 4 lines and 512 bytes above the site, and the `tail=` sentence rides only beside a `tail=`. + +Gate `forregrowcheck` (N23, N24 for the last two rules) covers bare subjects on Rust, Python, Go and TypeScript fixtures, a weak row beside a strong one, weak-only rows of four kinds (each answer byte-identical with the block switched off), the no-call shapes, and the vocabulary cases above with their twins. + +What it does NOT do: it does not list a weak row for a task with no cue word, even when that row is the answer; ask with a table or registration word for those. A callee's name is still no proof the call registers. + +### Added — a `--for` task that names a `--flag` reaches the call that defines it and the default it takes + +A task naming a flag (`kubelet --max-pods`, `How does the kubelet use the --max-pods flag?`; a `--` token whose name holds a hyphen, or a `--` name of 5 bytes or more right before the word flag or option) now gets a `` when no earlier literal of the task has a source site. Its rows are the sites where the name is a whole quoted string (`"max-pods"`, `"--max-pods"`; its definition is one of them, an invocation or a lookup another); a name with no inner hyphen (`--compress`) is a site only in its dashed form (`'--compress'`) or as an argument of a flag-definition call (`fs.Bool("verbose", ...)` or a TCLAP `ValueArg name(...)`), never a dict value, an `__all__` entry or an error message. Any other source hit (usage text, a longer flag such as `max-pods-per-core`) is counted in `mention=`. On the first such row that has one, `def=` is the default of a flag-definition call, read from the text: a `default=`, `default_value=` or boost `default_value(10)` argument, else the default slot of a Go flag, pflag or cobra call (the argument after the name, two after it for the `P` form such as `StringVarP`), never a usage sentence. A call that is no such flag call (an `exec.Command(...)` invocation, a nushell `.named("end-date", SyntaxShape::String, ...)`, an argparse alias `"-v"`) has no `def=`. When `def=` names a field (`c.MaxPods`), `` rows list the constant writes of that field (`obj.MaxPods = 110`, `MaxPods: 110`, never a copy `out.MaxPods = in.MaxPods`) in code that names the type of the receiver in `def=` (its parameter type, a package variable's declared type or its own name; a name match, no type check; a receiver with no type found lists none), functions and files named for defaults first, at most 2, with `writes=` counting all of them. Measured on kubernetes-05 (stored and both natural wordings): the `max-pods` registration row prints with `def="c.MaxPods"`, and the first `` row is the 110 default in `SetDefaults_KubeletConfiguration`. Gate `forliteralcheck` F1-F28 covers Go pflag (plain and shorthand), Python argparse and click, a flag with no default, a default that is a call, a C++ TCLAP argument, an alias after the name, a nushell `.named`, an invocation ranked before the registration, a no-hyphen name met as a dict value, a same-named field of another struct, and near misses (docs only, no site, a Rust clap field, no flag word, no flag). + +What it does NOT do: it does not read a flag that has no quoted name (a Rust clap field, gflags `DEFINE_int32(name, ...)`), it does not follow a default through a variable or a call, and `def=` is the text of the argument, not the value in effect. + +### Changed — a `--for` excerpt shows the match arm or case that holds the literal; `body="above"` only when the body was printed + +An `` excerpt in the `` block shows one window around each site, from the line before it. When that window leaves out the head of the match arm or case holding the site, the window now starts at that head, at most 12 lines up, and the excerpt says `arm="1"` (a legend clause rides only with it). Arms are an allowlist: a Rust, C# or PHP `Pattern => {` with balanced parens (never a JavaScript `x => {` callback), a `case` or `default` label (C, C++, Java, JavaScript, TypeScript, Go, C#, Swift, Python `match`; a `default: {` key of a JavaScript object literal, which heads no `switch`, is not one), a Python `elif` or `else:`, a Ruby `when`, `elsif` or `else`, and an `else if` or `else` branch in C-family and Go code. Openers the window already shows (an `else {` right above the site) are passed over to the arm above them; an `if`, a loop or a function above the window ends the walk with the old window. Measured on risingwave-02 (all three wordings): the window of `handle_alter_streaming_rate_limit` now starts at the `(Table, Source) =>` arm and holds the field read that picks the throttled id. + +A `` row said `body="above"` when its owner's line, path and name appeared anywhere after the ranked rows, including a hop row (``) whose body the fold skipped. It now says so only when the answer printed the owner's whole ``; a hop row, a fold skip or a truncated `` that stops before the site is no body above, and the block serves its own excerpt. Gate `forliteralcheck` E1-E12 and B1-B3. + +### Added — `--for` prints the re-export hops to a definition the task names + +A question like "where is `ShellError`, used in ls.rs, defined?" got the definition and nothing between it and the use site: +the `pub use` / `export … from` statements a reader walks through were never rows. A `--for` answer now ends with a +`` block when a task word names an in-tree definition that something re-exports: one `` row, then each +re-export statement nearest the definition first (``), and the task-named file's own import +(``). Rust: `pub use path::Item`, `pub use path::{A, B}`, a module re-export (`pub use other::module`) and a glob +(`pub use path::*`, marked `glob="1"`: hedged), with paths resolved through `crate` / `self` / `super`, child modules and +Cargo package names, so same-named items in two modules keep their module identity (`cands="N"` when two manifests spell +one crate name). TypeScript/JavaScript: `export { a } from`, `export { a as b } from` (`as=`), `export * from` +(`glob="1"`), recorded by the TS/TSX/JS tags queries through the shared binds channel as a new `LocalBindKind::ReExport` +(parser version bump). Rows only: nothing here is a call edge. Not followed: a Rust `pub use a as b` rename (counted +`as_skipped=`; help-wanted #445), `export * as ns from`, and items defined inside an inline `mod { }`. A `pub use` inside an inline `mod { }` prints its full module path; `src/main.rs` beside a lib.rs and `src/bin/**` are binary crates, never the library's modules; a path through a name the file imports reads through that import (never a same-named workspace crate), and only an import of the same module counts — a child `mod` inherits no parent `use`, and a `use` inside a function body binds nothing outside it; `pub(crate) use` is labelled `vis="restricted"`; a walk that stops at 256 spellings says `capped="1"` beside `total=`. Under a token budget (`--token-budget=N`) the rows are not printed (they sit outside `est_tokens`): a one-line `` names how many definitions carry a chain. `RIPWIRE_NO_REEXPORT_HOPS` +turns it off. Gate: `test/reexportcheck.sh`. + +### Changed — `--for` anchor choice: a test-only anchor among several no longer commits the route; a path word picks one namesake + +- `--for` and the MCP `for` verb: a name-exact anchor that has two or more definitions, none of them source (test, doc, data, CI or vendored copies), no longer commits the route to whichever copy was found first. The words rank instead (`route="subtoken+body…; anchor has no source definition (…), not committed: ranked as words; hint anchors: …"`), the anchors stay in `route=` as a hint, and confidence reads low as before. A name with ONE such definition (a test helper that is the only thing by that name, however often the task repeats it) or with none (an env var served by the literal block) keeps its name-exact route. +- The query-mention anchor: an identifier the task names with several definitions (`syncProxyRules` in iptables/, ipvs/, nftables/, winkernel/) is lifted at the one whose file stem or parent directory another task word names (`kube-proxy nftables syncProxyRules`). A word that names several of them, two words that name two different ones, or a word that names only a C-family prototype while the definition sits elsewhere choose none, and the ranking is as before. A dot that ends a sentence (`… in nftables.`) does not make the word a qualifier. + +### Fixed — `--arch`, `--deps`, `--cochange`, `--impact` and `--export=cc.json` stop reporting coupling from edges the index never had + +Go, Java, Kotlin, C#, PHP, Swift, Dart, GDScript and Clojure draw almost no file-to-file edge in this build (Go only across a +`go.mod` replace), so `--arch` printed Ca = Ce = 0 and `zone="pain"` for every typed module of those languages, a design finding made +only of missing data. A module holding a file in such a language now reads `zone="unknown" no_file_edges=LANGS`: with no edge observed +it has no `ca`/`ce`/`I`/`D`/`reachable`, with one it keeps `ca`/`ce` as floors and has no `I`/`D`/`leaf`. `` counts it in +`zone_unknown=`. Every no-edge file, with or without an observed edge, is left out of both system denominators (`dep_files=`, `propagation_cost`), so one +real edge elsewhere cannot bring back a `shape="horizontal"` over files that could not have an edge, and `masked_files=N` says how many +were left out; with no edge left among the other files, `ccd`/`nccd`/`shape` and `propagation_cost` are absent. `--cochange-groups` puts +`no_file_edges=` and `unknown_pairs=N` on its root when pairs with such a side are in no group, so `groups="0"` no longer reads as a clean answer. `--deps` drops `afferent=`/`instab=`/`transitive=` +on those files, and `--report` no longer calls such a tree acyclic. Roots carry `no_file_edges= unmeasured_files= graph_partial="1"`, +and `--arch` says on stderr that `violations=` is a floor. The same missing data used to print elsewhere as a measured zero, and no +longer does: `--cochange` (and the MCP `cochange`, and `--pr-context`) put `no_file_edges=` where `surprising="1"` claimed hidden +coupling for a Go pair with a real import; `--impact` (and the MCP `impact`) carries `no_file_edges=` on the root, because its +`importers=` is then a floor; `--export=cc.json` omits a zero `fan_in`/`fan_out` for such a file instead of writing it, and a nonzero one is a floor (the attribute descriptors say so). Which languages is the +`fileEdges` column in `src/langfacts.h`. Trees with no such file are byte-identical. Thanks to @jflowers, whose #478 led to this finding. + +### Fixed — `--for` follows TS/JS namespace re-exports, checks every segment of a written qualifier, and gives an unresolved name a fallback ladder + +- `--for` ``: a TS/JS namespace hop is now a row. `import * as X from './m'` followed by a local `export { X }` (or `export { X as Y }`), and `export * as X from './m'`, publish the whole module under `X`; the row says `ns="X"`. Behaviour change: `export * as ns from` used to be no row at all (the old legend said so); it is now an `ns=` row, and `test/reexportcheck.sh` arm T2 was rewritten with it. +- `--for` `` on a qualified subject (`Q.leaf`, `Q::leaf`): every segment of `Q` must be accounted for. Full proofs: the file stem or a dot-suffix of it (`Completions` for `ts.Completions.ts`), the path's own tail (`utils.legacy` for `src/utils/legacy.ts`), a package index file's directories (`utils` for `src/utils/index.ts`), the whole owner path, or a namespace hop that publishes the definition under `Q`. Three weaker proofs only hedge: a directory elsewhere on the path (`src/utils/legacy/old.ts` for `utils`, `q_by="dir"`), a consumer's own `import * as Q from …` (`q_by="alias"`), and a path proof the tree contradicts (a file does `import * as Q from M` and no such `M` reaches the definition, so a `tests/utils.ts` stem proves nothing; `q_by="path"`); a hedged definition is listed only when nothing proves `Q` fully. The definition carries `q=` and its proving chain prints first. When one definition proves `Q` fully, a same-named one that cannot is no longer listed, so such a task may print fewer definitions than before. When none does and a cue word asks where the name comes from (`where is utils.formatDate defined`), every chain still prints, marked `q_unmatched="1"`. A subject that is one code-shaped name (the whole task) now prints its chain without a cue word; a bare `Q.leaf` prints only chains with some proof. A `this.X` / `self.X` subject is a member access and gets no block. A namespace re-export cycle (`a.ts: export * as B from './b'`, `b.ts: export * as A from './a'`) now ends without reaching the spelling cap. +- `--for`: a name-exact route that resolved no definition (`reason="no_candidates"`, empty ``) now prints a `` block with a fallback ladder, each rung named: `leaf` (a dotted key's last segment, filtered by the same qualifier proof; exactly one fully proven definition is `exact="1"` and its body is printed, several are hedged with `cands=`, hedged-only proofs say `q_by="alias"`, `q_by="path"` or `q_by="dir"` and are never exact, a method or nested function gets only its owner-path proof, never its file's, none is `q_unmatched="1"`; C++ stores only the innermost owner, so a namespace segment stays unproven), `substring` (definition names containing the key, never exact) and `literal` (declaration-shaped source lines the index does not store as definitions: `using X =`, `typedef … X;`, `type X`, `#define X`, `… as X` on an import line, `const`/`let`/`var`/`static X`, `X =` at column 0; comment lines are never rows). Every block carries `next=`; rows are capped at 6 per rung with `total=`, a printed body is cut past 8192 bytes with `truncated="1"`, and `floor="1"` marks a literal scan that could not read every file. The rungs can all be empty (`total="0"`). Only XML answers on a single root carry the block: `--json`, `--format=candidates` and multi-root answers print neither the block nor a marker; under `--token-budget` a held marker prints instead. A `this.X` / `self.X` subject gets no ladder. `RIPWIRE_NO_SHORT_KEY=1` turns it off. +- Measured on the practice questions this targets: `--for=Completions.getCompletionsAtPosition` on TypeScript went from a 1.3 KB answer with no definition to the namespace hop and the definition's body; `--for=ColumnPtr` (ClickHouse) and `--for=ExprType` (RisingWave) now name the `using` / `use … as` lines that declare them. Gate: `test/barrelshortkeycheck.sh`. + +### Fixed — C/C++ file edges: a quote include resolves at the include roots a build passes (#478, thanks @jflowers) + +A quote `#include "source/common/http/utility.h"` was probed only in the includer's own directory, so a tree that includes by root path had almost no file graph: Envoy resolved 289 of its 34,973 include edges, and `--deps` called it `shape="horizontal"`. On a miss in the includer's directory, a quote include is now probed at the repo root, at each `include/` directory beside the includer's directory or an ancestor (the same module-root rule the Python src-layout fix uses), and at a root `src/`; an `include/` or `src/` the tree does not hold is never probed, so `"../a.h"` cannot reach a root `a.h` through a directory that is not there. Each root is a shape a build file in the test corpus passes as `-I` (the repo root: Envoy's Bazel, htop's automake, tmux's `-iquote .`; `include/`: waybar's meson, CPython's `Modules/_hacl`; root `src/`: ninja's fuzzer build); `source/` and `lib/` bound nothing and are not probed. The includer's own directory still wins, the hit must be unique across the roots, and a name two roots both hold draws no edge and is counted in `imports_unresolved=` with `graph_partial="1"` on `--deps` and `--arch` (and `imports_unresolved=` on `--impact`). Angle includes are unchanged, so `` never binds a sibling `string.h`. + +Measured with @jflowers' ground-truth method from #478 (exact afferent and efferent coupling per directory, and mean absolute error, against an independent truth): Envoy goes from 289 to 34,196 of 34,973 static-truth edges, directories exact on Ca/Ce from 41 % / 1 % to 96 % / 92 %, MAE 9.62 to 0.21; htop, tmux and ninja reach 100 % / 100 % with MAE 0. A second oracle, the compiler's own `clang -E -H` with the `-I` flags read by hand from each build file, finds 0 false edges on htop, tmux, ninja and waybar, and a stratified hand sample of 30 Envoy edges finds 0. Envoy's health line now reads `shape="vertical"`; `--impact=source/common/common/assert.h` lists 614 importers where it listed 0, with its directory and test rollup. + +What it does NOT change: the call edges. The C/C++ call narrow (Rule 3's include evidence) keeps the includer-directory resolution, so no call edge moves. What does move is the declaration-to-definition proof a header-qualified selector widens through (`--defs=core/api.h:work`, and `--callers`, `--callees`, `--impact`, `--edit-check`, `--for` and MCP on the same selector): a bodyless declaration in `core/api.h` now folds into a definition whose file includes `core/api.h` by its root path, under the file graph's own rule (the includer's directory wins, and a name two roots hold proves nothing). Measured: on 61 graded C/C++ questions (Envoy, htop, tmux, ninja) 58 answers are byte-identical and 3 change only in file-graph counts, two `--impact` importer counts on tmux (149 to 157) and one `--defs` count on Envoy (79 to 78: `MainCommon::main`'s bodyless declaration now folds into its definition, because `main_common.cc` includes its header by root path); no graded item moves. With the root edges, Rule 3 bound about 2,060 more Envoy call sites without a hedge, and a hand read put about 300 of them on a wrong in-tree namesake (`std::format` on a local `fmt.h`, `absl::Span` on a local `class Span`). The two re-couple once external-qualified calls are vetoed. Still unresolved: the 2 % of Envoy edges written as a path suffix (`"utility.h"` for `source/common/http/utility.h`) and angle includes of in-tree headers; both need the build's own `-I` flags. + +### Changed — `ripwire wrap claude` routes edits through a ranged Read and Claude Code's native Edit + +Claude Code refuses a native Edit on a file it has not Read in the same session, so the pasted wiring line "apply a whole-symbol edit without a whole-file Read" could not save a Read there: an agent that followed it still had to Read the whole file before it could edit, and in our own usage the ripwire edit verbs were never called from Claude Code (0 calls against thousands of native edits over five weeks). For `ripwire wrap claude` the line now says to take the symbol's line range from ripwire, Read only that range (`offset`/`limit`), then use the native Edit, and the MCP command it prints now passes `--mcp-tools=no-edit`, a new profile that lists every tool except `replace_symbol_body`, `insert_before_symbol` and `insert_after_symbol` (about 4 KB of tools/list, roughly 1,000 tokens per session; `--mcp-tools=full` restores them). The estimate behind the change is from our own usage and a size sweep (a ranged Read plus native Edit costs about a third of a whole-file Read plus Edit at 6 KB, and the gap widens with file size); it is an estimate, not a controlled agent run. Every other agent's `wrap` output is byte-identical, and the edit verbs themselves, their CLI flags and their docs are unchanged: codex, opencode, Gemini CLI and other agents without a read-before-edit rule keep the "no whole-file Read" line and the edit tools. + +### Fixed — C/C++ call edges read the include roots, and a call qualified by a namespace the tree never defines stays hedged (#478, thanks @jflowers) + +The C-ROOT-INCLUDE change (#524) gave C/C++ trees their include-root file edges but kept the call narrow on the includer-directory closure, because coupling them bound about 300 Envoy calls to a wrong in-tree namesake without a hedge. The call narrow's include-file rule (Rule 3) now reads the root edges, behind two vetoes. First, a C/C++ call qualified by a name the tree never defines (no class, struct, alias or namespace of that name in any definition's scope or member path) takes no include proof: `absl::Span(…)` no longer binds Envoy's own `class Span`, and `fmt::ptr(…)` no longer binds `OptRef::ptr`. The pick keeps the existing `via="name"` hedge. Second, a `std::f(…)` call must name a member of namespace std itself, so `std::format(…)` never binds the `format` member of a `std::formatter<…>` specialization. Member calls keep the includer-directory closure, both for the receiver's class and for the include-file hedge, so a header only the root probe reaches no longer spreads `factory->isTerminalFilterByProto()` over its three factory classes. A bare call whose file declares `using ext::make;` with `ext` foreign is hedged the same way as `ext::make(…)`. A header that holds a prototype and an `#else` `static inline` stub of one C function no longer pins the stub over the real definition in a `.c` file (htop `Demangle_demangle`); the call keeps its split. + +Measured on Envoy (site dumps, hand read; the method is the one #478's Ca/Ce measure motivated): 2,819 call sites gain an unhedged bind against the decoupled build, and a hand sample of 30 of them is 29 right. Without the vetoes the same coupling was 18 / 30, and all 156 calls qualified by `absl`, `fmt`, `nostd`, `grpc`, `testing` and the like were wrong; the vetoes remove all 156. On htop, `Machine_new → Machine_init` gains on nine platforms through the root `Machine.h`, all right; ninja is unchanged. On ClickHouse, 122 sites gain (about 121 right), and 183 wrong binds drop: `std::find(…)` and `std::string_view(…)` had bound a polyfill `namespace std { struct string_view }` that only one file includes. Answers, against main with the same arguments: of 61 graded C/C++ questions (Envoy, htop, tmux, ninja), 50 are byte-identical; the 9 graded ones that change keep their tier and their credited items, and none loses one. Of the 60 stored practice questions, 59 are byte-identical and one (ClickHouse) moves only a counter. + +What it does NOT fix: a qualifier the tree does define but that aliases an external namespace (Envoy's `namespace Protobuf = google::protobuf`, so `Protobuf::Map` binds an in-tree `Map`): about 37 Envoy sites, because C++ namespace aliases are not recorded yet (so `namespace M = mine; M::f()` also stays hedged). It also does not fix the right typed-receiver binds the second veto gives up (about 44 on Envoy), a `using namespace ext;` directive (a bare call under it can still bind an in-tree namesake), or a foreign root over an in-tree inner segment (`ext::ns::f()`, where only `ns` is read). + +### Added — C++ pointers to member functions and C class tables with macro-cast receivers are joined to the calls through their fields (`--callers`, `--callees`, `--for`) + +A C++ pointer to a member function stored in a struct field (`{ "browse", …, &NinjaMain::ToolBrowse }` in a table of a struct +this file defines, a field assignment, an argument) is now a value reference to that class's method — its definition with a +body, else its declaration, never a same-named method of another class — and a call through a member pointer held in a field, +`( ninja.*options.tool->func )( … )`, is a call through that field (typed by the root's declared struct, as `x->f( … )` is). In +C, a designated initializer nested in an untyped braced member (`.super = { .writeField = f }` in a `ProcessClass` literal) +keys the field path from the enclosing literal's type (`ProcessClass.super` is a `RowClass`), and a call whose receiver is a +function-like macro whose whole body is one pointer cast (`#define As_Row( x ) ( ( const RowClass* )( … ) )`, +`As_Row( this )->writeField( … )`) is typed by the cast's type; any other call receiver (`f( x )->g( … )`, a macro of another +shape) joins by the field name only (`via="slot-name"`, `~` on `--for`). Not joined (floors): `&a::Twin::Go` when the repo +defines two classes of that name (a symbol carries no namespace), a namespace function `&ns::f`, a member pointer in a plain +variable, `->*` and an inline member-pointer field declarator `R ( C::*f )( … )` (both parse as an ERROR node: use a typedef, as +ninja's `ToolFunc` does), table entries inside an `#ifdef` within the initializer list (an ERROR node), `std::function` members (a call result or lambda is no value), and an inherited method whose base is +declared outside the tree (the store is dropped, and the slot lists the targets it does have). A pointer to a virtual method is a +candidate SET: the declaring class's definition and every override in a class derived from the pointer's class (a non-virtual +method a derived class hides is listed too: a hedge, never a pin), and a pure virtual's bodyless declaration is never a target when +an override has one (with no in-tree override, the pure declaration is the target). A template's dependent base written as a template parameter (`template struct M : B`) names no in-tree class, even one called `B`. An inherited or CRTP-base method (`&D::inh` defined in a base) names the nearest base's definition (two bases +at one level: none). An overloaded name lists every overload, so `targets=` counts definitions, not stored values. A qualified +spelling must match the namespace the in-tree class sits in (`&grpc::Server::start` is no `mine::Server`; neither is a bare +`&Server::start` under `using grpc::Server;` or `using namespace grpc;` written outside `mine`; a bare `&Server::start` written INSIDE `namespace grpc { … }`, where an external `grpc::Server` hides an in-tree GLOBAL `Server`, still binds the global class: a floor). A call site whose typed +tier hides name-only stores says `slot_name_also=K` on its ``, and `--for` `slot_fields=` keeps the fields a caller calls +through when its cap of 4 cuts. +Nothing here is a proven call: count= and reaches= never move. + +### Fixed — `--deps` stops calling every deep graph `tangled` + +`` was read off `nccd` alone, and `nccd` passes 2 on any graph whose average file reaches a few hundred +others, cycle or not: with true include edges injected, Envoy's verdict went from `horizontal` (`nccd="0.09"`) to `tangled` +(`nccd="16.37"`) while it had three cycles of 2, 3 and 6 files among 7,786 measured files. `tangled` now means what it +says: at least 1 in 20 of `dep_files=` sits inside an include cycle, and the number behind it rides beside it as +`cyclic_files=` (absent at 0). Otherwise `nccd` says how deep: `horizontal` below 1, `vertical` from 1. That Envoy graph now +reads `vertical cyclic_files="11"`; cpython (314 of 3,544 files in cycles) and vscode (1,277 of 15,574) stay `tangled`. A +cycle list longer than its 20 rows now lists the largest first and says so: ``; `--limit` above 20 lists more. The 1-in-20 line is a heuristic, like +the `nccd` one it replaces. + +### Fixed — `--recall` no longer hides hand-written reference pages as "generated" + +`--recall` ranks a generated document (a command capture, a test golden, an API dump) below every other match. Its size-and-fences test (at least 5x the median document and at least 35 % of lines inside ``` fences) also caught long hand-written reference pages full of short examples, so a page could miss even when its own heading was the query. The test now also requires that the document has almost no prose of its own: under 15 % of its bytes on non-blank, non-heading lines outside fences. Over the in-sample documentation repos, every compiler golden and fence-dump fixture the old test caught stays demoted (their prose share is 0.00), and the 74 hand-written guides, reference pages and blog posts it caught (prose share 0.31 or more) are ranked by relevance again. The fence count now follows CommonMark's closing rule: only a line with the same character (backtick or tilde) and at least as many of them closes a fence, so a four-backtick fence that quotes a ```bash block stays one fence and a capture of nested or cut-short markdown output stays demoted. Stated limit: a generated document whose output is mostly tables or unfenced text, such as a table-style OpenAPI markdown export, has prose-shaped lines and is not demoted by this test; only a marker phrase demotes it. The marker test (a document that says it is generated) is unchanged. The header now names the demoted documents too: `generated_demoted_paths="a,b"` lists up to five, best-first, even when they fall past `--top-k`, and `generated_demoted_named=K` appears when it lists fewer than the tally; a space, `%`, `"` or `,` in a path is percent-encoded. Thanks to @muayyad-alsadi, whose #409 and #428 started this documentation track. + +### Changed — `--recall` serves the best-matching part of an HTML page it has to cut, not its start + +A recalled document with no headings to cut by (an HTML page's text, a heading-less note) that does not fit its share of the budget used to keep its first bytes: on an HTML page, that is the navigation list and the table of contents. It now keeps the run of whole lines, of the same size, that carries the most query evidence (each line scores the query words it holds, rarer words in this document weighing more; the earliest of equal runs wins, and a document whose start scores best, or ties the best, still serves its start: the run must out-score the whole lines of the head cut as it would be served). The separator line says so: `[truncated: K of N bytes; served bytes LO-HI, best query-term window]`, where LO-HI is the byte range of the served text (the converted text for an HTML page), and the body opens with a `…` line; on an HTML page the page's title (its first line) sits above that line, so a window from deep in a long page still names its page (the title is not part of LO-HI). A window that reaches the end of the page keeps the page's last line. A window that starts inside a code fence repeats the fence's opening line (`fence_reopened`); one that ends inside a fence or table keeps its own last line and closes the fence (`fence_closed`), rather than dropping back behind the line it was chosen for; the repeated opening line is charged to the window before its lines are scored, so the lines scored are the lines served; an opening line over 64 bytes is repeated as its bare fence run plus the first word of its info string (or not at all when the run alone is over 64 bytes), and a table whose rows start with a backtick is not a fence. + +Measured on the same 48-question docs set as the 6-doc recall default change: development questions fully answered went from 13 to 16 and held-out from 4 to 5, all on HTML pages (8 development and 4 held-out HTML questions; Markdown and reStructuredText answers are byte-identical). Mean answer size on the development set rose 0.7 %, from the marker, the `…` line and the title line. + +### Changed — `--recall` and MCP `memory_recall` serve 6 docs by default, not 8 + +With no `--top-k` (CLI) or `top_k` (MCP), recall now serves the 6 best documents instead of 8. The 8000-token body ceiling is unchanged and is still divided across the served documents, so each one gets a bigger share: the usual miss at 8 was the right file with its answering section past that file's share. The default is now one constant shared by both front doors, and the capped note, `--help` and the MCP tool description name it. + +Measured on a 48-question docs set (34 development, 14 held out; Markdown sites, Markdown references, reStructuredText and HTML from six open-source projects), graded by a mechanical anchor rule: development questions fully answered went from 11 to 13 and held-out from 3 to 4; held-out questions with none of the answer printed went from 4 to 1 (development: 11, unchanged). Mean answer size on the development set rose 2.5 % (14.2 KB to 14.6 KB). One question got worse: its second answer sat in the 8th-ranked document, a release-notes page that is no longer served by default. Ask with `--top-k=8` to get the old behaviour. + +### Fixed — a Rust prelude call or a macro with no in-tree binding no longer binds to a same-named definition + +`Ok(x)`, `Err(e)`, `Some(x)` and `drop(x)` with no binding of the name written in the caller's file (a definition there, or a `use` that names it) used to bind to whatever the tree defined under that name: a serde serializer's associated `type Ok = ();` (also in the caller's own file), a `type Err` alias, a function nested in another function and reached through `use super::*;`. Only a definition a bare call can name now cancels that: a function in scope (a nested one only inside the function that holds it) or a tuple struct, never an associated item, a type alias, an enum or a braced struct. A macro invocation (`assert!`, `json!`, `format!`, `write!`, `panic!`) bound by name to a function, module or type of the same name. Those calls are now counted as calls outside the tree (`external=` in the map header) and carry no edge. A macro invocation binds to an indexed `macro_rules!` of its name, or to a `#[proc_macro]` function its file's `use` names (a procedural macro such as `sql!`; a plain or nested function of the name is no macro); a user's own `Ok` type reached through a `use`, a same-file definition, and a local `macro_rules! assert` called in its own file keep their edges. A `macro_rules!` without `#[macro_export]` is in scope only where Rust puts it: its own file, its child modules (a crate root named by Cargo's `[lib] path` owns its directory like `lib.rs`), the parent module's scope when the module is declared `#[macro_use]`, or where a `use` names it; elsewhere, including a `src/bin/` file or a `main.rs` beside the library root, a call of that name is the standard or an outside crate's macro. Not read yet: a module declared with `#[path = ..]` (its calls of a parent's macro are counted `external=`), and source order inside a module. A Clojure `defmacro` called as `(name ..)` keeps its edge. + +Five Rust member names joined the standard-member gate after a hand sample of their unproven calls: `.expect()`, `.into()`, `.join()`, `.lock()` and `.status()` (0 of 40 sampled calls on Zed bound to the right definition; 2 of 20 on Nushell). Such a call with no receiver proof is now declined and counted in `declined_calls=`; a receiver typed as an in-tree class, or an impl's own `self.expect()`, keeps its edge. + +Rust macro invocations now carry `role="macro"` instead of `role="call"`; the cache is re-parsed once (parser version 297). + +### Added — `--for` on an add-a-new-X task names X's family: the existing siblings a new one joins + +A task like "add a new builtin scalar SQL function", "I need to write a scheduler plugin that lives in the tree" or "I want to add a new dialect" now ends with an `` block before ``: how the existing X are grouped (`rule=`: implementors of one base or interface, the entries of one registry map or dispatch table, typed instances of one C/C++ struct type, items under one attribute macro or decorator, or classes whose names end in X in one directory), the shared `key=` (and, for the base and registry rules, `key_p=`, where it is defined), `matched=` (the words of the task found in `key=` or `scope=`: why this family won, and a family that matches few of them is a weak guess), the directory the family was narrowed to (`scope=`), its size (`n=`, test, sample, example and benchmark code excluded) and up to four members, the typical one first (nearest the median size). Go registries and C vtable instances are found even where the implementors list is empty (Go has no `implements`; a C `const MeterClass X_class` is not a subclass). The block is present when a family resolves, or, when none resolves, only if the scan skipped something that may have been it (``: `oversized=` counts registry candidates over 4000 lines that were not read, `unread=` the files the scan could not read; the same two attributes ride a resolved family when it skipped something); the answer before it is unchanged, a token budget holds it (`held="budget"`), and the MCP `for` tool prints the same element. New gate `test/forexemplarcheck.sh` pins one fixture per rule (C typed instances, Go and C++ registry maps, a Rust attribute macro, a TS interface, a Python subclass), the wordings that fire and the ones that stay silent. + +The same block now carries a ``: the files outside the members' own that mention at least half of the family, ranked by share — where existing siblings are registered or listed (a name constant list, a defaults list, an exhaustive `match`, a package `__init__` or barrel `index.ts`), each with `share=` (the distinct member names it mentions out of the distinct names, or with `by="stem"` the distinct file stems out of the distinct stems: a name or stem several members share counts once, so one shared word never credits every member), the line to copy (the first naming the typical member; an import or a comment only when no code line does) and its text, cut at 120 bytes with `cut="1"`. A touch list whose scan could not read some listing files (unreadable, or over 4 MiB now) says `floor="1" unread="K"` beside `total=`: the count is a floor, and a `total="0"` is not "none". Names are matched with snake, Camel, kebab and UPPER forms folded, plus each member's registered name and file stem. Test, docs, vendored, generated, frozen and baseline files are not read; more than five listing files print five rows with `total=`, `shown=` and `capped="1"`. On the kubernetes scheduler-plugin task it lists `apis/config/v1/default_plugins.go` and `plugins/names/names.go`; on the risingwave scalar-function task `scalar/mod.rs`, the frontend's `expr/pure.rs`, the optimizer's `strong.rs` and the binder's `builtin_scalar.rs`. + +The block's legend follows `--legend`: the short form by default (`--legend=compact`), the full prose with `--legend=full` and in the MCP `for` tool; both define every attribute. A decorator is matched whatever its case (`@Widget()` for a task about a widget), a function's parameter list is never read as registry entries, and the name-ending rule names classes only (a function named `execute_command` is not a sibling of a command), needs them in at least three files (one file of `*Options` interfaces is a type list) and not only in `.d.ts` declaration files, and, because names alone are the weakest grouping, prints `grouping="affix-only, weak: verify one m before copying"` on its own family line, so the row hedges itself without the legend. + +### Changed — the file graph's include closure is one flat store, built by a fold over strongly connected components + +The file graph's direct edges are now one flat offsets-plus-targets array (`FileAdjacency`), and the transitive include closure is a chunked hybrid store (`FileSetStore`: a presence bitmap and popcount rank per row, each chunk a sorted array or a bitset, whichever is smaller, identical rows shared by hash and verified). On vscode the closure is 3.74 MB where the sorted vectors it replaced were 32.5 MB; on a 100,000-file graph of mean closure 11,098 the process peaks at 580 MB where the rows alone in sorted vectors were 4.4 GB. Answers are byte-identical; `--deps`, `--arch=FILE` and the resolver read the same sets in the same order. + +It is built by one fold over the strongly connected components, sinks first: a component's row is the OR of its successors' rows plus their ids, taken back from the store into a single working row, so a file that imports 40,000 modules costs one row and not 40,000 (a first form of the fold held a bitset per imported module: 744 MB against 11 MB at 100,000 files). The fold asks the memory guard as it goes: over `--max-memory`, it stops, and a stopped fold is a failed build: every CLI verb exits 5 with one line, except the range form `--quality-delta=A..B`, which answers from the changed files with exit 6, disclosed, an edit whose write was already made, and the `--lsp` server (below); an MCP call is refused by name (the next call retries the build); and no `--quality-delta`, `--edit-check` or `quality_baseline` snapshot is scored or written from it. Nothing answers from a partial store, on the graph build and on the second fold `--deps` and `--arch=FILE` run. A write verb whose fold stops after its write is not refused: an edit (`replace_symbol_body`, `insert_before_symbol`, `insert_after_symbol`, the CLI edit verbs, each op of `--edit-plan --apply`) answers its receipt with `post_check` "not run" and the stop named, and `quality_baseline` answers what it wrote with `_index` "not rebuilt" and `_memory_stop`, so no answer tells an agent to retry a write it already made. `--lsp` answers a request whose fold stops with -32803 RequestFailed naming the stop and keeps serving. A payload past a 32-bit offset (8 GiB) is a disclosed stop, not a wrapped offset; that limit was modelled with a lowered bound, not run at 8 GiB. + +What it does NOT mean: no answer changes, and no edge is added. `RIPWIRE_TEST_FILESET_CHUNK_BITS` (test only) forces the chunk size on every fold. `bench/FILEGRAPH.md` has the measurements, the forms that lost, and the umbrella shape. `docs/LINEAGE.md` credits the classic closure and bitmap results the store rests on. + +### Fixed — a `--for` second-hop caller reached only through a by-name caller is hedged `via="name"` + +The `` element of a `--for` answer hedged a `d="2"` row only when that row's own call into a first-hop caller was matched by name. A row whose call was resolved but whose first-hop caller was itself matched by name alone (`via="name"`) printed unhedged, though nothing proves it reaches the function: on nushell, `--for="what calls PolarsPluginObject::into_value"` printed `CustomValueSupport::to_pipeline_data d="2"` with no hedge under three by-name first-hop rows. A `d="2"` row now carries `via="name"` unless at least one resolved call joins it to a first-hop row that is not matched by name alone, including a first-hop row the 16-row cap hides; a row with one such path stays unhedged. The six caller-hop legend clauses (named, ranked and subject routes, full and compact) say so: `via=name if every path has a by-name hop`. Only `--for` answers that carry a `` element change: that element's legend clause and the `via=` attribute of the second-hop rows this rule hedges. `test/forcallerhopcheck.sh` gains arm V1 on Rust, Python, TypeScript and C++ fixtures: a row reached only through a by-name caller, a row with one proven and one by-name path, a row whose own call is by name, a row whose only proven call goes into the by-name caller (Python), and a row whose only proven path runs through a first-hop caller the cap hides (Python). + +### Added — `--for` lists a function's prototypes when a signature change is in question + +A `--for` task about one C, C++ or Go function's signature (signature, prototype, new argument), or the function's bare name, now carries ``: its definition and every non-test declaration the index holds, including the header prototypes `--defs` folds into the definition's row. Present only when such a declaration exists. `functions="N"` says when the rows are N separate functions (a static elsewhere, an overload, another scope): a change reaches its own definition row and the `decl_only` rows right after it. `untied="1"` marks a prototype the index could not tie to a definition (a name match: still a declaration of that name at its line, possibly of another function of the same name). A name that is both a macro and a function stays silent. A macro name is never the subject (`PyAPI_FUNC(int) f(int)` names `f`); the bare form resolves the whole spelling only (`api.c` is silent). A token budget holds the block as `held="budget"`; its `next=` is the same `--for` call without the budget, which prints the rows (`--defs` folds a tied prototype into its definition). Capped at 8 rows (`shown=`, `capped="1"`, `next=--defs=`, which lists definitions with their prototypes folded). Not listed: call sites, the implementations of an interface contract (`--lego`), and prototypes hidden behind a macro (`__P((..))`, `DECLARE_FN(name, t)`). + +### Added — `--for` prints the top-ranked doc sections for a prose question about the docs + +A `--for` task that reads as prose, whose first ten file-tail rows (read in rank order) hold two or more doc files (`.md`, `.markdown`, `.mdx`, `.rst`, `.txt`, `.html`, `.htm`), now ends with ``: the sections of the first three doc files the answer prints (a signature row or a printed tail row, which a token budget can end before its doc rows) that rank highest for the task's words, chosen the way `--recall` chooses them. Each section row names its file, source lines, heading (redacted like the text, unless `--no-redact`) and rank in that file. The block is off under `--signatures-only`, and a multi-root run serves it in XML as it does in `--json`. + +"Reads as prose" is a cue list, not a parse: the task holds no backtick, `::`, `()`, lowercase snake_case word that ends in a letter or underscore, camelCase word, capital-lowercase-capital word, or dotted call `a.b(`. A task that is code-shaped in other ways still reads as prose and fires: `handle_upload_3`, `MAX_RETRIES`, `__init__`, `HTTPServer` and `getX` all do (`fordocbodycheck` pins it, and the block's legend says so). + +The block has its own allowance of 7500 bytes of text, so the rest of the answer is unchanged, and it prices itself: `est_tokens=` on the `` root counts the block alone (the answer's `est_tokens` leaves it out). The files share the allowance by `--recall`'s water-filling; a section that does not fit its file's share is skipped and the next tried, and the unspent rest goes to the best section still left: a section too big for it is an excerpt around the densest window of the task's words (`cut="1" range=S-E/N`, bytes S to E of the section's N). A cut says `capped="1"`, `dropped=` and `next=` the `--recall` call; a file that served nothing is an `` row with its reason (`unreadable`, `allowance` or `budget`). + +A section is served only when it is about the task's subject: it must hold a third of the task's subject words, at most three (subject words are its distinct words of three or more letters that are not common English nor `doc`, `docs`, `describe`, `explain`, `show`), except that each file's top-ranked section is always kept (it can be worded around the mechanism while the task is worded around the symptom). Sections that hold fewer are counted in `below_floor=` on the root (`"below_floor"` in `--json`). `fordocbodycheck` arm q1 pins it. + +A section is a lexical match, not a checked answer, and the block says so twice: `match="lexical"` on the root and a sentence in the legend (`--json`: `"match":"lexical"` and `"pick"`). + +Under a token budget (`--token-budget`, `--max-tokens` with `--detail`, MCP `budget_tokens`) the answer is served first and the block takes the tokens left after it: the allowance shrinks until the priced block fits, and only what does not fit is held (`held="budget" held_bytes=` on the root, `` per file, `next=` the same call without the budget). When `--token-budget` and `--max-tokens` with `--detail` are both given, the block is served into the smaller of the two. A budget the answer alone spends holds the block whole (the files named, with its own `est_tokens=`), and a held-whole block does not add the redactions of text it never printed to the run's redaction summary. `dropped=` counts the sections of a file the allowance did not reach, and `held=` / `held_bytes=` are read off the sections the budget kept out, not off totals. `--json` carries the block as a `doc_body` key (`unserved` entries carry a `why`) and MCP `for` carries the same element over its own rows. `RIPWIRE_NO_FOR_DOC_BODY=1` turns it off. Gate: `test/fordocbodycheck.sh`. + +### Fixed — `--for` on a file name answers with that file (reported in #466 by @pguerrerolinares) + +A `--for` subject that is a file used to miss it in every language: a bare basename (`--for=DynamicScreen.c`, +`--for=sync_ledger.py`) was split on its dot and anchored nothing, a snake_case stem (`--for=sync_ledger`) ended in an empty +`reason="no_candidates"`, and a hyphenated stem (`--for=document-remind`) returned a `confidence="high"` answer whose head was a +test sibling or a decoy that merely shared its words. Now a bare basename with a code extension, a hyphenated or snake_case stem +(when no symbol has that exact name) and a directory anchor the file(s) they name, and a subject that is nothing but the file +(stopwords and words like "file", "package", "script" aside) gets a `` page first in the answer: every def in line +order with its last line, its indexed callers (`in=`; `in="0"` is "no caller in the index", never "unused") and their files +(`by=`), a pasteable `next=` graph query listing the defs neither `main` nor the file's own top-level code reaches within 16 calls, and for a script whose work is its top-level code +(Bash, Python `__main__`, JS/TS, Ruby, Lua) that top-level text, capped at 40 lines / 2 KB with the rest disclosed +(``). The named file's top defs rank first. A name that matches several files lists them in `` and +picks none; a file named as context in a longer question is anchored but gets no page; a dotted `Class.method` with no such +file keeps its old reading. `RIPWIRE_NO_FILE_PAGE=1` turns the page off. Gate: `test/forfileanchorcheck.sh`. A path that names a directory (`--for=pkg/foo`) is read as that +directory before any shorter suffix, so it is never answered by an unrelated same-stem file elsewhere; a shorter-suffix match is +listed as `near="1"` and never paged. The whole page stops at 16 KB at a file boundary (`bytes_capped="1"`). The note the lift leaves in the answer's header comment now says what it did to the auto-served bodies: they follow the named file's rank, a def ranked lower gets none, and the expand flag reads one by name. + +### Added — `--for` names the shown files' other symbols (``) + +A `--for` answer often shows the right file and the wrong symbol of it: a neighbour ranks on body or doc text while the function the task needs sits a few lines away, unnamed. The answer now ends with ``, a few symbols of its first six shown files that it does not name anywhere, one row each (`n=` the owner-qualified name, `p=file:line`, no body). They are leads to read, not the answer. "Does not name" is read off the bytes the run emitted before the block (every section and every trailing block: declaration rows, the graph verb, re-exports, the short-key ladder, the literal anchor, dotted strings, `--with-graph`), by the spelling the answer prints: a name with its `#`, `$`, `@` or `~` mark or its `?` or `!` ending (`#addRoute`, `$tryShow`, `valid?`) is looked up as spelled, and a symbol a shown row stands on is never offered. A C or C++ header declaration and its definition elsewhere are one row (the definition's). `by="name"`: the name shares a task word. `by="callee"`: a definition named like one of the first eight `` rows calls, in the index's graph, a definition named like it: names resolve at both ends (a row's name is what the answer gave). The row is plain only when the shown row itself calls that very definition over an evidence-bound edge; `prov="split"` marks one arm of a split call; `via="name"` every other row (a namesake's call, a call to a namesake, or one bound by name alone). A row in a test or fixture file or scope carries `test="1"`. Three rows per rule, six files, eight head rows: the legend says so, and the open tag prints `files=` / `head=` only when fewer were read, and `name_total=` / `callee_total=` / `files_total=` only when a cap cut (the cut rows are counted, not listed). Present only when a candidate scores. Outside `est_tokens`; under `--token-budget` or `--max-tokens` the block is held (`held="budget"`, `next=` the same call without the budget, with the ranking flags the call had: `--no-route`, `--anchor`, `--pack-top-n`), with a one-line legend of its own, and a run that could not buffer its bytes, or whose buffer failed to flush or read back whole, prints `` and a line on stderr (the second also exits 1: the answer may be short). The block's legend comment sits right before it and spells every attribute the block prints as `name=`, in both legend dialects (a compile-time check holds the two strings to one attribute list); `legendcoveragecheck` reads a legend comment that immediately precedes a trailing block (``, ``, ``) for that block's own elements. On the MCP `for` surface the block is the last thing before ``, as on the CLI. XML only: `--json`, columnar, multi-root and the ranking-candidates export carry no block. `RIPWIRE_NO_FILE_SYMS=1` turns it off. + +### Changed — a broad `--for` answer's file tail walks the file page's first 160 rows, not 40 + +A broad `--for` answer cuts its `` of lower-ranked files. The tail used to list its rank rows and then the first 40 rows of the file page; on the 15 stored R-ladder rows the wanted file sat past that cut, at file-page rank 57-177, in 6 of the 7 items the raised-limit page recovered. The tail now keeps its rank rows unchanged and walks the first 160 rows of the file page (`kForFileTailPageWalk`; on a code question its first 160 code rows, plus the doc rows within its first 160 rows). Every row the old 40-row walk listed keeps its place and its `sym=`, and comes first; the rows past that cut follow in the page's own order, path-only (a third to a half of a full row: 46.8 B mean on the in-sample calls, 70.6 B on the largest repo), and a file the head already shows is not listed there. `p_only=` on `` (JSON: `"p_only"` with a `"p_only_why"`) counts the path-only rows shown. The cut stays disclosed (`shown=`, `total=`, `capped="1"`, and `next=` continues the page), and the tail legend gains a clause that says so, present only when the page has more than 40 rows. Under `--token-budget` the old walk's rows fill first and the `` rows are funded as before the widening; the MCP `for` twin under `budget_tokens` serves the old walk's tail. An answer whose file page fits the first page is byte-identical to before, legend included. + +Measured on the 15 stored R-ladder rows (12 `--for`, 3 map) against main: R items credited 2 to 6 of 35, the 4 gained arrive in the first answer instead of after a raised-limit call, none lost, and the 15 answers grow 12 % in bytes (10 answers grow by 1.6-6.7 KB; the 3 map answers and the 2 whose file page fits one page are byte-identical). + +### Fixed — `--for` reads `confidence="high"` on a name-exact route only when the anchor heads the rows + +- `--for` and the MCP `for` verb: `confidence=` measures how far the scores drop below the ranked head, so on a name-exact route it vouches for the anchor only when the head row is the anchor (a symbol of the anchor's name, or a row in the file the anchor is defined in or a file subject names). When another symbol heads the rows, as a plain word's namesake does in `where concurrent.futures.ThreadPoolExecutor is defined`, confidence reads low and `route=` appends `; anchor is not the ranked head (r=1 is NAME in PATH)`. The rows themselves do not move. A conceptual route has no anchor and reads as before. "The ranked head" is the row the answer prints as r=1: code above docs moves Markdown rows below the code rows, so a Markdown heading that scored first does not make the anchor look unproven when the anchor prints first. A word that spells a symbol the index holds (`FooBar.py` as the method `py` of `FooBar`) is not read as a file subject because a file of that spelling exists. The review of #568 found both (thanks CodeRabbit); `test/subjectanchorcheck.sh` arms FH(d) to FH(f) cover them, CLI and MCP. + +### Changed — open pull requests stop conflicting on generated totals + +Every landing used to rewrite the totals in `README.md`, `docs/EVALS.md`, `docs/LIMITS.md`, `docs/TUNING.md` +and the showcase deck (the gate count, the cap inventory, the class counts, the TUNING census), so every other +open pull request that had added a gate or a cap went CONFLICTING and needed a main merge plus a full CI run. +Those totals are now a dated snapshot, `docs/counts.tsv`: the generators publish them from it, every site that +prints one carries its date ("counts snapshot YYYY-MM-DD"), and they rewrite only per-item lines (the absorb +list, the per-file cap tables, the per-cap sensitivity tables), which two pull requests touch on different +lines. `python3 docs/counts_snapshot.py show` prints the live numbers beside the snapshot, `check` compares them +and `refresh` re-takes the snapshot, all or nothing. How stale it can get is bounded: a refresh rides every train +pull request, `test/gendocsmergecheck.sh` goes red once the tree is more than 10 gates or 20 caps away from the +snapshot (`check --max-drift`), and the release workflow refuses a tag unless `check` is exact. A snapshot whose +totals do not add up is refused by every generator. Replayed with `git merge-tree` against the open pull requests +of 2026-11-03, each side cut after this change: #512 and #513 against each other conflicted on `docs/LIMITS.md` +and now merge clean; #505 against main keeps only its one-time absorb-list reshape; #506 keeps only the files a +build pins (`changelog.d/versions.md`, `docs/COMMANDS.md`, two showcase captures). Pull requests opened before +this change need one more main merge, which `scripts/merge_main.sh` settles on the total lines. + +`scripts/merge_main.sh` marks main's newer showcase capture for re-recording even when it did not conflict, and +the new `scripts/merge_main.sh --regen-after-build BIN` settles the marks of every merge since its last run from +a build of the merged tree: it re-pins only the parity labels the two merged sides pinned differently (and refuses, +naming it, a label both sides agreed on that moved), re-records the newest capture's legend-dictionary `dictv=` +pair, regenerates `docs/COMMANDS.md`, and commits. A marked line where one side only re-ran a generator now takes +the other side's rewording, unless the rewording changed a hand-written figure, its sign or qualifier included +(`~210` to `210` stays a conflict). New gate `test/gendocsmergecheck.sh`; `test/mergemaincheck.sh` gains the +rewording, qualifier and re-pin arms. + +### Added — `--for "path from A to B"` shows both sides of a missing path + +When the path verb a `--for` task already runs (`path from A to B`) prints no directed path (`reachable="0"`: none was found, or a `route_hedged="not-proven"` refusal withheld the chain it found), a new `` block follows inside the graph block. It carries A's callees and B's callers, each the plain `--callees` / `--callers` answer cut at 10 rows (`count=` is the total, `next=` the page after) with its own legend, and says a name common to both sides is a candidate link, not a proven path. A found path prints exactly what it printed before, and the ranking and `route=` are unchanged. The block is CLI only; the MCP `for` verb does not print it. New gate `test/graphformendpointscheck.sh`. + +### Fixed — a history walk no longer downloads, and no longer hangs + +- On a long real history, `--for`, `--metrics` and `--exemplar` waited on `git log --name-only` with no limit: past 3600 s on a 310 K-commit ClickHouse clone, and on a blobless clone git fetched blobs from the network as it went (`.git` 15 GB to 33 GB). Now: + - No git command of ripwire's lazy-fetches on a partial clone: each carries `GIT_NO_LAZY_FETCH=1` (git 2.45 and later) and a closed `GIT_ALLOW_PROTOCOL` (git 2.6.1 and later), as a prefix on that command only. They are not set in ripwire's own environment, so the command `--run-trace` runs, and any other child that is not git, sees the environment you gave ripwire; and the clone of a URL root uses your own `GIT_ALLOW_PROTOCOL` and `protocol.*.allow` as `git clone` would. Versions are from git's source at the tags (`environment.h`, `git.c`, `transport.c`); only git 2.50.1 was run, an older git is simulated by a shim. A verb that truly needs an absent blob says so: `--quality-delta=REF` refuses naming the partial clone and the `git archive >/dev/null` that fetches it yourself, `--pr-context` lists the changed files from the trees (a dirty tree too) and refuses when it cannot, `--stray-content` reports a branch it could not read as `unknown`, never `merged`. + - The walks carry `--no-renames`. Rename detection was the only thing that read blobs. It changes one thing: a rename commit now also lists the path it left, which matters only when that path exists again at HEAD (whole history: textual 5 of 354 such paths, hono 3 of 286, tmux 1 of 23). + - Every record walk has a budget, 20 s or 200 K commits (`RIPWIRE_GIT_WALK_SECONDS`, `RIPWIRE_GIT_WALK_COMMITS`, 0 = none, at most 1,000,000,000; a larger or malformed value keeps the default). A cut keeps the newest commits and says so: `history_unread="1"` on documents with `churn=`/`amp=`, a `history_cut` comment and stderr line on every other CLI verb, and a `_history_cut` envelope note on an MCP answer. Where the clock cut a walk, the prefix it reached moves with machine load, so two runs can differ; the cut is cached until HEAD moves except under `--no-cache`, and a clock cut under about 1,000 commits is not cached. ClickHouse `--for`: 3600 s and unfinished, now 50 s. + - On a partial clone, no git that scans the promisor pack is started. With lazy fetch off, git parses every object of the promisor pack before it fails on the first absent blob, which is minutes to hours on a clone like ClickHouse (5.2 M objects: `diff --numstat`, `archive` and `log -p` each still running at 90 s). `--pr-context=REF` lists the changed files from `git diff --name-only` there and says so on stderr (a mode-only change is not told from a content change); elsewhere `--numstat` runs under the walk budget, and a stalled or failed one falls back to the same list, which keeps the budget too: if that read stalls as well, `--pr-context` refuses and says so. `--quality-delta=REF` and `--merge-scout` first ask git, without fetching, which blobs of the tree are absent and refuse before `git archive` when any is. `--whereis --with-history` and `--naming-calibration` answer `probed="0" r="partial-clone"` without running `git log -p`, and the rename map behind `--quality-delta` reports a window of 0 commits, truncated. Reads that fail fast on an absent blob still run (`blame`, `show`, `cat-file -e`, a zero-context `diff`, and `archive` when the tree is whole), and a failed one is said, never read as an empty answer: + - Plain `--quality-delta` on a dirty partial clone used to pass its gate: short-horizon-churn needs the churn window's tree and a blame of the edited lines, and when git refused them the row vanished or read `churn="ambient"`, so the answer said `gating="0"` and exited 0. Now the root says `unread_history=` (`churn-tree`, `churn-blame`, `churn-window` for a window walk cut by its budget, `renames` for a pinned sidecar floor whose rename record was refused), in XML, `--json` and MCP, a row whose blame failed says `churn="unknown"`, and an answer with no gating row exits 6, the partial-answer code, with a stderr line. A dirty `--quality-baseline` there refuses unless `--allow-dirty`, since it cannot tell what it would absorb. A clean tree reads no history and is unchanged. + - `--slice=SYM:VAR --since=REV` said `status="file_absent_at_rev"` of a file that is in that tree when its blob was not in the clone. It now says `status="unread_at_rev"` (`comparable="0"`) when the tree lists the path, or when the path is not there and the rename record that could reach an earlier spelling was not read. + - Over MCP, an `owners` call whose history walk was ended by its budget names the budget instead of "no git history". + +### Added — `--path` and `--connect` hops print the call site, the call and the callee's signature + +A `--path` hop row and a `--connect` `` row used to name only the two definitions an edge joins, so the next step was opening the caller to find the call and the callee to read its parameters. Each hop now also prints `site="file:line"` (a call in the caller that names the callee: one whose argument count the callee accepts first, then one qualified by the callee's owner; `sites="N"` when the caller has more than one such line), `call="…"` (that call's text from its line start, or from the statement when the caller is a one-line function, whitespace collapsed, joined across up to 3 lines while its parentheses are open, at most 120 bytes) and `sig="…"` (the callee's declaration signature, at most 160 bytes). The site is a name match, not proof that this call is the one the edge binds, and the legend says so; `site_by="first"` marks a pick among equally fitting lines when the caller also calls another callee of that name, and `site_by="nofit"` a site whose argument count the callee does not accept. Call text is redacted before any cut, and no cut lands inside a token or a redaction, so a credential prints whole-redacted or not at all; a callee name inside a redacted run withholds `call=` (`hop_redacted=`). On `--path` the first reaching pair prints all three and later pairs print the site only; on `--connect` a callee's signature prints once per group and not where its join row already shows it. A call or signature whose file cannot be read, or whose length changed since indexing, is left out and counted (`hop_unread=`), and a length change leaves the site out too; a site whose text does not name the callee is counted too (`hop_notext=`); an edge with no call naming the callee in its caller prints no site (`hop_nosite=`) but still prints the callee's `sig=`. Under `--max-tokens`, `--connect` drops these attributes first (`sig=`, then `call=`, then `site=`, named by `hop_shed=`) and only then trims as before. Two `` rows that would read alike (two same-named targets reached from one caller) now print `tp=`/`fp=`, the definitions they join. The MCP `path_between` and `connect` tools print the same rows. `RIPWIRE_NO_HOP_CONTEXT=1` turns the attributes off. New gate `test/hopcontextcheck.sh` covers C, C++, Python, TypeScript, Go and Rust hops, the site pick (owner, argument count, ties), credentials a cut would split, hops with no site, unreadable and edited files, the shed order, and one reference pass for a 40-hop path. + +### Changed — a C++ member written as a smart-pointer alias, and a Rust local filled by a trait-object call, hand an interface-typed call to the dispatch tier + +A C++ member written `Http::FilterSharedPtr handle_;` (`using FilterSharedPtr = std::shared_ptr;`, a `typedef` of `std::unique_ptr`, or up to three plain aliases of such an alias) is read as its pointee through `->`, exactly like a member written `std::shared_ptr`: an interface pointee declines `gate="iface"` with its implementors counted, in place of every same-named method as a `via="name"` guess. A Rust `let d = s.get_decl( id ); d.run()` whose one proven callee returns `&dyn Command`, `Box` or `impl Command` types `d` the same way, and an inherent method of another type that happens to be called `run` is no longer listed. + +The alias must be in scope where the member is written: its own class, an enclosing namespace or class, or the namespace the member's qualifier names. A template parameter that shares an alias's name, a name that is not visible there, an alias that is also a class, two same-scope aliases that disagree, a pointee written in `std`, and an alias of a concrete class whose virtual methods derived classes override all keep the by-name rows; a deleted or defaulted operator, a `friend` declaration or a member-template declaration never makes a class an interface. + +Floors, stated: a using-directive (`using namespace x;`) is not followed; a using-declaration of the alias's name in a nearer scope than the alias is not seen; a Rust `type` alias of a class-like name in a mixed C++/Rust tree can stop an unrelated C++ alias from peeling (the base rows stay); a local or parameter written as an alias is not peeled; Go interface-typed results are not read. + +### Fixed — `--impact=` lists the file's importers instead of answering `found="0"` + +The selector is a symbol, so a path matched nothing and the answer was `found="0"` with no hint, even for a file hundreds of +files include. When no symbol matches and the selector names one indexed file (root-relative, with a leading `./`, absolute +under the root however the root was typed, or a path suffix only one file has), the answer is that file's importers under +`selector="file"`, exit 0, with the importer listing's own `shown_importers=`, `importers_capped=`, `importers_next=` and +rollup. The answer names the file it used, `file=`, and a suffix match says so, `matched="suffix"`: a path to a deleted or +unindexed file that another file's path ends with answers for that other file, visibly. It has no call reach: `FILE:SYM` +still picks one symbol. A symbol match always wins, so no selector that answered before answers differently. A suffix several +files share stays a refusal whose `near=` is the first such path, with `matches=N` counting them. +When files name the path in an include or import that resolved to no indexed file (an include root the index does not know), +the answer carries `unresolved_naming=N`, so `importers="0"` on such a file reads as a possible floor, not as unused: Envoy's +`envoy/registry/registry.h` answers `importers="0" unresolved_naming="580"`. A directive that resolved to another file of the +same name (`a/util.h` beside `b/util.h`) is not counted. + +### Added — `--impact` sums up the importers a cut listing hides + +A cut importer listing showed the 40 most-imported importers and nothing about the rest. Beside `importers_capped="1"` the +root now carries `importers_tests=` (how many importers are test, bench or fixture files) and `importers_dirs=` (the three +directories, by their first two path segments, that hold the most, as `dir:count`), both counted over every importer. On +cpython, `Lib/os.py` has 512 importers: `importers_tests="353" importers_dirs="Lib/test:346,Lib:58,Lib/idlelib:24"`. XML, +columnar, JSON and the MCP tool carry it. + +### Added — C++ `using` aliases, namespace aliases and function-pointer typedefs are definitions, and `--defs` prints their target + +- C++ `using X = Y;` (plain, templated `template using V = …;`, and class-scope `using value_type = …;`), `namespace L = lib;`, function-type and function-pointer typedefs (`typedef void (*Fn)(int);`, `typedef int Fn(int);`, `t="functype"`) and pointer typedefs (`typedef struct S *SP;`) are now definitions in C++ files and in every `.h` (a header reads as C++). A `.c` file reads with the C grammar and is unchanged: a function-pointer typedef there is still no definition. `--defs=ColumnPtr` on a tree with `using ColumnPtr = COW::Ptr;` used to find nothing; it now lists the alias. A `using` written inside a function body or lambda stays a local name, and `using namespace ns;` / `using ns::name;` add no definition. +- `--defs` rows for a `using` or namespace alias carry `to=` (the written target, whitespace folded and comments dropped), `to_next=` (the defs call for the target: `--defs=Owner::leaf` when that spelling binds a definition, else the last name segment, which may list namesakes) and, when the file can no longer be read, `to_unread="1"`. A target longer than 160 bytes is cut with an ellipsis and marked `to_capped="1" to_bytes=N`. The JSON form carries the same keys, and a multi-root workspace (`ripwire a b --defs=X`) prints them too. A `typedef` row keeps its old shape (no `to=`). +- Because aliases are now definitions, a name-exact `--for` on an alias lands on it (on ClickHouse, `--for=ColumnPtr` now routes name-exact and prints `IColumn_fwd.h`'s declaration), and C++ trees gain namesakes where an alias shares a name with another definition. +- A `--for` sentence around a name whose only definitions are `using` or namespace alias rows (``what does `using ColumnPtr = ...` expand to``) still gets the `` ladder, so its literal rung prints the `using` lines; a name that also has another definition stays resolved. A literal row no longer names its own alias row as `in=`; it names the definition around the alias, so a class-scope `using` keeps its class. +- TypeScript module-level `const` values are unchanged. Gate: `test/aliasdefcheck.sh`. + +### Security — `skills/install.sh` takes the installer hardening list from #293 (hardening from #293 by @s0undt3ch) + +@s0undt3ch's #293 proposed moving skill and hook installation into the binary. We kept the installer a script, but +its four review rounds produced a list of ways an installer can be talked into writing or deleting the wrong thing, +and every item that applies to a shell installer now lives in `skills/install.sh` (#293, thanks @s0undt3ch): + +- **Absolute roots only.** A relative or empty `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTS_HOME`, `CODEX_HOME` or + `HERMES_HOME` is refused with status 2 instead of installing relative to the current directory (an empty `HOME` + used to mean `/.claude`). Windows drive paths still count as absolute; an explicit destination argument still + resolves against the current directory. +- **Nothing it creates is group- or world-writable.** The umask is only tightened, so `umask 000` no longer yields a + 0777 skills directory or a 0666 `settings.json`, and a stricter `umask 077` is left alone. +- **`settings.json` / `hooks.json` are merged safely.** A symlinked file (a dotfiles setup) is written through and the + link kept, the temp file is built beside the target so the final rename is atomic, and the file's mode survives the + merge (CWE-732). A first-time file is still created owner-only (0600), as before. +- **The hook command is quoted** when its path needs it, so a checkout under a path with a space registers a hook + that runs; the ordinary path is registered byte-for-byte as before, and re-runs still recognise the entry. + `ripwire --doctor --agent=claude|codex` and the hook-removal step in `INSTALL.md` read the quoted form too, so a + working hook is not reported broken and the documented removal takes it out. A path holding a single quote is + refused. +- **`jq` is found only through absolute `PATH` entries** (CWE-426): an empty or relative entry no longer runs a + `jq` planted in the current directory. +- **No stray temp files.** The installer's temp files are removed on a normal exit and on HUP, INT and TERM. A + SIGKILL cannot be trapped, so it can still leave one `.tmp.*` file beside the file being written. +- **The prune removes only what is provably ripwire's:** a symlink the previous manifest recorded, a dangling one, or + one pointing into this checkout (compared by whole path components, never a string prefix, and never through a `..`). + A live symlink of your own that happens to be named `ripwire-*`, and a regular file with such a name, are kept and + reported. Likewise a regular file of yours sitting at the name of a skill ripwire ships is kept, not replaced. + +Not carried over from #293, and why: the items that apply only to the embedded store (byte-comparing it against the +binary, `lstat` of its key and intermediate directories) have no counterpart here, because this installer links +straight to the checkout; the release-archive lookup of the `ripwire` binary does not apply, because this script never +needs the binary; the Windows copy fallback and the link-loop/link-failure reporting that #293 also added were already +here (the copy fallback and the `FAILED` report came with #334, and a loop at a shipped name is repaired). Thanks again +@s0undt3ch. Windows behaviour is unchanged. Gate: `test/skillinstallcheck.sh` sections (J)-(Q), each red against the previous script. + +### Fixed — a `--for` answer's `` block keeps a header declaration apart from an unrelated same-name definition (thanks CodeRabbit, #555) + +The `` block folds a bodyless C/C++ header declaration into a same-(scope, name) definition in another shown file, so the definition's row stands for both. Until now it did that with no check that the two were one function, so an unrelated namesake in another shown file could replace the header's row. A declaration now folds only when the definition's file answers its header: the definition is in a file that `#include`s the header, directly or through other headers (an umbrella header such as `tmux.h`, or a quote include written from an include root, `#include "include/api.h"` in `src/`), or the two share a path stem (`src/foo.h` and `src/foo.c`, `.cc`, `.cpp`, `.cxx`, `.m`, `.mm`). Otherwise both rows print and the header keeps its row. An angle include, or an include no rule resolves, ties nothing, so such a pair prints two rows. Measured on the 36 C-family calls of the 321 in-sample `--for` calls: 24 printed rows came from a fold, and all 24 still fold (every definition file there includes its header); one call (mf1-tmux-04) changes after the prototype rule, `name_total=` 54 to 55 (a header declaration that had folded into a source-file prototype keeps its own row), with the five printed rows identical, and the other 35 are byte-identical. `test/forfilesymscheck.sh` arms k1-k7 cover C and C++: an unrelated definition, a direct include, an umbrella header, a shared stem, a root-relative include, a source file holding only the prototypes, and an uppercase `.C` stem. The case came from a CodeRabbit review on #555; the prototype and `.C` cases from the one on #566 (thanks CodeRabbit). + +### Fixed — macOS peak footprint about halved: freed large blocks leave the process + +The default macOS zone keeps freed large blocks resident, and an index frees many: each parse worker's reference array +doubles through 8-65 MB blocks. `operator new` of 256 KiB or more now gets its own mmap region and goes back to the OS on +delete (`src/largealloc.cpp`, macOS only, off under sanitizers). Peak footprint of `--deps --no-cache`, interleaved +runs: Envoy 1,515 to 767 MB, cockroach 1,848 to 984, vscode 2,873 to 1,482. Answers are byte-identical; CPU time is flat and wall stays within load noise. +`--max-memory` reads the footprint between phases, and the blocks the route returns early are gone from that reading, so a run +can now peak above its cap, pass every reading and finish. That is not stopped; it is said: one stderr line on any verb, +`memory_peak=` in the default map's header, `_memory_peak` in the MCP envelope, naming the peak and the cap and noting that the +cap is a rule of thumb against running out of memory, not an exact line (the 65 % default is not a measured threshold). The +disclosure is on in builds that link the route. `test/largeallocpeakcheck.sh` keeps the peak ratio and pins the disclosure. A chunked worker store removed only 0-17 % and was not kept. Linux has no route: on a generated +fixture glibc's max RSS moved 2 % with a fixed mmap threshold. Windows was not measured. + +### Changed — `` orders a literal's sites by nearness to the answer and cuts the far ones when they do not all fit + +The literal-site block used to list a literal's byte-exact sites in an order that did not look at the question: on a widely used name (a setting read in hundreds of places) its 32 rows filled with readers in unrelated directories, CI scripts and other tables that hold the same string. Now each read site is placed against the answer above the block: its signature rows and the bodies it prints (the fold, the slot, `--detail` and the auto bodies), plus the literal's own source definitions (a symbol named its last segment that the earlier segments place; a generic last segment alone, such as `enabled`, anchors nothing). A site is near when it stands in one of their files or directories, or its function calls or is called by one of them; a hub (called from more than 16 functions, such as `getSetting`) puts no caller one call away, and when more than half of the answer's rows are themselves readers of the literal (a body match) those rows anchor nothing. Table rows still come first, and near sites come before far ones in each group. + +A block whose sites all fit is only reordered (a vendored, minified or generated copy is listed after the product sites); nothing is cut. When the sites do not all fit the rows: a vendored, minified or generated copy beside a product site is cut (an authored `static/` or `locale/` file is product code); when something stands near the answer, the far sites are cut; when a table row stands in the answer's file or one call from it, the rows of the other tables are cut; with none there and several tables, the far rows outside the home table are cut. The home table is the file with the most table rows of the literal, then the one that declares it (an X-macro `#define` list over a map or set), then the one with the most sites outside any function; a tie names no home and cuts nothing. The registrations the reserve keeps (quoted keys outside any function) are never cut. When the cut moved a row (a site the old order would have listed is gone), `lit_cut="N"` counts the sites cut and a legend clause, printed only with it, says why; a cut of sites that stood past the rows anyway is not said again (`capped="1"` and `shown=` already say it). `next=` still lists every site. A block with nothing near the answer and a single table keeps its old rows. Gate: `test/forliteralcheck.sh` arms D1-D14. + +### Added — `--for` shows the manifest lines that list a shown file when the task adds something new + +When a `--for` task is a creation task (it says new, add, register, plugin or extension as a word — not inside an identifier such as `Plan::AddTarget`) and a build, package or registry manifest at the root or in a directory above the ranked rows lists two or more of their files (a list of the exemplar's siblings, not a single `"main"` entry), the answer now carries those lines after the file tail: `` with each line byte-exact as ``, preceded by the less-indented line it sits under (automake's `dist_tmux_SOURCES = \`, package.json's `"./timing": {`, a Bazel `EXTENSIONS = {` dict). Read: Makefile.am (a configure-written Makefile beside it is skipped), Makefile, CMakeLists.txt, meson.build, BUILD/BUILD.bazel, `*.bzl`, package.json, jsr.json, deno.json, and Cargo.toml (CODEOWNERS is ownership, not a listing, and is not read). `by="path"` names the file, `by="dir"` only its directory (two or more whole path segments, so `middleware/log` never matches `middleware/logger`; make, CMake, meson and Bazel BUILD files are matched by file path only, so a dependency label is not a listing); `also=` lists the other shown files the same manifest names. Files under vendor/, third_party/, node_modules/ and bower_components/ are vendored and skipped. Comment lines, glob or variable listings and lists keyed by a command or option name (man pages, docs indexes) are not matched. Default regime and XML only; `RIPWIRE_NO_LISTED_IN=1` turns it off. On "new tmux command" the answer now prints the Makefile.am sources list (+0.5 KB), on hono's "new middleware" the package.json and jsr.json `exports` entries (+1.3 KB), on Envoy's "add a new HTTP filter extension" `extensions_build_config.bzl` and the extension's BUILD file; answers whose task has no creation word are byte-identical. + +A name counts as listed only where the manifest resolves it from its own directory, inside one stanza: `legacy/x.c` is not `x.c`, a subdirectory manifest's root-anchored `$(top_srcdir)/x.c`, `${CMAKE_SOURCE_DIR}/x.c` or Bazel `//:x.cc` is not its own `sub/x.c` (and is not listed), another package's `package.json` does not list this package's directory, an `exclude = [...]` / `list(REMOVE_ITEM ...)` list and a `#[[ ... ]]`, `/* ... */` or trailing `#` comment name nothing, and a second name counts as a sibling only in the same top-level stanza. A manifest above the crawl size ceiling is not read. The legend says the block is not exhaustive. The two caps on what is read (a directory's `*.bzl` files past the eighth, shown files past the sixteenth) are disclosed on the first `` as `files_capped="1"`; an answer with no `` makes no claim, so there is nothing to disclose. + +### Fixed — `scripts/merge_main.sh` no longer stops on an RST underline, and refuses a kParserVer both sides bumped + +The staged-marker check read the RST `=======` underline at `test/restdocscheck.sh:77` as a leftover conflict marker, so every lane's merge of main stopped on a file that was identical to main's. The check now skips a path whose staged text is `MERGE_HEAD`'s own; a real `<<<<<<<` / `=======` / `>>>>>>>` hunk in any other path is still refused and named. + +When both sides bumped `kParserVer` (`src/ingest_cache.h`) above the merge base, the merge is refused unless the merged value is above both. The message names the lane's and main's numbers and suggests max+2 (the rich file tag is `kParserVer` + 1), and reminds you to update `kIngestParserVerMirror` in `src/quality.h`. One side bumped, or neither: nothing is checked. `test/mergemaincheck.sh` has arms for both. + +### Added — a `--for` task that names a literal carries the literal's sites and the code that handles it + +A `--for` task that carries a "double-quoted message" (at least six bytes inside the quotes, a letter, an ASCII space inside, no leading or trailing blank: `"a b"` does not qualify) or a CONST_SHAPED word (an environment variable, an +error code or a setting name such as `FZF_DEFAULT_COMMAND`, `FST_ERR_DUPLICATED_ROUTE`, `PYTHONASYNCIODEBUG`) now ends with a +`` element, just before ``: the grep verb's byte-exact scan of the index for that literal. Its `` rows are the +sites in source files, labelled by the parse (`k="string"` inside a string literal, `k="code"` outside one, `k="?"` a file not +classified) with the definition holding each (`in=`, `ink=`), sites inside a function first; comment sites, Python docstrings (a +triple-quoted string that is the first statement of a module, class or def body; the same string opening a call-argument line is a +site), test paths and doc, shell or config files are counted (`comment=`, `test=`, `doc=`), never listed as a handler. The function +holding a shown site is printed whole when it is ten lines or fewer and its body is at most 3,000 bytes (``, at most three; a short but large or minified body is an excerpt); a longer one is an excerpt (``): its +signature line and one window of the site line minus one to plus five per shown site (`l=` lists the ranges, windows that overlap or touch are merged and an ellipsis line sits +between disjoint windows only, `of=` is the body, `next=` the whole body; each window line is cut at 200 bytes, marked `cut="1"`, so a minified one-line body is never printed whole); a body the answer already printed is not repeated (`body="above"`); an owner past the three-body cap prints no body and its rows say `body="held"`. +At most eight `` rows are shown (`shown=` against `src=`, `capped="1"` when sites were left out; `next=` lists every site); `floor="1"` marks the counts as lower bounds when the scan stopped early or a source site was left unclassified (past 64 hit files, or a file the parse could not read), and the matched lines and excerpts are credential-redacted like any other body. +Several sites are always listed with counts; a literal with no source site (found only in tests, docs or comments, or not at all) +carries no block. The element carries its own legend (a 458-byte form under the default `--legend=compact`, the long text under `--legend=full`) and its size (`bytes=`, not in `est_tokens`); +everything before it is byte-identical to the answer without it (`RIPWIRE_NO_LITERAL_ANCHOR=1`); `--json`, the columnar and +candidate-list dialects, a multi-root run and an explicit `--token-budget` carry none (the default XML dialect with a single root only). A `--flag` spelling is not read as a literal. Gate: +`test/forliteralcheck.sh`. + +Two more shapes are read from the task's own words, since a stored or agent task is usually bare. `shape="dotted"`: a dotted key of three or more segments, or two with a whole word `setting`, `option`, `key`, `command` or `flag` beside it (`files.autoSave afterDelay setting flow`). Segments are identifiers; a leading dot, a file or host suffix, the name of an indexed file, a `self.` or `this.` chain and a call are not keys, a capitalised first segment (`Class.method`) needs the cue word, and a word of 4 or more segments is a key even when its last segment is a language extension (`envoy.filters.http.lua`), so a quoted file name that long reads as a key too. A row holds the key: a site inside a longer name or a child key is counted in `longer=`, never a row. Quoted sites rank first, with 2 row slots kept for a quoted string site outside any function (a registration or table; a C `#define` is not one), and a row cut at 200 bytes opens 40 bytes before the key. `shape="message"`: three or more words with a whole word `error`, `notification` or `message`: a backticked or single-quoted span, else the task minus a leading or trailing cue, else the words before a cue inside the sentence. The whole text is tried first and needs one word that is not a function word and must end in one (`must not be empty`); then fragments of the longest placeholder-free run of words, down to three words with at least two such words (`frag="1"`: `s=` is only part of the task's words). Hits stand on word edges (a hit inside a longer word is counted in `longer=`; the letter of an escape such as `\n` or of a printf verb such as `%s` right before a hit is not a word byte). A code row (`k="code"`) is never printed for a message; a `k="?"` row stays. Either shape adds a second legend comment. + +Literals are tried in order, quoted span, CONST words, dotted keys, message, until one has a source site, and `try="N"` says the anchor was the task's Nth. The literal caps: 4 scans of a quoted span, CONST word or dotted key, and up to 8 scans for a message (that second cap is silent). When the first cap skipped a literal and nothing scanned anchored, the answer ends in `` with its own legend and no ``; `next=` is a `--grep=` of the first skipped literal. Not covered: a key past byte 512 of its line stays hidden with no word, and a registration inside a function ranks among the readers by path, so it can fall past the shown rows (`capped="1"` and `next=` say rows were left out). + +### Added — `--for` names the implementations that can run at an interface call the task names + +When a name-exact `--for` task names a function and a member call inside it (`Parser::Load file_reader_->ReadFile`, `MDBook::execute_build_process renderer.render`) and that call goes through an interface, trait or abstract receiver the resolver declined, the answer now carries `` after the caller hop: one `` row per in-tree implementation that can run there (`ctor=` rows name where that type is made), and, when exactly one production candidate is left, its body. Test doubles (test code, or a type only test code names) are counted in `test=` and not shown; a type that production code builds — in a registry, a factory with a production caller, a file-level registrar, `main`, a Go `init`, or a C/C++ OS entry (`wmain`, `_tmain`, `WinMain`, `wWinMain`, `DllMain`) — is never counted as a double. When the task writes a receiver (`disk_->ReadFile`), only a site with that receiver answers. Languages that type structurally carry `floor="1"` (an unlisted type can still run there) and never get a single body. Default regime and XML only; `RIPWIRE_NO_DISPATCH_HOP=1` turns it off; answers whose task names no such call are byte-identical. On ninja's "Parser::Load file_reader_->ReadFile" the answer now prints `RealDiskInterface::ReadFile`'s body (the test file system is counted as a double); on mdbook's "renderer.render" it lists the four production renderers and where they are built. + +Not covered: a task that writes the TYPE as the receiver (`Disk.ReadFile`) or a reference alias of the field gets no element. On an index the memory guard cut (the MCP `for` answers over one; the CLI refuses it), a type no kept file builds is not counted as a double, because the cut may have dropped the file that builds it; only a definition on a test path is demoted there. + +### Fixed — a "not found" from an index the memory guard cut no longer reads as proof of absence + +When the memory guard stops a crawl or a parse, the MCP server keeps answering from the partial index. A result from that +index has always carried `_memory_stop`. A refusal did not: `find_symbol` on a name outside the kept files, and every other +not-found or error built after the first request, said only "symbol not found", which an agent reads as "the tree does not +define it". Every error from a cut index now ends with the same disclosure, prefixed with the one thing a not-found reader +needs: the index is partial, so an absence is unproven. It names the stop, the number of files the answer covers (and how many +of them were parsed) and the override (`--max-memory`, `RIPWIRE_MAX_MEMORY`, or a smaller root). The sentence is the same on every path: the first request that built the cut index (which is often an agent's +first call), a `batch` sub-query's `err=`, and the `edit_check` / `quality_delta` refusals. A `grep` payload from a cut index +no longer says `"complete":true`, since a hit may sit in the files that were never read. An error from a whole index, +including another root served by the same process, is unchanged. The CLI needed no change: every verb except the map already +refuses a cut index with exit 5. + +The crawl also walks in a fixed order now (pre-order, each directory's entries by file name). It used to take the filesystem's +directory order, and the memory guard stops after N entries, so which files survived a cut depended on the filesystem: the same +tree under the same limit answered from different corpora on macOS and Linux (CI `memguardcheck` B6b). A walk that finishes +sees the same files in the same final order as before; only a cut walk changes, and it is now the same cut everywhere. + +One visible side effect on trees past the report cap: where a repository holds more than 500 files of types ripwire does not +index, the off-index scan of `--grep` and the `--skipped` listing keep the first 500 of them, and "first" now means file-name +order instead of the filesystem's directory order. The indexed hits are unchanged; the off-index hits and the listed rows can +differ (measured on `textual`: `--grep=Widget` off-index hits 20 to 25), and the scanned count can now show +`unindexed_files_skipped=` for files in the new set that could not be read. The cap is still disclosed +(`unindexed_candidates_capped=1`). + +### Changed — `--for=path from A to B` keeps the path's own rows in the ranked head + +In a task that asks `path from A to B` (opening with `path from`, or `trace`, `show`, `explain` or `what is` then `the call path from`, `the code path from` or `the execution path from`, and not also asking `who calls` or `callers of`), a ranked caller of A (a resolved call, not a by-name one) that A does not itself reach through resolved calls is upstream of the asked path. It is scored below every other row, keeping its own order; files_first may still list a file's best row early. The header says how many moved (`[path from: N callers of the from endpoint scored below the path's own rows]`); pack-task prints it as its own comment, so the compact legend keeps it. A caller of A that A reaches at any depth stays (recursion, an eval loop), and so does B itself. A's reach walk stops at 4096 symbols, and then nothing moves (`path_from_capped=1`). B need not name a symbol. Before, a newly bound caller of A (`python -m django`'s `management.execute_from_command_line()`) could push a hop under A past the 40-row head. The MCP `for` and pack-task twins apply the same rule. + +### Changed — warm `--deps` on a large TypeScript tree uses about a fifth less CPU + +Resolving one relative TS/JS import (`./util`) tried up to 16 file names (`util.ts`, `util.tsx`, …, `util/index.jsx`), and each try built and normalized a new path string. The directory and the specifier are now joined and normalized once per import and each try is a lookup; inside one include-graph build, an import that repeats a (directory, specifier) pair takes the first answer, including a miss. On a single root, the shared join step behind the other languages' path probes also stops allocating per try; Rust, Python and Ruby still build their candidate strings, and a multi-root tree takes the previous path. Measured warm on one machine, median of three interleaved pairs against the previous build: vscode `--deps` user CPU -22 % and instructions retired -23 %. envoy (C/C++) and cockroach (Go) do little of this work: instructions -1 % and 0 %. Peak footprint (MiB) is unchanged in each of those runs. + +Answers are unchanged where it was checked: 9 warm and 3 cold base-versus-new output pairs on those three repos are byte-identical. A new case in `test/tsimportprecisecheck.sh` resolves one specifier from two directories to two files and keeps a miss a miss after another directory's hit. + +### Fixed — an unreached `--path` whose `to=` is stored in a struct field no longer prints the caller's body + +A `--path` whose target is a function stored in a struct field (`--path=real_main,ToolBrowse` on ninja's tool table, +`--path=Row_display,LinuxProcess_rowWriteField` on htop's class table) answers `reachable="0"` with a `` whose last row +carries `via="slot"`, `slot_callers=` and `slot_fns=`. Its `` used to print the `from=` function's body without +that hedge: a body holding a call through a field, not a call to `to=` (+4.0 KB on ninja, +1.1 KB on htop, against the answer +without it). The row whose only next step is the slot crossing into `to=` is now left out of the route bodies; a slot crossing in +the middle of a route keeps its hedged body, and `next=` still names `--callers=`, which lists the call through the field. +A C++ class that names a template parameter as its base (`template class M : B`) joins no in-tree class's family for +the name-template dispatch either, as it already did not for the member-pointer candidates. + +### Fixed — a `--path` chain stitched from calls bound by name alone is no longer printed as the plain answer + +`--path` (and MCP `path_between`, and the `` block of a `--for` path subject) printed the shortest chain the search +found, `reachable="1"`, even when several of its hops were `via="name"` calls whose receiver nothing proves. On Django, +`--path=QuerySet.__iter__,SQLCompiler.execute_sql` answered a 12-hop chain with 5 such hops through the prefetch code, and +`--path=Model.save,SQLInsertCompiler.execute_sql` a 5-hop chain through `QuerySet.aggregate`: neither is how the call gets there, +and nothing on the answer said so. One shared verdict now judges the chain a single-pair answer prints: + +- 2 or more by-name hops, and a chain over proven call edges only exists between the same definitions: that proven chain is + printed instead, with `route_hedged="proven-preferred" route_len= route_by_name=` describing the shorter by-name one. A by-name hop, a markdown link, a link to a missing anchor and one arm of a resolver split are not proven edges for this search; the one-implementor edge (`via="impl"`) is, and its row says so. +- 2 or more by-name hops and no proven chain: printed as before, hedged on the root with + `route_hedged="name-stitched" route_by_name="K" route_cands="P"` (K by-name hops, P the product of their same-named candidates). +- 4 or more by-name hops and `route_cands` of 64 or more (and no proven chain): refused as the answer (3 is only hedged: on CPython, `--path=asyncio.run,Handle._run` is a true chain with 3 by-name hops, and a true TypeScript or Go dependency-injection chain through interfaces carries 2 to 16 candidates, so it is hedged and printed). The root says + `reachable="0" hops="0" route_hedged="not-proven" route_len="N"`, no hop rows print, and `next=` follows `from=`'s callees (quoted, so a path with a space pastes into a shell). The + legend says reachable=0 here means "not proven", never "no route". `route_cands` counts in-repo same-named definitions along the route; a by-name call on a builtin or external receiver is not counted, so a low count is no proof. +- 4 or more by-name hops below that count: printed with `route_hedged="unlikely"` on the root (the chain is probably not the real route, said on the answer itself, not only in the legend); `next=` names the step that verifies it. + +A chain with at most one by-name hop prints exactly as before. An answer listing several definition pairs hedges its first pair's chain (`route_hedged="name-stitched"`, or `"unlikely"` from 4 by-name hops), and is never refused. The compact `path` legend now reads "reachable=0 hops=0 when none shown", so it no longer contradicts a refusal. + +### Fixed — a Python call through a from-imported submodule is a call + +`from . import mod; mod.f()`, `from pkg import mod`, `from pkg import mod as m`, `from .sub import mod` and `from .. import mod` now bind `mod.f()` to the module-scope `f` of that submodule, the way `import pkg.mod as mod` already did, so `--callers`, `--impact` and the map count these callers instead of reading zero. Before, such a call was read as a call outside the tree (external=). The binding is used only when nothing else may bind the name: the package's own namespace (a def, assignment, import of another file, star import, module `__getattr__` or `globals()` write), a rebinding in the importing file (including a star import placed after the from-import whose module binds the name, or any star import of a module outside the tree placed after it; a star import placed before counts too when the from-import sits under an `if`, `else`, `try`/`except`, `with` or loop, since that from-import may not run), or a second import of the name to another module each leave the call unproven, and an unproven call is declined and counted (declined_calls=), not picked. A submodule that writes its namespace dynamically, star-imports another module, or defines `f` only under an `if`/`try`, withholds the call too, and so does any second module-scope binding of `f` there (a later `f = g`, `del f`, or `from _speedups import f`, plain or under `try`). When both `p/mod.py` and `p/mod/__init__.py` exist, the package is the module, as in Python. A from-import inside a function binds a local only, so it neither proves nor contests the module-level name. A local or parameter named like the module is never the module. Not read yet: a package that re-exports `f` from another file (declined), and `pkg.mod.f()` written in full. Also not read: an `if TYPE_CHECKING:` guard, so `if TYPE_CHECKING: from p import mod` with `else: import p.mod_rt as mod` binds `mod` to two files and the call is declined and counted (before, the runtime import was picked). Known limits, still proven though Python binds otherwise: a submodule that writes `vars()['f'] = g` or runs `exec('f = g')` at module scope, and a package that assigns `sys.modules[__name__ + '.mod']` to another module. The module form `import pkg.mod as mod` followed by a star import that binds `mod` is read as before (proven). + +### Fixed — a Python call to a builtin or an imported third-party name no longer binds to a same-named def in an imported file + +`dict(...)`, `bool(...)`, `type(...)` or `float(...)` with no local, import or same-file def of the name, and a call to a name imported from a stdlib or third-party module (`from rich.padding import Padding`), used to be narrowed by Rule 3 to a namesake in a file the caller imports before the external-name veto could refuse it: on CPython 1,383 call edges and on Textual 25 (14 `type(...)`, 11 rich imports) went to an unrelated def. The veto now decides first, so those calls carry no edge. A file with `from m import *` of an indexed module, a name imported from an indexed module, a same-file or nested def, and a parameter or local of that name all keep their edge. Left as before: member calls with an unknown receiver, calls through a local variable, and a call through a module that exists in the tree under a stdlib name (`os.stat`, `inspect.signature` with an in-tree `os.py` or `inspect.py`). + +### Documented — README: an answer-quality-by-release table + +The README now shows an answer-quality-by-release table (in-sample, same questions and grader for every release). + +### Added — `--for` table rows: a write into a registry with a computed key, and the reads of that registry + +A `shape="table"` site can now be a write into a table with a computed key: `storage[resource+"/log"] = podStorage.Log` in Go, `handlers[name + "_svc"] = handler` in Python, ``table[`/api/${ver}`] = route`` in TypeScript. The row is listed when the hit is the key or the value of that one statement (not the table's own name, and not a later statement on the line) and the enclosing function is named `New*`, `new_*`, `Register*` or `register_*` (an `async_` prefix aside). The name is the evidence, not proof. Such a row carries `reg=` with the table and key up to the `=`, and it is a strong row, so a task with no table or registration word lists it too. A literal key (`storage["pods"] = x`, `handlers[0x10] = f`, `handlers[1_000] = f`) is left to the table rule as before, and so are a comparison (`storage[k] == x`), a line of a multi-line string that looks like a write, and a write in a function with any other name. + +When the answer above the block already names that table, the element also lists `` rows: reads of the table in the row's file and in the files of the function's callers. A read is `t[k]` that is not the left side of an `=`, or `t.get(`, in code: a read inside a comment or a string is not counted. Each read on a line counts. A local bound to the function's call (`s := p.NewRESTStorage()`, `const routes = registerRoutes(...)`) counts as the same table, in the row's own file as in a caller's; the call has to be the value itself, so `s := choose(p.NewRESTStorage())` binds nothing. Reads whose key shares a quoted part of the written key come first, and at most 2 are listed. `readers=` counts all the reads found. It is a floor, with `floor="1"`, when a file could not be read, when callers in more than 16 files were not read, or when no call graph was there to find them. When the `` rows would push the element past the table's byte cap they are left out, and `readers=` still counts every read. A read of a same-named table in a file that does not call the function is not listed. Where the answer does not name the table, there are no `` rows and the element is unchanged. A legend sentence rides only with a subscript row. Gate: `test/forregrowcheck.sh` (the subscript-write and table-reader arms). + +### Changed — `--for` reaches registration rows when a task asks where something is defined, built or handled + +The table block that ends a `--for` answer (``) now reads three more kinds of question and site. The words defined, declared, build, builds, built, handled, turned into and get(s) to start it, but like default and handler they print only beside a row: a table row, or now a registration row. `defined` and `declared` right after not or never (`name x is not defined`, `x was not declared`) are an error message, not a question, and start nothing. A registration row is an argument of a call that says it registers: a name with a register verb (`register`, `reg`, but not beside `read`, `write`, `get`, `set`, `peek`, `poke`, `load`, `store` or `dump`: `write_reg` and `get_registry` are lookups), an adding verb (`add`, `on`, `use`, `install`, `bind`, `map`, `connect`, `subscribe`, `mount`, `attach`) before route, command or handler (`add_route`, `addEventHandler`), or, for a decorator, a name with route, command or handler. `handler(x)`, `get_handler`, `logging.FileHandler`, `run_command` and `ParseCommandLine` register nothing and are no row. That covers a Python decorator `@websocket_command({"type": "get_states"})`, `component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")` spread over several lines inside a function, `registry::register_handler(x)`, and `async_reg(hass, handle_get_states)`, where the name is inside a longer one. Such a row carries `reg=` (the call up to its paren) instead of `tbl=`, because it is not a table opener and the callee's name does not prove the call registers anything. Not a call, and so no row: a declaration of the same name (a C++ constructor, a GNU-style `Registry::RegisterHandler(` definition, a JavaScript method `registerHandler(h) {`), a member of a type outside a decorator (a Rust enum's tuple variant, a TypeScript interface member), a C or C++ member-initializer list, and a statement inside a closure body (a JavaScript arrow function, a Go func literal) that sits in the arguments of a registration call. A Rust `$macro! {` inside a `macro_rules!` arm is now a table opener (risingwave's `for_all_params`). An unquoted dotted name that finds no site (`pydantic.ValidationError`) is tried again by its last part when that part is a symbol of the index (a backticked or quoted one is tried again either way); the retry lists only the rows where the last part is a quoted key (`'ValidationError': (...)`, `.method('redirect')`) and its own definition, and `tail=N` counts the sites it leaves out. Two dotted names with one last part read it once. An identifier the index has no symbol for (a type generated from a `.proto` file) is tried too. Both run after the task's other names, so they never use up those names' scans. Rows in generated and JSON files are ranked after source rows of the same nearness. + +Measured on the target questions (the same arguments as the gold-blind Stage 0 rule, unsealed copies): the registration rows that rule found now appear in the answer on risingwave-03 (both natural wordings), risingwave-04, homeassistant-03, homeassistant-15 and pydantic-03, and kubernetes-01 prints no block. Gate `forregrowcheck` covers Rust, Python, TypeScript, Go, C++ and JavaScript fixtures: the declarations, members, closure bodies and callee names above (each with a twin that must stay a row), a definition named like a handler, a closed call, calls not named for a registration, the dotted-tail retry on a common word and on a quoted key, and the error-message cue. + +What it does NOT do: it does not prove a call registers anything (the row quotes the call, nothing more). It does not follow a registration through a constant (`SERVICE_TURN_ON = "turn_on"`) or a Go map index assignment (`storage[resource+"/log"] = …`). It reads at most 4 lines and 512 bytes back for the open paren, so a registration call opened further up is no row, and the table legend says so; a missing `reg=` is no claim. An unquoted dotted name whose last part is no symbol of the index is not retried (a backticked or quoted one is). Parens inside strings and comments are skipped, which is a read of the nearest lines, not a parser. + +### Added — `--for` joins a registry key to what is registered under it (Python signals, TypeScript providers) + +A compact `--for` answer now carries a `` row with up to four `` registrations when the question is about a registry: Django-style `request_finished.connect( close_caches )` / `signals.request_finished.connect( … )` for a module variable `K = Signal()` whose `connect` stores its parameter into a container of `self` (the registrar is checked structurally, never matched by the name `connect`), and Angular/NestJS provider records `{ provide: TOKEN, useClass: X }` (`as=` names the field: `useClass`, `useExisting`, `useFactory`, `useValue`). The key is resolved through the site file's imports (and up to three re-export hops) to one definition, or there is no row. A row prints only when the question names the key or a registered name, or a shown ranked row consumes the key (`signals.request_finished.send( … )`'s function, a constructor with `@Inject( TOKEN )` or its class), and only with two or more non-test registrations; test registrations are counted in `tests=`, not listed. The legend states it is a static fact at the site (the call as written; conditional or dynamic registration may exist; `sender=`/`weak=`/`dispatch_uid=` are not read; not proof a receiver ran), that `n=` is a floor (only exact shapes are listed, so an absent row or site proves nothing; decorator, keyword-argument, `forwardRef`, unresolved-import, function-local-import, bound-method, conditional and non-literal provider registrations are not counted), and defines `consumers_more=`, `reg_more=` (keys past the two-row cap) and `cut=1` (a re-export chain over three hops was not followed). A name a function writes locally (a parameter, a loop variable, an assignment) is never keyed to the module-level definition, `import pkg` keys to the package's own file, a stored `sender=None`-style option is never listed as a receiver, and a provider value is paired only within one object literal. On "which receivers run when request_finished is sent" (Django) the answer now names `close_caches`, `reset_urlconf` and `close_old_connections` with their sites and the send sites (+0.7 KB); on "which event plugin handles (keydown.enter)" (Angular) it names the `DomEventsPlugin` and `KeyEventsPlugin` providers in browser.ts (+0.7 KB). Python files are re-parsed once (parser version bump). `RIPWIRE_NO_REG_ROWS=1` turns it off. Listing is exact-shape only, and an absent row (or an absent site) does not prove nothing is registered: a registration is listed only in a shape the index can read, and a shape it cannot read is left out and counted nowhere. Listed shapes: an aliased import is looked up under the written name; a Python registrar is a method whose one stored parameter is the registered value, whose other required parameters appear only as the subscript key of the store (`self.d[ name ] = handler`), and whose defaulted parameters are never themselves stored; a provider value pairs with the `provide` of the same object literal, and only when that literal is an element of an array (a call argument, a `const` initialiser or a `providers` / `viewProviders` property). Left out, with no row or no site: a function-local import, a lambda parameter / comprehension target / module-level `for`, `with` or `except` target named like the key, a bound method, a decorator or keyword-argument registration, a provider object in a ternary, `||`, `return`, reassignment or `let`, a registrar whose other parameter is validated, logged or stored, and an inline literal or arrow value. Round 4: a Python site counts only when its value resolves to a function (an event class, a label, a constant or a class registry such as `REG.register( PluginA )` is not a receiver, and class-registry rows are no longer listed); a TS provider literal counts only as an element of an array (a call argument, a `const` initialiser or a `providers` / `viewProviders` property) holding plain unique pairs, so a spread, computed, duplicate or accessor entry, `reg.remove( { … } )`, `console.log( { … } )`, a bare `register( { … } )` and a `const X = { provide: … }` object are not counted; a function-local `function K(){}` in a file that imports `K` gives no row. The legend lists what is not counted, what is counted without proof it ran (TS decorator providers, `if ( flag ) register( [ … ] )`) and what stays unproven. + +The registrar facts are no name evidence for the by-name call gate: a Python file holding a registrar method no longer reads as naming a class called `g` (the fact's internal scope letter), so a call such as `sys.path.insert(...)` is not bound by name to a class `g` defined in another file. + +### Fixed — `--naming-calibration` no longer mines a reworded comment as a rename + +The rename miner paired a removed and an added line that differ at exactly one identifier. A comment is such a pair whenever one of its words changes (`// the rows are read` → `// the rows is read`), so a grammar fix in prose was credited as a rename `are → is` and joined to any function called `is`, which moved the `naming-short` score of a pull request that touched no name. A pair whose two lines are both comment-only (a `//` or `/* … */` line, a `#` line where `#` opens a comment, or a `*` continuation line inside a block comment) is no longer a rename site. A comment after code on the same line leaves the line code, so a rename there still counts. A file in a language ripwire cannot tell comments from code in (Markdown, an unknown extension) keeps every pair. Measured on this repository's own history, the labelled pairs fall from 215 to 172; the `naming-short` rule goes from 10 fires (3 on the abandoned name, 7 on the chosen one) to 9 (3 and 6), `naming-case` and `naming-predicate` lose their one or two fires, and no rule has enough fires to be judged against its floor either way. + +### Fixed — a Rust or Python call Rule 3 narrowed no longer reads as proven without a written binding + +Rule 3 keeps the one imported file that defines a call's name. Member calls already took that answer as a hedge; a bare or qualified +call did not, so Rust `Ok(..)`, `assert!(..)` and Python's `conv()` through a local printed as plain edges to an unrelated namesake. +Those languages write what a name is bound to, so the writing is now the proof: a Rust `use` of the name into the target's crate, a glob +of the target's module, a qualifier (read whole from the root it is written with) naming its module or type; a Python import binding +that no local or parameter of the caller or an enclosing function shadows, or a star import whose chain reaches the target's file. +Without it the edge stays, marked `via="name"`, and only Rule 3's own file and the caller's own file are offered: no directory namesakes. +Which item a binding names is not compared with Rule 3's target, so a `use` or `from b import conv` that reaches a different definition +of the name still proves it. A Python call through a local or parameter holds a value, not a definition, so it gets no edge and counts +under `declined=`, also when the file imports the same name. A Rust qualifier that provably leaves the tree, read from its written root +(`oneshot::channel()`, `String::new()`, `std::io::stdout()`), reads as outside it; a path rooted at `crate`, `super`, `self`, a crate of +the tree or a package directory of the tree does not. `test/rule3memberguardcheck.sh`. + +### Fixed — `--deps` and `--report` stop reading Rust module declarations as dependency cycles + +A parent's `mod x;` and the child's `use crate::…` or `use super::…` drew edges both ways, so every parent and child read as a +cycle and a crate read as one tangle: with true edges injected, nushell's largest cycle held 228 files. The parent-to-child +edge of a `mod x;` pair, with the child at the module-file rule's own location (beside a `mod.rs` or a crate root, or under +`//`), is now containment when the declaration is its only reason: it leaves `--deps`' cycles, +`ccd`/`acd`/`nccd`/`shape` and a row's `transitive=`, and `--report`'s cycle list, and stays in `afferent=`, `instab=`, +`godfiles` and `stabledeps`. A parent that also uses its child (`pub use child::Thing;`, `use self::child::…`, a call +`child::f()`) depends on it, so that edge stays and a cycle through it is still reported. `` and +the `--report` heading count the pairs left out, and `--report` says "acyclic without the containment pairs" rather than a bare +"acyclic". On this build's own edges nushell leaves out 725 pairs and keeps three cycles of 3, 2 and 2 files, each a parent +that re-exports a child which uses the parent's items; zed leaves out 289 and has none. A crate root is `lib.rs`, `main.rs`, or a +file no `mod` declares (a `[lib] path`, a `[[bin]]`, an integration test). Not covered yet: `#[path]` modules and a `mod` inside +an inline `mod { }` (both read as the parent's own `mod x;`), and `--arch`'s `propagation_cost`, which still walks these pairs. + +### Fixed — Rust file edges: `crate::`, `super::`, `self::` and `mod x;` resolve in the includer's own crate and module + +The Rust import probe anchored every `crate::` path at the first `lib.rs`/`main.rs` of the tree, so in a workspace most +`use crate::…` lines either drew nothing or drew an edge into another crate. It now reads, for each file, the crate it is +compiled in: the nearest `Cargo.toml` above it and that manifest's target (`[lib] path`, `src/main.rs`, `[[bin]] path`, +`build.rs`, and Cargo's `src/bin/`, `tests/`, `examples/`, `benches/` directories). Alongside: + +- `mod b;` in a file `a.rs` (not a `mod.rs` or a crate root) is `a/b.rs` or `a/b/mod.rs`; the includer's own directory is + read only when neither exists (the `#[path = "b.rs"]` shape, whose attribute is not captured). +- `super::` in a plain file `a/x.rs` is module `a`, `super::super::` climbs twice and never above the crate root, and + `self::` is the file's own module; a one-segment `super::x` reaches the parent module's own file. +- `use crate::a::b` where `a/b.rs` exists draws `a/b.rs` even when `a.rs` exists too: the full path as a module file is + read first and the parent module's file only when it misses, each step unique-or-degrade on its own. +- A brace group (`use crate::{a::b, c::{self, D}}`) resolves each member, and an `as` rename resolves the path it renames. +- A directive inside an inline `mod x { }` reads from that module, not from the file's: in `#[cfg(test)] mod tests { use + super::*; }` the first `super` is the file's own module (no edge), a `super` past the inline depth climbs from the file's + module, `self::` is the inline module itself, and `mod y;` is `x/y.rs` below the module directory. + +Measured on three in-sample trees (distinct file pairs from `.rs` includers, base main 292a26dec vs this change): +mdbook 64 → 108 (+44, none removed), axum 139 → 331 (+208; 16 removed: 15 edges into the `axum-core` crate from `axum` +files and one `self::rejection` that names an inline module), nushell 1,870 → 3,001 (+1,136; 5 removed, all into +another crate). The call narrow (the import evidence Rust call resolution reads) takes the same edges except brace-group +members, which it still leaves unresolved: there it adds 30 / 64 / 405 pairs and removes the same 0 / 16 / 5. Read as a +member call's namesakes by name, a group's edges turned two declined calls into name-only edges (card RULE3-MEMBER-GUARD). +The Rust crate-root table is built once per run and shared by the file graph, the call narrow and the containment cut +(each used to re-read every `Cargo.toml`). + +What it does NOT do: a workspace crate named by its package (`use nu_protocol::…`) still draws no file edge (card +RUST-ROOTS-B), a one-segment `crate::Item` does not reach the crate root file, and `#[path]` values other than the +module's own name are not read. + +### Added — `--for` names where a base class reaches a served Python override through `self` + +A served Python method whose name has two or more definitions in its inheritance cone now carries a `` element: the override +relation (`overrides=` / `op=` when the inheritance edges prove which base definition wins, `candidates=` when two unrelated bases define it, or `hook="1"` when only subclasses define it) and the `self.x()` / `cls.x()` sites in its base classes, +with a `hasattr` / `getattr` guard said `guarded="1"` and a reference without a call said `form="ref"`. `--for="what calls +SessionMiddleware.process_request"` now names `MiddlewareMixin.__call__` and `__acall__`, which `--callers` cannot bind. The list is +hedged (a call through `self` reaches this override only for instances of its class) and adds no edge, no count and nothing to `--impact`. +Every site is read from the parse: a docstring or a string is no site, and a rebound `self`, a nested def, a lambda or a static method is no site and is counted in `skipped=`; a method whose class or an ancestor has a by-name base beside a proven one is not listed (the by-name base may come first in the resolution order); a base file that cannot be read again is said in `unread=` on the row, not counted as `skipped=`; `next=` runs a verb that lists the rest; under `--token-budget` only `` is printed. Other answers are byte-identical. `RIPWIRE_NO_SELF_HOOK` turns it off. + +### Fixed — `--for` gives an unresolved code-shaped name inside a sentence the same fallback ladder a bare name gets + +- `--for`: a task the route reads as words (`definition of ChunkPtr, the lookup is empty`, `where does Wrong.formatDate come from`) now prints the `` fallback ladder when it holds exactly one code-shaped name and no definition of that name resolved. Code-shaped means a qualified spelling (`Q.leaf`, `Q::leaf`, every segment starting like an identifier, a leaf that is no file or host suffix) or a camelCase / snake_case word; a plain capitalised word (`Widget`, `Python`) is not one, and a bare word that is a segment of the qualified spelling beside it counts as that name. Unresolved means a bare name that no symbol in the index carries, or a qualified name whose fully proven definition the answer did not print; a qualified spelling that names a file gets no ladder: an indexed file's basename, its module path (`http.server` for `Lib/http/server.py`) or a module path down to a dotted basename (`app.module` for `src/app/app.module.ts`), or a file at the root the index does not hold (`go.mod`, `build.ninja`). A span that ends in a file suffix (`CMakeLists.txt`, `src/net/HttpClient.cpp`) is a file name, so none of its words is a name. The block carries `from="sentence"`; its rungs (`leaf`, `substring`, `literal`), the qualifier proofs, `exact=`, `next=`, the row caps, the held marker under `--token-budget` and `RIPWIRE_NO_SHORT_KEY` are those of the bare-name ladder. A task with two or more code-shaped names, a name that resolves, or no code-shaped name prints no block. An empty rung means the index holds no such definition; a file the index skips can still define the name, and `next=` searches the tree. Gate: `test/barrelshortkeycheck.sh` arms D1-D14. + +### Added — a dotted string that names a shown Python definition + +`--for` and `--callers` now print `` when a Python string literal whose whole value is a dotted path resolves to a row +the answer shows: `_builtin_context_processors = ("django.template.context_processors.csrf",)` beside `csrf`, with the function in which +the constant's name reaches an import sink's argument through that function's own assignments. A `%`-template handed to an import sink (`import_module("%s.base" % name)`) gets a hedged +row when the directories of three or more shown files fill it. The string is matched through its whole path, never a bare name; whether it +reaches the sink is not traced, so nothing enters a count, `reaches=` or `--impact`. `RIPWIRE_NO_DOTTED_STRINGS=1` turns it off. + +The compact legend entry for a tests-only `` no longer contains a double hyphen, which made the whole `--callers` answer ill-formed XML (an XML comment cannot hold it) when the only site of a string was in a test path. + +A sink call whose flow tokens are over the size cap, or that names nothing, joined no constant and was dropped without a trace, so `loaders=` could read as a complete count. The row of a constant such a call could name (same file, or a file importing the name from the constant's file) now says `loaders_floor="1"`, defined in both legends. + +### Fixed — `--for` anchors the symbol a subject names, and says when no source defines it + +- `--for`: a descriptive subject that names ONE function can anchor it. Where the answer on that function alone prints its body and its callers (one function in the whole index, a body that fits the single-anchor allowance, at least one caller), the route trades its ranked map for the function, and `route=` lists the plain words beside it under `dropped:` (`transformNodes new parameter`, `how does X work`). Every other call keeps the route and answer it had, the low-confidence note below aside: a subject that names several symbols, a type, or a function that fails that test, and a call that prints no body or no callers (`--json`, `--detail`, `--signatures-only`, `--token-budget`, `--format=candidates`, the `--limit`/`--offset` file page, an explicit `--pack-budget-bytes`). A conceptual route whose descriptive subject names a function carries `` on top. The MCP `for` verb, which serves signatures only, keeps its map. +- `--for`: `confidence="high"` on a name-exact route needs a source definition behind the anchor. An anchor only a test, doc or data file defines reads low, and `route=` says why. A plain word never vouches for a subject that spells a name: `max_concurrent_queries read`, whose key lives in a data file, reads low although `read` is defined in source. + +### Fixed — `--for` prints the callers of the one function a sentence asks about + +- `--for`: a sentence that names one function and asks what depends on it now gets that function's `` block, as the short subject already did. Examples: "I want to add a new parameter to transformNodes; what would that affect?", "who calls async_prepare_setup_platform and would break if its signature changed?", "I need to change how decompress_data works; what depends on it?". The plain-word limit now leaves out the task frame: question leads, pronouns, helpers ("I need to"), conjunctions and prepositions. The sentence also needs a dependents cue (change, add, affected, breaks, depends, calls, used, update, modify, invokes, references, rename, exercise), read in its dependents sense, and exactly one named function. A sentence without one reads as before, for example "how does X work", "where is X defined" or "how do A and B relate". So does a cue that asks what the function itself does ("what does X depend on", "what does X modify"), a noun or purpose ("API calls", "used to"), a negated cue ("I changed nothing in X"), and a sentence naming two functions ("when X changes, who calls Y?"). This change does not alter the route: only a short descriptive subject can take the name-exact route, through the existing flip checks, and the rest of the answer is byte-identical. Gate: `test/subjectanchorcheck.sh` arms SC. + +Answers, against main with the same arguments: of the 60 practice questions, the stored wording is unchanged (26 Complete, 118 items); the first natural wording goes from 25 to 27 Complete and 114 to 118 items, the second from 20 to 22 Complete and 105 to 108 items, and no question loses a tier or an item. Every question that moved gains the new block. Of 321 in-sample calls, 316 print the same answer as main (timestamps aside) and 5 that already printed the block gain its legend clause "(several: the first with callers)". + +### Added — a `--for` task about a table or a registration reaches the sites of the identifier it names + +When no other literal of the task has a source site, `` makes a last try, `shape="table"`. It needs one of these in the task's own words, outside backticks: a registration word (`register`, `registered`, `registry`, `registrar` and their forms), `table`, `compile-time` or `compile time`, `add a new` or `implement another`; or `default` or `handler`, which print only when a site reads as a table row. Its anchors are the task's backticked spans, then identifier-shaped words, then a plain word after `like`, `alongside` or `following`: any word that is not backticked must be the name of a symbol in the index. A backticked span or a word after `like` is the thing to read and prints its sites; a bare identifier-shaped word prints only beside a real table row, so `add a new test for run_it` or `fix register allocation in run_it` print nothing, and so does a backticked span right after `to` in an add task (``add a new argument to `run_it` ``, the thing changed). A hit stands on word edges; a hit inside a longer word is counted in `longer=`. + +Each anchor with a source site prints its own ``, the legend on the first only. A site that reads as a table row ranks first, wherever it stands: a map or dict key, a list member, a `{ name, ...` row or a call beside a sibling of the same shape at its indent, under an opener the allowlist knows, or a macro or ALLCAPS call outside any function (a Go map entry, a Rust macro row, a C X-macro line). The allowlist's openers are data: an assignment, a `return` or a key whose value is a bracket (a `(` only in Python, where it is a tuple; in a TS or JS function body a bare `{` is an object literal, not a table), a typed composite (`[]T{`, `map[K]V{`, `new T[] {`), a macro's bracket or a named wrapper (`Object.freeze({`, `new Map([`, `frozenset({`, `Map.of(`); `export default {`; an enum's members; a `#define`; a struct literal outside a function. Anything else makes no claim: a call's arguments, an arrow function's parameters (`export const f = (`), an options object a function builds (`const opts = {`), a destructuring import (`const {`), a callback, a statement, a `struct`, `interface` or `type` declaration, a Java `static {` block. Such a row carries `tbl=`, the line that opens the table (`item` for a top level call), and the first four carry `lines=a-b`, the lines after them. This is a reading of the line's text, not of the parse; a row with no `tbl=` is no claim either way. Inside a function body a call is a statement and never a row. Past 4,096 source sites the rest are not read for a table row: `read=` says so, and an anchor that needs a row and has none in those read is counted by ``. Rows stop at 32 per anchor and all the elements of a task at 40,000 bytes, cut on a row boundary with `capped="1"` and `next=`. At most 3 anchors print and 4 are scanned (an anchor with no site costs a scan); every anchor past a cap, or left without room, is counted by ``. Everything before the element stays byte-identical. Gate: `test/forliteralcheck.sh`. + +### Added — `--for` answers "which public X in are unused" + +A `--for` subject with an unused cue (`unused functions in util/network.py`, `quoteString.h unused functions`, `which exports of src/path.ts are never called`) that names a file or directory now carries an `` table: one row per public def, with a verdict that says what was proven. `used` means a non-test caller elsewhere (or an in-file chain from one); `test-only` that every reference found is in a test; `file-only` that only the same file refers to it; `unused` that the name appears nowhere in the tree outside its definition, declarations and comments. Anything an index cannot follow reads `unknown`, with `why=` naming it: a word no edge explains, a string lookup, a built name, a re-export, a star barrel, dynamic dispatch, a decorator or attribute, a surface consumers outside the tree may use, a build file. `--dead-code` keeps its internal-linkage meaning; a `--dead-code=PATH` root now names the table on `next=`. + +### Changed — a warm run no longer holds extra copies of the cached facts + +A warm run used to decode every cache record into one path-keyed map before the parse workers started, move each file's facts into a worker's arrays and move them again in the merge, so the parse-pool join held up to three sets of fact arrays. On envoy that put the warm `--deps` peak memory footprint 45 % over the cold run that wrote the cache. Now the cache load reads only each record's head (its key, hash and family counts), and the parse worker that takes a hit reads that one record, checks its checksum and key, and decodes it straight into its own arrays. The merge moves one fact family at a time and frees each worker's array as soon as its rows have moved. On envoy the warm peak drops 37 % and sits 3 % under the cold one; the answers are byte-identical to a cold run and to `--no-cache`. A record that fails its read, checksum, decode or key check in the worker is parsed from source after the pool joins, so a torn or colliding record still costs only time. The cache format moves to 49 (the record head gains the eight family counts), so an older cache is rebuilt once. New gate `test/warmpeakcheck.sh` pins the warm peak against the cold one. + +### Fixed — a refused file no longer rewrites the whole cache on every warm run + +A file the nesting guard refuses (a JSON, YAML, Markdown or Kotlin file nested past its ceiling) is stored in the cache as unknown on purpose, so every warm run re-reads it, re-refuses it and lists it again under `--skipped`. That re-read also marked the cache changed, so the whole cache was serialized and written again on every warm run of a tree holding one such file; on envoy that was a 146 MB write per run. A cache miss that ends before the parse (unreadable, binary or refused) now marks the cache changed only when the cache's record for that file differs from the one this run would write. A file that becomes refused, or stops being refused, still updates the cache. The refusal note and the `--skipped` row are unchanged, and the cache format is unchanged. `RIPWIRE_CACHE_STATS`' `reparsed=` no longer counts an unchanged refused file. New arms (w1)-(w5) in `test/savecachecheck.sh` pin both directions. + +Not covered yet: a file whose extraction was partial is still stored as unknown with its facts, so a warm run re-parses it and rewrites the cache as before. None of the seven large repositories measured has one. + +### Added — an alias leads to what it names: `--for` follows the chain, `--uses` lists the sites written through it + +`--for=ColumnPtr` printed `using ColumnPtr = COW::Ptr` and stopped there: what the alias stands for (`COW::Ptr`, then `using Ptr = immutable_ptr`, then the class `immutable_ptr`) and the class it points at (`IColumn`) each took another call. A `--for` answer now ends with an `` block when the name the task spells (the route's name, or a sentence's one code-shaped name) has an alias definition: one `` per distinct walk, one `` row per definition reached with its declaration text and `d=` hops, the template argument of the first target as `arg="1"` (the pointee of a smart-pointer alias) with its member count and first names, and a `` for a spelling no definition in the tree binds (`std::shared_ptr`, a template parameter's member). The walk is bounded (4 hops, 3 definitions per spelling, 3 chains, `cycle="1"` / `depth_capped="1"` / `capped="1"` when they cut), sits outside `est_tokens`, and under a token budget prints a one-line held marker. Under `--max-tokens` with `--detail=N` (the one case `--for` honors that ceiling; a bare `--max-tokens` is noticed as ignored and the block prints whole) it leaves out the member names (`m=`) and the chains that reach only leaves, and says so with `shed=`. Its legend explains only the attributes the block prints. A written qualifier is checked against the namespaces and classes around each definition, read from the file's braces (a C++ class's own scope is just its name): `using WQ = N2::Widget` binds the class in `N2` and never a namesake in `N1`. A leading `::` (`using W = ::Widget`) binds the global definition, and a space beside `::` (`N :: Widget`) is still one name. When a bind cannot be proven (the braces do not balance, at the target or at the alias itself), the row says `bound="name"` instead of claiming the target is not in the tree. A string literal in the target (`Tok<"…">`) is no name, and the declaration text, `to=` and leaves are redacted like every other printed source line (`--no-redact` passes them through). `--defs to=` is redacted the same way. A type's `members=` counts its own member functions only. The block is printed by the CLI in XML only: the MCP `for` tool, `--json` and `--format=columnar` carry none (`--help` says so). `RIPWIRE_NO_ALIAS_HOP` turns it off. + +`--uses=IColumn` now also lists the sites written as an alias whose chain reaches `IColumn` (`ColumnPtr`, an alias of that alias, `std::shared_ptr`), each marked `via="alias" alias="ColumnPtr"`, after the direct rows. A site counts in `count=` and `via_alias=` only when the alias in scope there (the innermost one of that name visible from the site) is one that reaches `IColumn`; other sites written with the same name are listed last with `hedge="name"`, counted in `alias_hedged=` and not in `count=`. `alias_namesakes=` says how many other definitions share an alias's name, and `alias_depth_capped="1"` says the reverse walk stopped at 4 hops with an alias still to follow (one hop per round, whatever the declaration order). An alias declared later in the same file is not the one in scope at an earlier site, and a site whose braces cannot be read is hedged when the alias name has namesakes. A `file:Name` or `Scope::Name` selector lists the alias sites of the definitions it chose. The columnar form counts them as `alias_sites_held=` instead of listing them. A definition inside another translation unit's source file (an example program's own `class IColumn`) is not what a header alias names while another definition of the name remains. + +C++ only for now (headers included): its alias definitions are the ones that record a target. A TypeScript or Rust `type X = Y`, a Rust `use … as`, and a Python module alias get no block yet. The MCP and LSP `uses` answers list no alias sites. Gate: `test/xfaliashopcheck.sh`. + +### Fixed — a qualified `--connect` terminal and `--edit-check` selector answer about the definition they name + +`--connect=ProcessList::insert,InterpreterFactory::get` already searched only the two qualified definitions, but each terminal row printed the bare name's definition count (`defs="196"`, `defs="645"`) under a legend saying all of them were searched, which read as the qualifier being dropped. A terminal spelled with an owner, module or file (`A::m`, `Class.method`, `ns.fn`, `pkg.mod.fn`, `file:name`) now reports the definitions its spelling searched (omitted when one). On TS/JS, which store no owner scope, `--edit-check=Alpha.run` (and the MCP `edit_check`) unioned a same-file `Beta.run`, and `ns.make` a sibling namespace's `make`, so the contract listed the other definition's callers; a selector that names an owner now keeps only the definitions it resolves to. The edit preview (`--edit-check=SYM --edit-payload=FILE --dry-run`, MCP `edit_check` with `new_body`) narrows the same way, so it agrees with the answer it predicts. The connect legend now says `defs=` counts the definitions the terminal's spelling matched, and when two spellings pick one definition (`--connect=useAlpha,run,Alpha.run`) their shared row prints the wider count. Bare names get the same answers; only the connect legend text changes. New gate `test/qualendpointcheck.sh` pins every endpoint verb's resolution of these spellings in C++, Python and TS, and the refusal of an unknown qualifier. + +### Changed — the versions this change moves, stated once + +`kParserVer` 259 → 301 (the mirror in `src/quality.h` is 301 too; later changes bumped it from 283 to 301, each one a grammar or extraction change that makes an older cache replay older rows). Steps on the way: 283 (train 43b): Python registrar facts and TS/JS provider-record literal marks (their branch +carried 273–281), Python dotted-string and import-sink facts (their branch carried 274–275). 271 (file tag 272): the C++ member-pointer slot records (a `&Cls::m` value reference, a +`( o.*x.f )( … )` slot call, a C nested designated path or macro-cast slot call, a class body fact's scope path, and a C++ +base that is a template parameter of the deriving class; 269 on main), the interface-receiver peel's records (a C++ +smart-pointer alias carries its pointee, a member whose written type is a template parameter is marked, a Rust `&dyn T` / +`Box` / `impl T` result is recorded) and the TS/JS re-export specifier records each carried lower numbers on their +branches (257–262), so a cache written before them would replay the older rows. 271 sits above every live branch and +pull-request number (the string/literal records carry 263, 265 and 267 on theirs). `kCacheVersion` 46 → 49: a re-export +specifier is a new binding kind appended to the cache record (48; its branch carried 47), and the file record's head gains the eight family counts (49, #556). A cache written by 0.6.9 is re-parsed +once. The quality-snapshot scheme moves 24 → 25 (#472: `isTestPath` reads more shell test names, which moves the dead set's test-script exemption; no snapshot record changes shape, and a v24 blob is rebuilt once). The session legend dictionary is `dictv=c43b9c72ce2a139a entries=1050`. + +The history-walk cache's `kQChurnCacheScheme` moves 3 → 4 (#433: a blob written by a pre-fix binary holds the old line-frame reader's stream, which for the repositories the framing fix targets is the corrupted one, and no key component can tell that blob from a fresh read; every repository's history is walked once more after the upgrade, and on trees with ordinary names the walk produces the same stream, so the cost is one cold walk and no changed answer). + ## [0.6.9] — 2026-10-29 Twelve merged batches of reviewed changes (pull requests #424 to #463). What a user sees (each entry below states its own measurement and its floors): diff --git a/CMakeLists.txt b/CMakeLists.txt index 4910661e6..bd5540548 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) # The version string lives in exactly ONE place — this project() call. --version (main.cpp) # reads it via the generated version.h below; test/versioncheck.sh asserts the two never drift. -project(ripwire VERSION 0.6.9 LANGUAGES C CXX) +project(ripwire VERSION 0.6.10 LANGUAGES C CXX) # ---- language standards (C++23 for our own code / C11 for the vendored C dependencies) ---- set(CMAKE_CXX_STANDARD 23) diff --git a/README.md b/README.md index bf72ed73d..b69a00b2e 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ claim cannot quietly drift. The row-by-row ledger is JavaScript · Java · Ruby · PHP · Lua · Elixir · Dart · Kotlin · GDScript · Clojure · Bash · C# · JSON · TOML · YAML · Markdown — see [language support and limits](#languages). -**Latest: 0.6.9** — hedged or declined answers instead of confident wrong ones, `--path` that prints the bodies it names, `--defs` and owner-named rows. [Release notes](#release-notes) · +**Latest: 0.6.10** — more complete `--for` answers, warm runs on large repositories that use 35 to 67 % less CPU, and about half the peak memory of an index on macOS. [Release notes](#release-notes) · [the presentation](present/ripwire-showcase.pdf) · [the changelog](CHANGELOG.md) — with thanks to the contributors named there. @@ -1058,7 +1058,7 @@ report back. The exe is not code-signed. `Expand-Archive` does not pass the down files it extracts, so SmartScreen does not prompt for an exe unpacked this way; no prompt is not a verdict. In PowerShell: ```powershell -$v = "0.6.9"; $a = "ripwire-$v-windows-x64"; $u = "https://github.com/redhat-et/ripwire/releases/download/v$v" +$v = "0.6.10"; $a = "ripwire-$v-windows-x64"; $u = "https://github.com/redhat-et/ripwire/releases/download/v$v" Invoke-WebRequest "$u/$a.zip" -OutFile "$a.zip"; Invoke-WebRequest "$u/$a.zip.sha256" -OutFile "$a.zip.sha256" # extracts only when the SHA-256 matches; on a mismatch it throws and nothing is unpacked if ((Get-FileHash "$a.zip" -Algorithm SHA256).Hash -eq (Get-Content "$a.zip.sha256").Split(" ")[0]) { Expand-Archive "$a.zip" -DestinationPath "$env:LOCALAPPDATA\Programs" } else { throw "SHA-256 mismatch: do not run $a.zip" } @@ -1649,20 +1649,20 @@ cmake --build build 2>&1 | ./build/ripwire . --from-trace=- Our goal is for ripwire to be the best tool an agent can have for understanding and changing code: complete answers, nothing stated wrongly, and as few bytes as possible. We measure every release against that goal. Each release answers the same questions with the same commands, graded the same way. These are **in-sample** questions: ripwire was developed against them, so the table shows progress between releases, not held-out performance. A held-out comparison comes with 0.7.0. -| | 0.6.5 | 0.6.7 | 0.6.8 | 0.6.9 | -|---|---|---|---|---| -| Everyday code questions (163): complete / partial / none | 50 / 69 / 44 | 81 / 57 / 25 | 112 / 38 / 13 | **129 / 31 / 3** | -| Questions whose answer spans several files (39) | 4 / 26 / 9 | 4 / 27 / 8 | 5 / 27 / 7 | **12 / 23 / 4** | -| Questions on large repositories (56) | 2 / 35 / 19 | 5 / 38 / 13 | 14 / 37 / 5 | **23 / 29 / 4** | -| Everyday answers that stated something wrong | 3 | 0 | 0 | **0** | -| Bytes per complete answer (everyday / large repos) | 22.5 KB / 182 KB | 14.8 KB / 123 KB | 16.5 KB / 91 KB | **15.1 KB / 60 KB** | -| Bytes per required item delivered (everyday) | 1.7 KB | 1.4 KB | 1.7 KB | **1.7 KB** | -| Newly complete (everyday): questions, median size | 50, 4.7 KB | +31, 4.6 KB | +31, 16.8 KB | **+18, 11.9 KB** | -| Size change, questions already complete (everyday) | — | +4 % | 0 % | **+1 %** | +| | 0.6.5 | 0.6.7 | 0.6.8 | 0.6.9 | 0.6.10 | +|---|---|---|---|---|---| +| Everyday code questions (163): complete / partial / none | 50 / 69 / 44 | 81 / 57 / 25 | 112 / 38 / 13 | 129 / 31 / 3 | **132 / 27 / 4** | +| Questions whose answer spans several files (39) | 4 / 26 / 9 | 4 / 27 / 8 | 5 / 27 / 7 | 12 / 23 / 4 | **19 / 19 / 1** | +| Questions on large repositories (56) | 2 / 35 / 19 | 5 / 38 / 13 | 14 / 37 / 5 | 23 / 29 / 4 | **30 / 25 / 1** | +| Everyday answers that stated something wrong | 3 | 0 | 0 | 0 | **0** | +| Bytes per complete answer (everyday / large repos) | 22.5 KB / 182 KB | 14.8 KB / 123 KB | 16.5 KB / 91 KB | 15.1 KB / 60 KB | **18.0 KB / 66 KB** | +| Bytes per required item delivered (everyday) | 1.7 KB | 1.4 KB | 1.7 KB | 1.7 KB | **2.1 KB** | +| Newly complete (everyday): questions, median size | 50, 4.7 KB | +31, 4.6 KB | +31, 16.8 KB | +18, 11.9 KB | **+4, 25.5 KB** | +| Size change, questions already complete (everyday) | — | +4 % | 0 % | +1 % | **0 %** | Harder questions take more bytes to answer. 0.6.5 already answered questions such as *Where is the `Compositor` class defined?* (one definition, 3.2 KB). The questions that first became complete in 0.6.8 and 0.6.9 include *How does Hono choose its router, and how does SmartRouter work?* (16.8 KB), *Who calls `options_get_number`?* (all 121 direct callers, 11.5 KB) and *How does a message get from `post_message` to a handler method like `on_button_pressed`?* (14.0 KB): several functions across files, or a long list. That is why the newly complete questions need more bytes while questions already complete stay about the same size. -*Complete* means every required item is in the answer; *partial*, at least one but not every required item, and nothing wrong; *none*, no required item, or something wrong. A bare name printed next to its file or qualifier counts. Large repositories: envoy, nushell, terraform, cpython, vscode, cockroach and zed. There was no 0.6.6. Bytes per complete answer fell overall: from 22.5 KB to 15.1 KB on everyday questions and from 182 KB to 60 KB on large repositories. *Bytes per required item delivered* divides all answer bytes by the required items credited, partial answers included (682 items in 0.6.5, 1,142 in 0.6.9). *Newly complete* counts the questions a release answered completely for the first time (for 0.6.5, all its complete answers): the harder questions that became complete later need more bytes. *Size change* is the median per-question change on questions complete in both this and the previous release. Earlier release notes quoted a 173-question set that included 10 held-out questions, graded with the rules of that time (0.6.8: 50, 83, 98 complete under the stricter method); this table re-runs every release on the 163 in-sample questions with today's grader, so its numbers differ. +*Complete* means every required item is in the answer; *partial*, at least one but not every required item, and nothing wrong; *none*, no required item, or something wrong. A bare name printed next to its file or qualifier counts. Large repositories: envoy, nushell, terraform, cpython, vscode, cockroach and zed. There was no 0.6.6. Bytes per complete answer fell from 0.6.5 to 0.6.9, from 22.5 KB to 15.1 KB on everyday questions and from 182 KB to 60 KB on large repositories, and rose in 0.6.10 to 18.0 KB and 66 KB: 0.6.10 answers are larger (everyday answers total 2.37 MB against 1.95 MB with the median answer unchanged at about 10.7 KB; multi-file 722 KB against 536 KB; large repositories 1.97 MB against 1.38 MB) and the extra bytes bought 10 more required items on everyday questions, 30 more on multi-file questions and 16 more on large repositories. *Bytes per required item delivered* divides all answer bytes by the required items credited, partial answers included (682 items in 0.6.5, 1,142 in 0.6.9, 1,152 in 0.6.10). *Newly complete* counts the questions a release answered completely for the first time (for 0.6.5, all its complete answers): the harder questions that became complete later need more bytes. *Size change* is the median per-question change on questions complete in both this and the previous release. Earlier release notes quoted a 173-question set that included 10 held-out questions, graded with the rules of that time (0.6.8: 50, 83, 98 complete under the stricter method); this table re-runs every release on the 163 in-sample questions with today's grader, so its numbers differ. A grader input lost in a cleanup was restored (GRADER-FIX-1107), so the 0.6.9 and 0.6.10 columns are graded by the same scripts and read as shown. One everyday question that read complete in 0.6.9 reads none in 0.6.10: textual-09, whose `--impact` answer now names the test file that exercises one of the eight definitions called `arrange`, which the mechanical wrong-name check counts as a wrong name (read in the source, that test edge is true, so the question is counted in *none* but not in the wrong-answer row). hono-17, a dead-code question with no required item, stays complete: its answer grew from 1.0 KB to 3.4 KB by also listing eight exported functions marked `unknown`, which the rule for exports flagged as public API accepts. The 0.6.5 to 0.6.8 columns are the numbers published with those releases; the scripts as they run now read them 2, 1 and 1 partial answers lower (50 / 67 / 46, 81 / 56 / 26, 112 / 37 / 14 for 0.6.5, 0.6.7, 0.6.8). The multi-file and large-repository columns are unaffected.
Read this first if you are here to check whether the tool is oversold — every number with its instrument and corpus, plus the claims this project deliberately does not publish @@ -2141,9 +2141,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-773 gate scripts (counts snapshot 2026-11-07), five contracts no unit test can hold, and the house rule: write the gate before the code it measures +776 gate scripts (counts snapshot 2026-11-07), five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` named **773 gate scripts** at the counts snapshot 2026-11-07 and is the authoritative list; +`test/regression.sh` named **776 gate scripts** at the counts snapshot 2026-11-07 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 @@ -2903,10 +2903,10 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. At the counts snapshot 2026-11-07 the tool had 396 compile-time caps and 8 ranking parameters. +Another gate derives the cap inventory. At the counts snapshot 2026-11-07 the tool had 403 compile-time caps and 9 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` -lists the measured cost of each cap. +lists the measured cost of the caps the sweep has measured, and names the ones it has not. ### 10. Quality evaluation @@ -3117,6 +3117,8 @@ terms. ## Release notes +**ripwire 0.6.10 — more complete `--for` answers, much less CPU and memory on large repositories, and fewer coupling claims the index cannot back.** On our fixed in-sample questions (the same questions, commands and grader as the answers table above), everyday answers complete went from 129 to 132 of 163, multi-file from 12 to 19 of 39 and large repositories from 23 to 30 of 56, with no everyday answer stating something wrong; the answers are larger (everyday 2.37 MB against 1.95 MB in total, large repositories 1.97 MB against 1.38 MB), and one everyday question that read complete in 0.6.9 reads none (the footnote above names it). `--for` reaches registration rows and the registry key to what is registered under it (Python signals, TypeScript providers, computed-key registrars and their readers), the sites of a literal, a `--flag` and its default, the family an add-a-new-X task joins, the manifest lines that list a file, Cargo `[[bin]]` and package.json `bin` / `main` entries, the doc sections for a prose question about the docs, re-export and alias hops, both sides of a missing `path from A to B`, the callers of the one function a sentence asks about and the implementations that can run at an interface call. `--path` and `--connect` hops print the call site and the callee's signature, `--impact` sums up the importers a cut listing hides and answers a file path, `--defs` prints an alias's target, reST and Sphinx documents are real units, and `ripwire wrap claude` routes edits through a ranged Read and Claude Code's native Edit. Fewer claims are false: `--arch`, `--deps`, `--cochange`, `--impact` and `--export=cc.json` no longer report coupling from edges the index never had, `--deps` stops calling every deep graph `tangled`, a "not found" from an index the memory guard cut no longer reads as proof of absence, `unused` is printed only when the scan can prove it, and a call to a Rust prelude name, a macro with no in-tree binding or a Python builtin no longer binds a same-named definition. Faster and lighter, as percent changes (each is that pull request's own measurement against the build just before it, on one machine; absolute seconds and megabytes come with the 0.7.0 table): warm `--deps` and `--for` on large trees use 35 to 67 % less CPU (vscode `--deps` -67 %, cockroach `--for` -57 %, envoy `--deps` -35 %, #542) and warm TypeScript `--deps` a further 22 % less (#554); on macOS the peak memory of an index is about halved (-47 to -49 % on envoy, cockroach and vscode, #497), a warm run's peak is 30 to 40 % lower and at or under the cold run's (#556), the include closure of a large tree is held in an eighth of the memory (#505), and a refused file no longer makes every warm run rewrite the whole cache (#553). Each entry in the changelog gives its own measurement and its floors. Caches are rebuilt once on first run and quality snapshots of the older scheme are not used. Thanks to @pt-act (a committed filename can no longer be read as commit metadata in `git log` framing: #433, #469), @llvm-x86 (a runnable `run=` hint for a TS/JS test in a subdirectory package, and Cargo and package.json entry points on `--for`: #335, #442, #459, #460, #511), @lennix1337 (langfacts slice 6c, Windows warm runs that leave the cache directory's ACL alone, shell test scripts read as tests: #471, #468, #496), @jflowers (an aliased Python `import a.b as c`, C / C++ include roots and the foreign-qualifier veto: #530, #524, #526, #478), @s0undt3ch (the installer hardening list: #293, #482) and @rainhuang0220 (#330); and for reports that led to fixes, @muayyad-alsadi (reST and Sphinx: #409), @pguerrerolinares (`--for` on a file name: #466) and @gucliti (the router skill's language list: #521). + **ripwire 0.6.9 — hedged or declined answers instead of confident wrong ones, `--path` that prints the bodies it names, `--defs` and owner-named rows.** A call through an interface-, trait- or abstract-typed receiver names the implementors that can run, with a floor stated when the list cannot be complete; a by-name hedge needs an import, and hedged hops are never counted as proven; a Rust `&dyn Trait` call, a JS/TS name written after its declaration and a Kotlin scope function no longer bind a same-named method; `--dead-code` stops over-reporting (`static` outside C-family code, Django and unittest hooks) and lists library functions only tests call; `--impact` counts who reaches a hedged caller. A short `--path` prints the body of its middle hop, an unreached one prints the bodies of the route it names, and `--path` lists every reaching definition pair when a name has several. `--defs=NAME` answers which definitions a name binds, with the spelling that picks each one alone; member rows print their owner (`Owner.name`, `Owner::name` in C, C++ and Rust, `Owner#name` in Ruby) on `--callers`, `--callees`, `--impact`, `--path` and signature rows, and `Class.method`-style spellings resolve; `--situ` and `--test-gate` start from the changed symbols, not the changed files; `--affected` adds tests that construct a reached class. By language: Rust receivers are typed by their written type; Python renamed imports, annotated fields and queue or message hand-offs resolve; functions stored in struct fields are joined to the calls through them in Go, Rust and C / C++. A `--for` question about an error carries the callers chain of the narrow function it ranks, and `--for` also follows a thin wrapper into the function it calls, names what a constructor calls, the functions a table initializer binds and the members of a shown owner, lists the callers of a function it is about, and answers a task spelled as a graph question with that verb. Each entry in the changelog gives its own measurement and its floors. Caches are rebuilt once on first run and quality snapshots of the older scheme are not used. Thanks to @lennix1337 (Windows `--quality-delta=A..B` and clang-cl PGO builds, `--uses` on a JS/TS literal receiver: #402, #410, #411, #422), @llvm-x86 (`--situ` and `--test-gate` from the changed symbols, Django and unittest hooks, Kotlin scope functions: #423, #437, #438), @mpapis (one order for every multi-definition name answer, `--lsp` says when a name has several definitions: #432), @pt-act (the pattern screen refuses overlapping alternation: #397) and @liziye627-design (a clone-gate arm fix: #431); and for reports that led to fixes, @hnipps (#228) and @s0undt3ch (#305). **ripwire 0.6.8 — more complete first answers and fewer false edges.** `--for` stops when its answer is complete, names one owner body for a "how does X work" question, appends the whole bodies of its top ranked rows and puts eight different files in its first eight rows; `--callers` and `--callees` serve up to 50 rows whole (a callers list whole while its rows fit 16,000 B); `--impact` of a type starts from the symbols that name it; and the calls the resolver declined are named as hedged rows. Fewer edges are false: Go methods carry their receiver type, a local bound to a call takes the callee's declared result type (Go, Rust, Java, Kotlin, TypeScript), Rust standard-type members no longer bind an in-tree namesake, and a method a constructed-name lookup can reach is `risk="hedged"` in `--safe-delete`, not `none-found`. On our fixed set of 173 questions, graded two ways (a stricter method and a more lenient one), the questions answered completely in one call went from 50 with 0.6.5 to 83 with 0.6.7 to 98 with 0.6.8 under the stricter method, and from 78 to 109 to 127 under the more lenient one (same machine, mechanical grading; 15 answers better and none worse since 0.6.7; on the 60 development questions the first answers are 19.9% larger). The 0.6.8 figure was measured on the train-28 build of 0.6.8; the later changes in this release were measured only on their own target questions. Clojure is a supported language (thanks @tag-und-nacht, #349). `--quality-delta=A..B` holds one commit's index at a time and answers what it can under a memory limit, and the guard counts a Linux cgroup's real charge (thanks @tonibergholm-codento, #387, #391, #392). Ruby mixins and Rails capture moved onto the shared path (thanks @mpapis, #396, #400, #401, #412), Python imports and Elixir `unquote` calls too (thanks @LaimaWu, #382, #380). A markdown section link `[text](other.md#slug)` edges to that heading, and a link no call shares is marked `via="link"` (thanks @muayyad-alsadi, #409, #414). Caches are rebuilt once on first run, and Go quality baselines, acks and notes pinned to a method need re-pinning once (see the changelog). @@ -3209,7 +3211,7 @@ record, and this line is only the pointer to it. | Vendored dependencies and their licences | [`THIRD_PARTY.md`](THIRD_PARTY.md) | | Installation and removal procedures | [`INSTALL.md`](INSTALL.md) | | The compile-time cap inventory | [`docs/LIMITS.md`](docs/LIMITS.md) | -| The measured cost of each cap | [`docs/TUNING.md`](docs/TUNING.md) | +| The measured cost of the caps the sweep covers | [`docs/TUNING.md`](docs/TUNING.md) | | Skill-file security checks | [`docs/COMMANDS.md`](docs/COMMANDS.md#--scan-skillsdir) | | The whole tool in 36 slides — the showcase deck | [`present/ripwire-showcase.pdf`](present/ripwire-showcase.pdf) ([pptx](present/ripwire-showcase.pptx), rebuilt by [`present/deck5_ripwire_build.js`](present/deck5_ripwire_build.js)) | diff --git a/changelog.d/335-ts-js-run-package-dir.md b/changelog.d/335-ts-js-run-package-dir.md deleted file mode 100644 index 8a84d8ea8..000000000 --- a/changelog.d/335-ts-js-run-package-dir.md +++ /dev/null @@ -1,19 +0,0 @@ -### Fixed — TS/JS `run=` runs from the package directory (#335, #442, thanks @llvm-x86) - -A TS/JS test in a subdirectory package got a `run=` that could not run. The command was root-relative. Run from the root, -the runner found no package config. Run from the package, the path was root-relative. Now the command is a subshell, -`(cd && )`, and the path after the `cd` is package-relative. The subshell keeps the `cd` from outliving the command, so a shell that keeps its directory between calls (an agent's) starts the next row at the root again. A multi-root run has no `root=` and keeps the plain command. Paste it from the shell at the root. - -A bracketed path, such as a Next.js route (`app/[slug]/page.test.ts`), had `--` before it. vitest reads `--` as the end -of its options and drops the file filter, so the row ran the whole suite. vitest and jest now get one quoted path. - -Two paths get no `run=` now. A path that starts with `-` gets no `run=`, because the runner could read it as an option. To run such a file by hand, use `node ./-suite.test.mjs`: on Node 26.9 `node --test` drops the `./` and exits 1 with `bad option`. Node's test runner globs its -file arguments, so a bracketed path under `node --test` matches no file and the runner exits 1. Both rows print `run_unknown="1"`. - -jest reads its argument as a regular expression, so a path under a package below `root=` has every regex byte escaped with a backslash, a dot included (`a.test.js` would also match `aXtest.js`). - -A package at `root=` keeps its command. A path there that is not shell-safe loses the `--` and has its regex bytes escaped. For `node --test`, a glob byte in the path gives `run_unknown="1"`. - -Limit: a jest package at `root=` keeps a plain dot as a regex dot when the path is shell-safe, so its filter may also match a sibling such as `route/test.ts`. vitest and jest match the path as a substring, so `src/lib.test.ts` also runs `deep/src/lib.test.ts`: a run can include extra files, never fewer. - -A vitest or jest command now needs a file the runner's default include collects: a name ending in `.test.` or `.spec.`, or, for jest only, a file under a `__tests__/` directory (jest matches that glob against the whole path, so one above the package counts too; thanks CodeRabbit). `x.test.helper.ts` (both runners) and a vitest `__tests__/x.ts` got a command that exits 1 with "No test files found". They are now `run_unknown="1"`. The rule follows `defaultVitestFileName` from #330 (thanks @rainhuang0220, #330). Limits: a `vitest.config.*` or `jest.config.*` `include`/`testMatch` is not read, so a config that narrows the default still gets a command; and jest 29's default lacks the `.mjs`/`.cjs`/`.mts`/`.cts` extensions that jest 30 collects, so a jest 29 package gets a command for such a file that exits 1. diff --git a/changelog.d/409-rest-sphinx-units.md b/changelog.d/409-rest-sphinx-units.md deleted file mode 100644 index 2e2068867..000000000 --- a/changelog.d/409-rest-sphinx-units.md +++ /dev/null @@ -1,13 +0,0 @@ -### Added — reST and Sphinx documents become real units (#409, thanks @muayyad-alsadi) - -`.rst` no longer goes through the markdown grammar. A hand-written parser reads titles in any punctuation, with the level -taken from first-seen order, and makes `.. option::`, `.. setting::`, `.. function::`, `.. class::`, `.. method::`, -`.. confval::` and their siblings into units named by their target. `.. _label:` is a unit too, and `:ref:` and `:doc:` are -link edges. A `.txt` is indexed when the nearest Sphinx `conf.py` declares it in `source_suffix`, which brings in django's -679 documents; a `.txt` anywhere else stays unindexed and stays in `unindexed=`. Past 20,000 units or 64 levels of nesting -a file keeps the units it has and reports `why="extract-partial"` in `--skipped`. - -On a code question, the `--for` tail lists code files before doc files. A question that asks for doc text keeps the old -interleaved order. When the index holds a doc file, the legend says which order the tail used. A reST directive or markdown -heading that shares a function's name is still listed as a second definition by `--callers`, `--impact` and `--safe-delete`. -`--path` starts and ends only at the code. diff --git a/changelog.d/433-git-log-framing.md b/changelog.d/433-git-log-framing.md deleted file mode 100644 index 6c6069f04..000000000 --- a/changelog.d/433-git-log-framing.md +++ /dev/null @@ -1,39 +0,0 @@ -### Fixed — a committed filename can no longer be read as commit metadata (`git log` framing) (#433, thanks @pt-act) - -Every `git log --name-only` consumer in the history miners framed its stream with a line-based reader and a -`__C__` marker matched as a line prefix, so a repository containing a file whose name begins with the marker -bytes had that file read as commit metadata. Measured on a hostile fixture (files named `__C__`, -`__C__x|1694000000.cpp`, `__C__ 1694000000.cpp`, `ab.cpp`, `q"uote.cpp`, `sp ace.cpp`, `back\slash.cpp`, -`pi|pe.cpp`, `--dashdash.cpp`): `--owners` analysed 8 of 11 files, five of them owned by an author email that -appears in no commit, one file's top owner flipped to a different real author, and the newline-named file lost -its churn entirely. - -One reader now parses the stream for all five consumers (`gitLogFileSets`, `gitLogNameOnlyRaw`, -`gitFileCommitCountsInDayWindow`, `gitLogDecayedFileMining`, `gitFileAuthors`): -`git log -z --name-only --format=%x00__C__%x00%ae%x00%at%x00%ct`, with the marker a whole-field sentinel a path -can never spell and one field of lookahead so a merge's prefix empty is not mistaken for a record boundary. -Any POSIX-legal filename — newlines, quotes, backslashes, pipes, leading dashes, the marker itself — -round-trips byte-exact, and ownership answers with the author, never the committer. On trees with ordinary -names the answers are byte-identical to before, pinned by a golden recorded from the pre-change binary over -`--owners`, `--hotspots` and `--cochange`. - -The history-walk cache's `kQChurnCacheScheme` moves 3 → 4: a blob written by a pre-lane binary holds the old -reader's stream, which for the repositories this fix targets is the corrupted one, and no key component can -tell that blob from a fresh read. Every repository's history is walked once more after the upgrade; on trees -with ordinary names the walk produces the same stream, so the cost is one cold walk and no changed answer. - -**The walkers' stop contract rides the field read.** The line readers this replaces checked the abandon flag -before every line; the first cut of the field reader checked it only between fields, so a stream whose fields -never arrive — a git answering in its default format, or a shim streaming bare lines — held the read inside one -unterminated field until end-of-stream instead of stopping at the next unit. The flag is now polled once per -byte, and the memory-guard fixture that measures it refuses in about a second where the narrowed read blocked -for the whole stream. - -**One case is disclosed, not fixed:** a merge whose FIRST combined path is a file literally named `__C__` -still reads as pathless — its prefix empty is followed by the marker-spelled path, which the boundary rule -cannot tell from a real record boundary, so that merge's files lose their attribution. Its next three paths -are read as a phantom record's email and epochs (which never parse, so ownership skips it); a fifth and later -combined path rides that phantom at epoch 0, which the co-change sets count as one more commit and -`--rank-by=churn-decay` weights as if it were HEAD's. The full fix is a format sentinel change, owned by a -follow-on spec. `test/gitframingcheck.sh` pins both the fix and this collision's exact observable, including -where the two churn miners disagree by design. diff --git a/changelog.d/436-win32-cache-dir-acl-skip.md b/changelog.d/436-win32-cache-dir-acl-skip.md deleted file mode 100644 index 158d777ea..000000000 --- a/changelog.d/436-win32-cache-dir-acl-skip.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — Windows: warm runs no longer rewrite the cache directory's ACL - -- The per-user cache directory ladder skips `chmod(0700)` when `lstat` already reports an owner-only directory. On Win32 that `chmod` re-applies a protected DACL with inheritable ACEs, which Windows propagates through every file under the cache: with 38,000 files in the cache a warm run took ~2,024 ms instead of ~10.6 ms (p50 of 3, same revision, only this guard changed). A permissive directory is still repaired. Regression: `test/cache_ladder_windows.py` and the Windows arm of `test/cacheisolationcheck.sh` (#436, thanks @lennix1337). diff --git a/changelog.d/447-langfacts-receiver-comment-columns.md b/changelog.d/447-langfacts-receiver-comment-columns.md deleted file mode 100644 index d4d5e3f64..000000000 --- a/changelog.d/447-langfacts-receiver-comment-columns.md +++ /dev/null @@ -1,11 +0,0 @@ -### Changed — Slice 6c: language receiver and comment rules moved to LangFacts columns (#447, thanks @lennix1337) - -Move the language predicates `implicitReceiverLang`, `memberNeverReachesFreeFunction`, -`moduleCallReachesFunctionsOnly`, `classNameReceiverLang`, and `langUsesHashComment` -into `constexpr` columns of `struct LangFacts` (`implicitReceiver`, `memberNeverReachesFreeFunction`, -`moduleCallReachesFunctionsOnly`, `classNameReceiver`, and `hashComment`) in `src/langfacts.h`. -Call sites in `src/receiverevidence.h`, `src/graph.h`, and `src/quality.h` now read `langFacts( l )` -directly, eliminating Type-2 clone group 99 (`langUsesHashComment` / `classNameReceiverLang`) from -`--clones` output and lowering the shared-file `== Lang::` ratchet ceiling in `test/langidentity/ratchet.tsv` -from 333 to 326. `isJsTsLitRecv` remains in `src/model.h` as it partitions `RecvKind` rather than `Lang`. -Output across the repository is verified byte-identical with 0 quality-delta regressions. diff --git a/changelog.d/448-unused-in-file-fix1.md b/changelog.d/448-unused-in-file-fix1.md deleted file mode 100644 index 84e88180e..000000000 --- a/changelog.d/448-unused-in-file-fix1.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — unused-in-file fix round 1: `unused` is printed only when the scan can prove it - -The `--for` "unused in file" table reads every text file under the root, indexed or not (`.vue`, `.svelte`, `.ipp`, Procfile, templates, `vendor/`, `dist/`), and reads `unknown why=unseen` when a file cannot be read. `#` opens a comment only where the language facts say so (Python, shell, Ruby), never in Rust attributes, C/C++ directives or JS/TS private fields. One-character names are scanned. A distribution's public API (a Python package with a build table, a published package.json, `export type *`) reads `why=api`, and the legend no longer says `never live`. File-path routes, `__init_subclass__` and metaclass registries, joined string names, dynamic directory imports and a stem aimed at the target module read `unknown`; the short-stem length rule is gone. - -Fix round 2 keeps to the same rule, `unused` is printed only on a proven shape. A C or C++ call no longer reads as a prototype. Non-ASCII names are one name. `.mdx` and `.txt` are read; prose docs are the one kind that is not, and the legend says so. A directory that cannot be entered counts as unseen, and `unseen_by=` names the limit that fired. Binaries count toward no cap. `* ` and `// ` line heads are comments only where the language has them. A module object indexed or passed along, a default export with an importer, a stem joined to anything computed, a multi-line decorator or attribute, a runtime-called name, and a module nothing imports (`why=unimported`, Python, TS, JS) all read `unknown`. diff --git a/changelog.d/450-cargo-bin-ep-manifest.md b/changelog.d/450-cargo-bin-ep-manifest.md deleted file mode 100644 index fa83ab7b6..000000000 --- a/changelog.d/450-cargo-bin-ep-manifest.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--for` shows a Cargo `[[bin]]` entry and the `main` it names (#450) - -- `--for` manifest entry points now read Cargo.toml `[[bin]]` tables (group `bin`), resolving each bin path to its `main` function; an omitted `path` follows Cargo's default (`src/main.rs` for the package's own name, `src/bin/.rs` otherwise), and a crate with no `[[bin]]` (and no `autobins = false`) lists its own bin at `src/main.rs` when the task names the package or asks about running a binary (run, bin, binary, cli, command, executable, launch, main, entry). A `path` with `../` or Windows `\` separators names the file it points at. Not read: the binaries Cargo auto-discovers (`src/bin/*.rs`, and `src/main.rs` beside declared `[[bin]]` tables) and `src/bin//main.rs` as a default path. Gate coverage in `test/epmanifestcheck.sh` (issue #450). Thanks to @llvm-x86 for the reader, the fixtures and the Cargo inference cases. diff --git a/changelog.d/451-package-json-ep-manifest.md b/changelog.d/451-package-json-ep-manifest.md deleted file mode 100644 index 58bcfe276..000000000 --- a/changelog.d/451-package-json-ep-manifest.md +++ /dev/null @@ -1,4 +0,0 @@ -### Added — `--for` shows a package.json `bin` / `main` entry and the file it names (#451) - -- `--for` manifest entry points now read the top-level `bin` and `main` fields of `package.json`, and resolve each to the file it names (its first exported function, else the file's module body); a bin is named as npm installs it (`@acme/tool` installs `tool`), and a string `bin` in a package with no `name` is no entry, as npm drops it; a `main` that names no exact file is looked up as Node loads it (`.js`, `.ts`, `.mjs`, `.cjs`, then `index.js` / `index.ts` in that directory; `"lib/"` with a trailing slash is the directory), while a `bin` names its file exactly; package.json is read for a `.js` or `.ts` row; a `package.json` inside `node_modules` is a dependency's and is not read. Gate coverage in `test/epmanifestcheck.sh` (issue #451). Thanks to @llvm-x86 for the reader and the fixtures. -- The Python, Cargo and package.json readers share one format table; Cargo and package.json targets go through one file-path resolver, and the pyproject and Cargo readers share one set of TOML helpers (headers, keys, string values, multi-line strings, so a pyproject `description = """` that spells a table is a value); a Cargo or package.json group is a manifest key, so no code is scanned for it as a loader literal, and the legend says so. diff --git a/changelog.d/466-test-hyphen-names.md b/changelog.d/466-test-hyphen-names.md deleted file mode 100644 index 564bf95e1..000000000 --- a/changelog.d/466-test-hyphen-names.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed — a shell test script named `test-*.sh` or `*-test.sh` reads as a test (#466, #472, thanks @lennix1337) - -`isTestPath`, the test-file question every verb shares, did not read `test-document-remind.sh` as a test, so `--for` ranked the test sibling beside the script it tests and `--ignore-tests` kept it. @lennix1337 found it in #466 and sent the fix in #472; the `src/filter.h` change lands under their authorship, and the narrowing below is ours. - -A false match here hides real source from every verb, and the bare hyphen forms (`test-*`, `*-test.*`, `*-spec.*`) also matched shipped files such as `pod-spec.ts`, `ab-test.ts`, `self-test.c`, `test-and-set.c` and zone.js's `async-test.ts`. So the hyphen forms are an allowlist: `test-*` and `*-test.*` apply to shell and batch scripts (`.sh .bash .zsh .ps1 .bat`), and `.e2e-spec.` and `.perf-spec.` apply in any extension. Of the 102 corpus code files the bare forms newly read as tests, the allowlist keeps 73, including every `.e2e-spec.` and `.perf-spec.` file and the shell scripts; the other 29 keep their previous reading as source. That includes real test files such as a bare `foo-spec.ts`, which stay visible: a missed test only stays on screen, a false test hides source. Underscore forms (`ab_test.ts`) already read as tests before this change and are untouched. Gate: `test/testpathhyphencheck.sh`. - -The mention-anchoring half of #472 is not part of this change: FOR-FILE-ANCHOR already anchors the bare filename and stem cases. diff --git a/changelog.d/478-py-alias-import-target.md b/changelog.d/478-py-alias-import-target.md deleted file mode 100644 index b59e3b9cc..000000000 --- a/changelog.d/478-py-alias-import-target.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — an aliased Python `import a.b as c` is a file edge again (thanks @jflowers, #478) - -`import pkg.mod as pm` recorded the Include target `pkg.mod as pm`, the whole aliased clause, and no resolver finds a module spelled that way. So every aliased plain import added nothing to the file graph: `--deps` listed the target with no edge behind it, `--arch` Ca/Ce, cycles and `--impact` never saw the dependency, and a bare call that only the import could disambiguate was declined (`declined_calls=`) instead of bound. The target is now the module itself (`pkg.mod`), the same one `import pkg.mod` records; the alias still binds `pm` as before. `from a.b import x as y` was never affected. A multi-clause `import a as b, c` still records only its first clause as an Include, as every multi-clause import does. diff --git a/changelog.d/478-py-import-layout.md b/changelog.d/478-py-import-layout.md deleted file mode 100644 index e1c2b3e01..000000000 --- a/changelog.d/478-py-import-layout.md +++ /dev/null @@ -1,9 +0,0 @@ -### Fixed — Python file edges: stdlib-named siblings, src layouts, and directories the crawl denylist removes (#478) - -Reported by @jflowers, who built an `ast` ground truth of pip's whole import graph and named the causes. - -Inside a package, an absolute `import logging` no longer binds a sibling `logging.py`, in the file graph or the call graph; Python 3 has no implicit relative import. (A stray `__init__.py` at the repo root makes the root itself a package and does not trigger this: its `a.py -> logging.py` edge stays.) A plain script directory keeps its sibling, because it is `sys.path[0]` when the script runs. An absolute import now also resolves under a `src` directory beside the importer or any ancestor, and under the source root of the importer's own package chain (`lib/mypkg/a.py` importing `mypkg.b`). Those layout roots are a fallback: the repo root and a plain script directory are probed first and win over a namesake under `src`, and two layout roots that name one module degrade to no edge (the base resolver had no layout roots, so this is the new rule). A `src` that holds an `__init__.py`, or sits inside a package, is a package and not a source root. A stdlib-named first segment (`logging`, `json`, from the CPython 3.13 `sys.stdlib_module_names`) does not bind through a layout root, because a repo file there shadows the stdlib only when that directory comes first on the path; a root that holds the stdlib itself (CPython's `Lib/`) is exempt. A package that imports ITSELF is decided at its own chain root first, with no stdlib guard and no repo-root namesake in the way: `src/email/sender.py` with `from email.templates import t` binds `src/email/templates.py`, and `src/foo/b.py` beside a root `foo/` binds `src/foo`. Any other importer of that name (`src/api/routes.py`, `tests/`) stays unresolved, because the run mode decides whether the stdlib or `src/` comes first on the path and the resolver cannot know it. `__main__` is guarded like a stdlib name. A file outside those shapes importing a package under a root that is not `src` stays unresolved. In a multi-root run, the call graph refuses every hit in a root other than the importer's own (the file graph always did), so a root that happens to be labelled `src` cannot bind another root's importer. The one base-true edge this also removes: with `mypkg` and `tests` as two roots, `import mypkg; mypkg.ff()` from `tests/` goes from 1 caller to 0, matching the file graph. Over `ast` truth, exact directories Ca/Ce: textual 86/3 to 98/98, flake8 62/25 to 100/100, pydantic 97/97 to 100/100 with 52 false edges gone. - -Limits. A script inside a package (`pkg/__init__.py` beside `pkg/tool.py`, run as `python pkg/tool.py`, with `import helper`) puts its own directory first on `sys.path`, but the import resolver cannot tell a script from a module, so it loses that true sibling edge with no hedge in the answer: in one fixture `--callers=run` goes from 2 callers to 0, `--impact=run` from 3 reached files to 0, and the importer count from 2 to 0. The commonest form is a `src/__init__.py` with `python src/train.py` doing `from utils import x` (`--impact` importers 1 to 0; `--callers` hedged). The corpus counted three of these over about 6,000 package files, but the corpus is library-heavy, so application repos with training or CLI scripts inside a package will lose more. A namespace package (no `__init__.py`) cannot be told from a script directory and keeps its sibling edge. The older suffix binding is unchanged: a plain `src/logging.py` or a unique `tools/logging.py` still binds `import logging; logging.info()` as a firm call edge from `pkg/`, `tools/` and `tests/` callers (3 callers in one fixture) while `--arch` and `--deps` show no edge. A non-self importer of a stdlib-named package under `src/` (`src/api/routes.py` doing `from email.sender import send`) is dropped from the file graph, and the answer does not say so. - -`--skipped` now rows the directories counted in `pruned_dirs=` (up to its 500-row ceiling per class; `rows_capped="1"` says a cut and the count stays exact): `why="pruned-package"` when a build-output name (`build`, `dist`, `out`, `target`, `captures`) holds its own `__init__.py`, else `why="pruned-dir"`. `--arch` and `--deps` carry `` naming the real packages, present only when one was pruned, up to 8 with `count=` and `next="--skipped"` for the rest. diff --git a/changelog.d/520-for-page-attrs-shed.md b/changelog.d/520-for-page-attrs-shed.md deleted file mode 100644 index 3f5154281..000000000 --- a/changelog.d/520-for-page-attrs-shed.md +++ /dev/null @@ -1,3 +0,0 @@ -### Changed — an over-budget `--for` page sheds optional row attributes instead of missing the budget (#520) - -When a `--for --token-budget=N --limit=…` page still exceeds `N` at its one-row floor, the row drops `cx=`, `ccx=`, `layer=` and its own `next=`, then `sc=` if needed, and the root says `attrs_shed="1"` (defined in the page's legend). The row and what names the answer (`n=`, `p=`, `l=`, `r=`, the signature) always ship; a page that already fits is byte-identical. diff --git a/changelog.d/523-skill-lang-drift.md b/changelog.d/523-skill-lang-drift.md deleted file mode 100644 index 15b38d80e..000000000 --- a/changelog.d/523-skill-lang-drift.md +++ /dev/null @@ -1,25 +0,0 @@ -### Fixed — the router skill no longer tells agents that PHP, Kotlin or Elixir are unsupported, and `--help` lists Clojure (#523, thanks @gucliti) - -Fixes #521, reported by @gucliti. `skills/ripwire-router/SKILL.md` said ripwire parses 15 languages "— nothing else" and -named PHP and Kotlin as examples of languages that get a thin or empty map. `ripwire --help` lists 27, so an agent that -read the skill on a PHP, Kotlin, Lua, Elixir, Dart, Clojure, GDScript, TOML, YAML or Markdown repo was sent to plain `rg` -exactly where ripwire works. @gucliti measured it on an Elixir/OTP project: `--skipped` showed all 126 `.ex` files -indexed (3185 symbols), `--for` returned ranked symbols with correct call edges, and `--recall` ranked the repo's ADR docs -by section. - -The "Wrong tool for the job" paragraph now names `--help`'s Languages line as the source of truth, lists the families -compactly, and uses languages that really are unindexed as its examples (Haskell, OCaml, Scala, Zig); the map's `unindexed=` -header names the skipped extensions. `docs/LOCALS_INDEXING.md` lost a stale "all 14 languages" count. `--help` itself had -drifted: Clojure (`.clj/.cljs/.cljc/.bb`) has been indexed since September but was missing from its Languages line. It is -listed now. - -New gate `test/skilllangdriftcheck.sh` checks both directions. It fails if `--help`'s Languages line and the crawler's -extension table (`src/ingest_crawl.h`) disagree on a language. It also fails if a skill, doc or hook calls a listed -language unsupported ("another language (PHP, ...)", "PHP is not supported", "no PHP support", "ripwire does not yet -parse PHP", "won't index", "Unsupported: ...") or restates a closed list ("nothing else", "and nothing more", "supports -only ...") that omits one. A closed list is judged on its own run of names, not on the paragraph around it. It is red on -the old router paragraph and on eleven reworded claims, and green on the new paragraph. A fake doc that calls Haskell, -OCaml, Scala and Zig unsupported passes, and so do ordinary sentences such as "files in an unsupported language are -counted in unindexed=". A per-feature sentence that only looks like a claim ("Per-language rules cover C++, Python, Go, -Rust; no other language has the gate") is exempted with `` on its line. Limit: it reads -these prose patterns, not every possible wording; "X and Y only." and "for a PHP repo, use rg" still pass. diff --git a/changelog.d/542-ladder-hoist.md b/changelog.d/542-ladder-hoist.md deleted file mode 100644 index 32bd4a12c..000000000 --- a/changelog.d/542-ladder-hoist.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — warm `--deps` and `--for` on large trees use 1.5-3x less CPU (#542) - -The by-name call resolver asked, for every candidate definition of a called name, facts that depend only on the call: the receiver's root name, whether it is `this`/`super`, the caller's class, and for JS/TS the `require()` alias and the path it joins to. They are now read once per call and tested per candidate, and the "is this owner a class" table is one byte per symbol instead of a lookup. Warm runs (cache hit) measured with user CPU (instructions retired in brackets): vscode `--deps` -67 % (-70 %), cockroach `--for` -57 % (-60 %), envoy `--deps` -35 % (-36 %). Cold runs (no cache) gain less: instructions -14 % on vscode, -12 % on cockroach and -4 % on envoy. Peak footprint is unchanged in each of those runs. The new storage is one byte per symbol plus one small per-call struct. The measurement is one machine and three repos; wall time was noisy on a shared machine, so the figures above are CPU and instruction counts, not wall time. - -Answers are unchanged where it was checked: 321 in-sample `--for` calls and 12 base-versus-new output pairs on those three repos (9 warm, 3 cold) are byte-identical, as are 92 caller and callee outputs on a crafted Python, JS, TS, Go and Java tree. A module alias is proved by its own call's `require()` path, never by a neighbouring call's (a new case in `test/tsimportprecisecheck.sh`). diff --git a/changelog.d/560-for-bare-subject-registration.md b/changelog.d/560-for-bare-subject-registration.md deleted file mode 100644 index d64b09371..000000000 --- a/changelog.d/560-for-bare-subject-registration.md +++ /dev/null @@ -1,9 +0,0 @@ -### Changed — a `--for` task with no table or registration word still reaches a registration row - -The table block that ends a `--for` answer (``) used to need a word such as register, table, default or handler in the task. A bare subject (`new_boxed_executor for HashJoinNode`, `light.turn_on hue async_turn_on`) has none, so the block never ran. Now such a task runs the same walk over its backticked spans and identifier-shaped words, but lists only strong rows: an argument of a registration call (`reg=`), a top-level macro or call row (`tbl="item"`), or a row of a macro table (`for_all_params! {`, `$macro! {`). A registration counts only when the site is a direct argument of the call (`register(handler)`; not `register(wrap(handler))`, `register(table[handler])` or a site inside an object literal). A dict, list, `__all__`, `#define` or const-array row, and a site that is no row, is weak: counted in `weak=N`, never listed; with no strong row the answer is unchanged. A task with no identifier-shaped word and no backticked span scans nothing, and neither does a no-cue anchor that is one plain lower-case word (`register`, `open`, `index`: no `_`, `::`, `.`, digit or capital, backticked or not), and a walk that lists no strong row adds no `` note either. The legend clause that says this rides only on such an answer. - -The callee names that count as a registration are narrower: `use` is no adding verb (it is the prefix of hooks such as `useRouter`), the noun after an adding verb must be the name's last word (`addEventHandler` is one, `addCommandLineOption` is not), and `reg` is a verb only as the last word or right before route, command or handler (`async_reg`, `reg_handler`; not `reg_value` or `reg_field`, a hardware register). The table legend is shorter: it no longer lists the vocabulary or the ranking of generated files, says the registration call is looked for 4 lines and 512 bytes above the site, and the `tail=` sentence rides only beside a `tail=`. - -Gate `forregrowcheck` (N23, N24 for the last two rules) covers bare subjects on Rust, Python, Go and TypeScript fixtures, a weak row beside a strong one, weak-only rows of four kinds (each answer byte-identical with the block switched off), the no-call shapes, and the vocabulary cases above with their twins. - -What it does NOT do: it does not list a weak row for a task with no cue word, even when that row is the answer; ask with a table or registration word for those. A callee's name is still no proof the call registers. diff --git a/changelog.d/560-for-flag-default.md b/changelog.d/560-for-flag-default.md deleted file mode 100644 index 0c527e4e4..000000000 --- a/changelog.d/560-for-flag-default.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — a `--for` task that names a `--flag` reaches the call that defines it and the default it takes - -A task naming a flag (`kubelet --max-pods`, `How does the kubelet use the --max-pods flag?`; a `--` token whose name holds a hyphen, or a `--` name of 5 bytes or more right before the word flag or option) now gets a `` when no earlier literal of the task has a source site. Its rows are the sites where the name is a whole quoted string (`"max-pods"`, `"--max-pods"`; its definition is one of them, an invocation or a lookup another); a name with no inner hyphen (`--compress`) is a site only in its dashed form (`'--compress'`) or as an argument of a flag-definition call (`fs.Bool("verbose", ...)` or a TCLAP `ValueArg name(...)`), never a dict value, an `__all__` entry or an error message. Any other source hit (usage text, a longer flag such as `max-pods-per-core`) is counted in `mention=`. On the first such row that has one, `def=` is the default of a flag-definition call, read from the text: a `default=`, `default_value=` or boost `default_value(10)` argument, else the default slot of a Go flag, pflag or cobra call (the argument after the name, two after it for the `P` form such as `StringVarP`), never a usage sentence. A call that is no such flag call (an `exec.Command(...)` invocation, a nushell `.named("end-date", SyntaxShape::String, ...)`, an argparse alias `"-v"`) has no `def=`. When `def=` names a field (`c.MaxPods`), `` rows list the constant writes of that field (`obj.MaxPods = 110`, `MaxPods: 110`, never a copy `out.MaxPods = in.MaxPods`) in code that names the type of the receiver in `def=` (its parameter type, a package variable's declared type or its own name; a name match, no type check; a receiver with no type found lists none), functions and files named for defaults first, at most 2, with `writes=` counting all of them. Measured on kubernetes-05 (stored and both natural wordings): the `max-pods` registration row prints with `def="c.MaxPods"`, and the first `` row is the 110 default in `SetDefaults_KubeletConfiguration`. Gate `forliteralcheck` F1-F28 covers Go pflag (plain and shorthand), Python argparse and click, a flag with no default, a default that is a call, a C++ TCLAP argument, an alias after the name, a nushell `.named`, an invocation ranked before the registration, a no-hyphen name met as a dict value, a same-named field of another struct, and near misses (docs only, no site, a Rust clap field, no flag word, no flag). - -What it does NOT do: it does not read a flag that has no quoted name (a Rust clap field, gflags `DEFINE_int32(name, ...)`), it does not follow a default through a variable or a call, and `def=` is the text of the argument, not the value in effect. diff --git a/changelog.d/560-for-match-arm-excerpt.md b/changelog.d/560-for-match-arm-excerpt.md deleted file mode 100644 index 3c6b4b9bc..000000000 --- a/changelog.d/560-for-match-arm-excerpt.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — a `--for` excerpt shows the match arm or case that holds the literal; `body="above"` only when the body was printed - -An `` excerpt in the `` block shows one window around each site, from the line before it. When that window leaves out the head of the match arm or case holding the site, the window now starts at that head, at most 12 lines up, and the excerpt says `arm="1"` (a legend clause rides only with it). Arms are an allowlist: a Rust, C# or PHP `Pattern => {` with balanced parens (never a JavaScript `x => {` callback), a `case` or `default` label (C, C++, Java, JavaScript, TypeScript, Go, C#, Swift, Python `match`; a `default: {` key of a JavaScript object literal, which heads no `switch`, is not one), a Python `elif` or `else:`, a Ruby `when`, `elsif` or `else`, and an `else if` or `else` branch in C-family and Go code. Openers the window already shows (an `else {` right above the site) are passed over to the arm above them; an `if`, a loop or a function above the window ends the walk with the old window. Measured on risingwave-02 (all three wordings): the window of `handle_alter_streaming_rate_limit` now starts at the `(Table, Source) =>` arm and holds the field read that picks the throttled id. - -A `` row said `body="above"` when its owner's line, path and name appeared anywhere after the ranked rows, including a hop row (``) whose body the fold skipped. It now says so only when the answer printed the owner's whole ``; a hop row, a fold skip or a truncated `` that stops before the site is no body above, and the block serves its own excerpt. Gate `forliteralcheck` E1-E12 and B1-B3. diff --git a/changelog.d/alias-import-reexport-rows.md b/changelog.d/alias-import-reexport-rows.md deleted file mode 100644 index 6cec3cdd1..000000000 --- a/changelog.d/alias-import-reexport-rows.md +++ /dev/null @@ -1,14 +0,0 @@ -### Added — `--for` prints the re-export hops to a definition the task names - -A question like "where is `ShellError`, used in ls.rs, defined?" got the definition and nothing between it and the use site: -the `pub use` / `export … from` statements a reader walks through were never rows. A `--for` answer now ends with a -`` block when a task word names an in-tree definition that something re-exports: one `` row, then each -re-export statement nearest the definition first (``), and the task-named file's own import -(``). Rust: `pub use path::Item`, `pub use path::{A, B}`, a module re-export (`pub use other::module`) and a glob -(`pub use path::*`, marked `glob="1"`: hedged), with paths resolved through `crate` / `self` / `super`, child modules and -Cargo package names, so same-named items in two modules keep their module identity (`cands="N"` when two manifests spell -one crate name). TypeScript/JavaScript: `export { a } from`, `export { a as b } from` (`as=`), `export * from` -(`glob="1"`), recorded by the TS/TSX/JS tags queries through the shared binds channel as a new `LocalBindKind::ReExport` -(parser version bump). Rows only: nothing here is a call edge. Not followed: a Rust `pub use a as b` rename (counted -`as_skipped=`; help-wanted #445), `export * as ns from`, and items defined inside an inline `mod { }`. A `pub use` inside an inline `mod { }` prints its full module path; `src/main.rs` beside a lib.rs and `src/bin/**` are binary crates, never the library's modules; a path through a name the file imports reads through that import (never a same-named workspace crate), and only an import of the same module counts — a child `mod` inherits no parent `use`, and a `use` inside a function body binds nothing outside it; `pub(crate) use` is labelled `vis="restricted"`; a walk that stops at 256 spellings says `capped="1"` beside `total=`. Under a token budget (`--token-budget=N`) the rows are not printed (they sit outside `est_tokens`): a one-line `` names how many definitions carry a chain. `RIPWIRE_NO_REEXPORT_HOPS` -turns it off. Gate: `test/reexportcheck.sh`. diff --git a/changelog.d/anchor-ns.md b/changelog.d/anchor-ns.md deleted file mode 100644 index 4d3433f4d..000000000 --- a/changelog.d/anchor-ns.md +++ /dev/null @@ -1,4 +0,0 @@ -### Changed — `--for` anchor choice: a test-only anchor among several no longer commits the route; a path word picks one namesake - -- `--for` and the MCP `for` verb: a name-exact anchor that has two or more definitions, none of them source (test, doc, data, CI or vendored copies), no longer commits the route to whichever copy was found first. The words rank instead (`route="subtoken+body…; anchor has no source definition (…), not committed: ranked as words; hint anchors: …"`), the anchors stay in `route=` as a hint, and confidence reads low as before. A name with ONE such definition (a test helper that is the only thing by that name, however often the task repeats it) or with none (an env var served by the literal block) keeps its name-exact route. -- The query-mention anchor: an identifier the task names with several definitions (`syncProxyRules` in iptables/, ipvs/, nftables/, winkernel/) is lifted at the one whose file stem or parent directory another task word names (`kube-proxy nftables syncProxyRules`). A word that names several of them, two words that name two different ones, or a word that names only a C-family prototype while the definition sits elsewhere choose none, and the ranking is as before. A dot that ends a sentence (`… in nftables.`) does not make the word a qualifier. diff --git a/changelog.d/arch-honest.md b/changelog.d/arch-honest.md deleted file mode 100644 index 6e850023c..000000000 --- a/changelog.d/arch-honest.md +++ /dev/null @@ -1,16 +0,0 @@ -### Fixed — `--arch`, `--deps`, `--cochange`, `--impact` and `--export=cc.json` stop reporting coupling from edges the index never had - -Go, Java, Kotlin, C#, PHP, Swift, Dart, GDScript and Clojure draw almost no file-to-file edge in this build (Go only across a -`go.mod` replace), so `--arch` printed Ca = Ce = 0 and `zone="pain"` for every typed module of those languages, a design finding made -only of missing data. A module holding a file in such a language now reads `zone="unknown" no_file_edges=LANGS`: with no edge observed -it has no `ca`/`ce`/`I`/`D`/`reachable`, with one it keeps `ca`/`ce` as floors and has no `I`/`D`/`leaf`. `` counts it in -`zone_unknown=`. Every no-edge file, with or without an observed edge, is left out of both system denominators (`dep_files=`, `propagation_cost`), so one -real edge elsewhere cannot bring back a `shape="horizontal"` over files that could not have an edge, and `masked_files=N` says how many -were left out; with no edge left among the other files, `ccd`/`nccd`/`shape` and `propagation_cost` are absent. `--cochange-groups` puts -`no_file_edges=` and `unknown_pairs=N` on its root when pairs with such a side are in no group, so `groups="0"` no longer reads as a clean answer. `--deps` drops `afferent=`/`instab=`/`transitive=` -on those files, and `--report` no longer calls such a tree acyclic. Roots carry `no_file_edges= unmeasured_files= graph_partial="1"`, -and `--arch` says on stderr that `violations=` is a floor. The same missing data used to print elsewhere as a measured zero, and no -longer does: `--cochange` (and the MCP `cochange`, and `--pr-context`) put `no_file_edges=` where `surprising="1"` claimed hidden -coupling for a Go pair with a real import; `--impact` (and the MCP `impact`) carries `no_file_edges=` on the root, because its -`importers=` is then a floor; `--export=cc.json` omits a zero `fan_in`/`fan_out` for such a file instead of writing it, and a nonzero one is a floor (the attribute descriptors say so). Which languages is the -`fileEdges` column in `src/langfacts.h`. Trees with no such file are byte-identical. Thanks to @jflowers, whose #478 led to this finding. diff --git a/changelog.d/barrel-short-key.md b/changelog.d/barrel-short-key.md deleted file mode 100644 index 55f92627e..000000000 --- a/changelog.d/barrel-short-key.md +++ /dev/null @@ -1,6 +0,0 @@ -### Fixed — `--for` follows TS/JS namespace re-exports, checks every segment of a written qualifier, and gives an unresolved name a fallback ladder - -- `--for` ``: a TS/JS namespace hop is now a row. `import * as X from './m'` followed by a local `export { X }` (or `export { X as Y }`), and `export * as X from './m'`, publish the whole module under `X`; the row says `ns="X"`. Behaviour change: `export * as ns from` used to be no row at all (the old legend said so); it is now an `ns=` row, and `test/reexportcheck.sh` arm T2 was rewritten with it. -- `--for` `` on a qualified subject (`Q.leaf`, `Q::leaf`): every segment of `Q` must be accounted for. Full proofs: the file stem or a dot-suffix of it (`Completions` for `ts.Completions.ts`), the path's own tail (`utils.legacy` for `src/utils/legacy.ts`), a package index file's directories (`utils` for `src/utils/index.ts`), the whole owner path, or a namespace hop that publishes the definition under `Q`. Three weaker proofs only hedge: a directory elsewhere on the path (`src/utils/legacy/old.ts` for `utils`, `q_by="dir"`), a consumer's own `import * as Q from …` (`q_by="alias"`), and a path proof the tree contradicts (a file does `import * as Q from M` and no such `M` reaches the definition, so a `tests/utils.ts` stem proves nothing; `q_by="path"`); a hedged definition is listed only when nothing proves `Q` fully. The definition carries `q=` and its proving chain prints first. When one definition proves `Q` fully, a same-named one that cannot is no longer listed, so such a task may print fewer definitions than before. When none does and a cue word asks where the name comes from (`where is utils.formatDate defined`), every chain still prints, marked `q_unmatched="1"`. A subject that is one code-shaped name (the whole task) now prints its chain without a cue word; a bare `Q.leaf` prints only chains with some proof. A `this.X` / `self.X` subject is a member access and gets no block. A namespace re-export cycle (`a.ts: export * as B from './b'`, `b.ts: export * as A from './a'`) now ends without reaching the spelling cap. -- `--for`: a name-exact route that resolved no definition (`reason="no_candidates"`, empty ``) now prints a `` block with a fallback ladder, each rung named: `leaf` (a dotted key's last segment, filtered by the same qualifier proof; exactly one fully proven definition is `exact="1"` and its body is printed, several are hedged with `cands=`, hedged-only proofs say `q_by="alias"`, `q_by="path"` or `q_by="dir"` and are never exact, a method or nested function gets only its owner-path proof, never its file's, none is `q_unmatched="1"`; C++ stores only the innermost owner, so a namespace segment stays unproven), `substring` (definition names containing the key, never exact) and `literal` (declaration-shaped source lines the index does not store as definitions: `using X =`, `typedef … X;`, `type X`, `#define X`, `… as X` on an import line, `const`/`let`/`var`/`static X`, `X =` at column 0; comment lines are never rows). Every block carries `next=`; rows are capped at 6 per rung with `total=`, a printed body is cut past 8192 bytes with `truncated="1"`, and `floor="1"` marks a literal scan that could not read every file. The rungs can all be empty (`total="0"`). Only XML answers on a single root carry the block: `--json`, `--format=candidates` and multi-root answers print neither the block nor a marker; under `--token-budget` a held marker prints instead. A `this.X` / `self.X` subject gets no ladder. `RIPWIRE_NO_SHORT_KEY=1` turns it off. -- Measured on the practice questions this targets: `--for=Completions.getCompletionsAtPosition` on TypeScript went from a 1.3 KB answer with no definition to the namespace hop and the definition's body; `--for=ColumnPtr` (ClickHouse) and `--for=ExprType` (RisingWave) now name the `using` / `use … as` lines that declare them. Gate: `test/barrelshortkeycheck.sh`. diff --git a/changelog.d/c-root-include.md b/changelog.d/c-root-include.md deleted file mode 100644 index ecceec5e6..000000000 --- a/changelog.d/c-root-include.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed — C/C++ file edges: a quote include resolves at the include roots a build passes (#478, thanks @jflowers) - -A quote `#include "source/common/http/utility.h"` was probed only in the includer's own directory, so a tree that includes by root path had almost no file graph: Envoy resolved 289 of its 34,973 include edges, and `--deps` called it `shape="horizontal"`. On a miss in the includer's directory, a quote include is now probed at the repo root, at each `include/` directory beside the includer's directory or an ancestor (the same module-root rule the Python src-layout fix uses), and at a root `src/`; an `include/` or `src/` the tree does not hold is never probed, so `"../a.h"` cannot reach a root `a.h` through a directory that is not there. Each root is a shape a build file in the test corpus passes as `-I` (the repo root: Envoy's Bazel, htop's automake, tmux's `-iquote .`; `include/`: waybar's meson, CPython's `Modules/_hacl`; root `src/`: ninja's fuzzer build); `source/` and `lib/` bound nothing and are not probed. The includer's own directory still wins, the hit must be unique across the roots, and a name two roots both hold draws no edge and is counted in `imports_unresolved=` with `graph_partial="1"` on `--deps` and `--arch` (and `imports_unresolved=` on `--impact`). Angle includes are unchanged, so `` never binds a sibling `string.h`. - -Measured with @jflowers' ground-truth method from #478 (exact afferent and efferent coupling per directory, and mean absolute error, against an independent truth): Envoy goes from 289 to 34,196 of 34,973 static-truth edges, directories exact on Ca/Ce from 41 % / 1 % to 96 % / 92 %, MAE 9.62 to 0.21; htop, tmux and ninja reach 100 % / 100 % with MAE 0. A second oracle, the compiler's own `clang -E -H` with the `-I` flags read by hand from each build file, finds 0 false edges on htop, tmux, ninja and waybar, and a stratified hand sample of 30 Envoy edges finds 0. Envoy's health line now reads `shape="vertical"`; `--impact=source/common/common/assert.h` lists 614 importers where it listed 0, with its directory and test rollup. - -What it does NOT change: the call edges. The C/C++ call narrow (Rule 3's include evidence) keeps the includer-directory resolution, so no call edge moves. What does move is the declaration-to-definition proof a header-qualified selector widens through (`--defs=core/api.h:work`, and `--callers`, `--callees`, `--impact`, `--edit-check`, `--for` and MCP on the same selector): a bodyless declaration in `core/api.h` now folds into a definition whose file includes `core/api.h` by its root path, under the file graph's own rule (the includer's directory wins, and a name two roots hold proves nothing). Measured: on 61 graded C/C++ questions (Envoy, htop, tmux, ninja) 58 answers are byte-identical and 3 change only in file-graph counts, two `--impact` importer counts on tmux (149 to 157) and one `--defs` count on Envoy (79 to 78: `MainCommon::main`'s bodyless declaration now folds into its definition, because `main_common.cc` includes its header by root path); no graded item moves. With the root edges, Rule 3 bound about 2,060 more Envoy call sites without a hedge, and a hand read put about 300 of them on a wrong in-tree namesake (`std::format` on a local `fmt.h`, `absl::Span` on a local `class Span`). The two re-couple once external-qualified calls are vetoed. Still unresolved: the 2 % of Envoy edges written as a path suffix (`"utility.h"` for `source/common/http/utility.h`) and angle includes of in-tree headers; both need the build's own `-I` flags. diff --git a/changelog.d/claude-edit-route.md b/changelog.d/claude-edit-route.md deleted file mode 100644 index 0a6ef3fce..000000000 --- a/changelog.d/claude-edit-route.md +++ /dev/null @@ -1,3 +0,0 @@ -### Changed — `ripwire wrap claude` routes edits through a ranged Read and Claude Code's native Edit - -Claude Code refuses a native Edit on a file it has not Read in the same session, so the pasted wiring line "apply a whole-symbol edit without a whole-file Read" could not save a Read there: an agent that followed it still had to Read the whole file before it could edit, and in our own usage the ripwire edit verbs were never called from Claude Code (0 calls against thousands of native edits over five weeks). For `ripwire wrap claude` the line now says to take the symbol's line range from ripwire, Read only that range (`offset`/`limit`), then use the native Edit, and the MCP command it prints now passes `--mcp-tools=no-edit`, a new profile that lists every tool except `replace_symbol_body`, `insert_before_symbol` and `insert_after_symbol` (about 4 KB of tools/list, roughly 1,000 tokens per session; `--mcp-tools=full` restores them). The estimate behind the change is from our own usage and a size sweep (a ranged Read plus native Edit costs about a third of a whole-file Read plus Edit at 6 KB, and the gap widens with file size); it is an estimate, not a controlled agent run. Every other agent's `wrap` output is byte-identical, and the edit verbs themselves, their CLI flags and their docs are unchanged: codex, opencode, Gemini CLI and other agents without a read-before-edit rule keep the "no whole-file Read" line and the edit tools. diff --git a/changelog.d/cpp-extqual-veto.md b/changelog.d/cpp-extqual-veto.md deleted file mode 100644 index 27d8d3d4c..000000000 --- a/changelog.d/cpp-extqual-veto.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed — C/C++ call edges read the include roots, and a call qualified by a namespace the tree never defines stays hedged (#478, thanks @jflowers) - -The C-ROOT-INCLUDE change (#524) gave C/C++ trees their include-root file edges but kept the call narrow on the includer-directory closure, because coupling them bound about 300 Envoy calls to a wrong in-tree namesake without a hedge. The call narrow's include-file rule (Rule 3) now reads the root edges, behind two vetoes. First, a C/C++ call qualified by a name the tree never defines (no class, struct, alias or namespace of that name in any definition's scope or member path) takes no include proof: `absl::Span(…)` no longer binds Envoy's own `class Span`, and `fmt::ptr(…)` no longer binds `OptRef::ptr`. The pick keeps the existing `via="name"` hedge. Second, a `std::f(…)` call must name a member of namespace std itself, so `std::format(…)` never binds the `format` member of a `std::formatter<…>` specialization. Member calls keep the includer-directory closure, both for the receiver's class and for the include-file hedge, so a header only the root probe reaches no longer spreads `factory->isTerminalFilterByProto()` over its three factory classes. A bare call whose file declares `using ext::make;` with `ext` foreign is hedged the same way as `ext::make(…)`. A header that holds a prototype and an `#else` `static inline` stub of one C function no longer pins the stub over the real definition in a `.c` file (htop `Demangle_demangle`); the call keeps its split. - -Measured on Envoy (site dumps, hand read; the method is the one #478's Ca/Ce measure motivated): 2,819 call sites gain an unhedged bind against the decoupled build, and a hand sample of 30 of them is 29 right. Without the vetoes the same coupling was 18 / 30, and all 156 calls qualified by `absl`, `fmt`, `nostd`, `grpc`, `testing` and the like were wrong; the vetoes remove all 156. On htop, `Machine_new → Machine_init` gains on nine platforms through the root `Machine.h`, all right; ninja is unchanged. On ClickHouse, 122 sites gain (about 121 right), and 183 wrong binds drop: `std::find(…)` and `std::string_view(…)` had bound a polyfill `namespace std { struct string_view }` that only one file includes. Answers, against main with the same arguments: of 61 graded C/C++ questions (Envoy, htop, tmux, ninja), 50 are byte-identical; the 9 graded ones that change keep their tier and their credited items, and none loses one. Of the 60 stored practice questions, 59 are byte-identical and one (ClickHouse) moves only a counter. - -What it does NOT fix: a qualifier the tree does define but that aliases an external namespace (Envoy's `namespace Protobuf = google::protobuf`, so `Protobuf::Map` binds an in-tree `Map`): about 37 Envoy sites, because C++ namespace aliases are not recorded yet (so `namespace M = mine; M::f()` also stays hedged). It also does not fix the right typed-receiver binds the second veto gives up (about 44 on Envoy), a `using namespace ext;` directive (a bare call under it can still bind an in-tree namesake), or a foreign root over an in-tree inner segment (`ext::ns::f()`, where only `ns` is read). diff --git a/changelog.d/cpp-member-pointer-slots.md b/changelog.d/cpp-member-pointer-slots.md deleted file mode 100644 index 97bcd9ba5..000000000 --- a/changelog.d/cpp-member-pointer-slots.md +++ /dev/null @@ -1,24 +0,0 @@ -### Added — C++ pointers to member functions and C class tables with macro-cast receivers are joined to the calls through their fields (`--callers`, `--callees`, `--for`) - -A C++ pointer to a member function stored in a struct field (`{ "browse", …, &NinjaMain::ToolBrowse }` in a table of a struct -this file defines, a field assignment, an argument) is now a value reference to that class's method — its definition with a -body, else its declaration, never a same-named method of another class — and a call through a member pointer held in a field, -`( ninja.*options.tool->func )( … )`, is a call through that field (typed by the root's declared struct, as `x->f( … )` is). In -C, a designated initializer nested in an untyped braced member (`.super = { .writeField = f }` in a `ProcessClass` literal) -keys the field path from the enclosing literal's type (`ProcessClass.super` is a `RowClass`), and a call whose receiver is a -function-like macro whose whole body is one pointer cast (`#define As_Row( x ) ( ( const RowClass* )( … ) )`, -`As_Row( this )->writeField( … )`) is typed by the cast's type; any other call receiver (`f( x )->g( … )`, a macro of another -shape) joins by the field name only (`via="slot-name"`, `~` on `--for`). Not joined (floors): `&a::Twin::Go` when the repo -defines two classes of that name (a symbol carries no namespace), a namespace function `&ns::f`, a member pointer in a plain -variable, `->*` and an inline member-pointer field declarator `R ( C::*f )( … )` (both parse as an ERROR node: use a typedef, as -ninja's `ToolFunc` does), table entries inside an `#ifdef` within the initializer list (an ERROR node), `std::function` members (a call result or lambda is no value), and an inherited method whose base is -declared outside the tree (the store is dropped, and the slot lists the targets it does have). A pointer to a virtual method is a -candidate SET: the declaring class's definition and every override in a class derived from the pointer's class (a non-virtual -method a derived class hides is listed too: a hedge, never a pin), and a pure virtual's bodyless declaration is never a target when -an override has one (with no in-tree override, the pure declaration is the target). A template's dependent base written as a template parameter (`template struct M : B`) names no in-tree class, even one called `B`. An inherited or CRTP-base method (`&D::inh` defined in a base) names the nearest base's definition (two bases -at one level: none). An overloaded name lists every overload, so `targets=` counts definitions, not stored values. A qualified -spelling must match the namespace the in-tree class sits in (`&grpc::Server::start` is no `mine::Server`; neither is a bare -`&Server::start` under `using grpc::Server;` or `using namespace grpc;` written outside `mine`; a bare `&Server::start` written INSIDE `namespace grpc { … }`, where an external `grpc::Server` hides an in-tree GLOBAL `Server`, still binds the global class: a floor). A call site whose typed -tier hides name-only stores says `slot_name_also=K` on its ``, and `--for` `slot_fields=` keeps the fields a caller calls -through when its cap of 4 cuts. -Nothing here is a proven call: count= and reaches= never move. diff --git a/changelog.d/deps-shape-honest.md b/changelog.d/deps-shape-honest.md deleted file mode 100644 index dab0ad069..000000000 --- a/changelog.d/deps-shape-honest.md +++ /dev/null @@ -1,10 +0,0 @@ -### Fixed — `--deps` stops calling every deep graph `tangled` - -`` was read off `nccd` alone, and `nccd` passes 2 on any graph whose average file reaches a few hundred -others, cycle or not: with true include edges injected, Envoy's verdict went from `horizontal` (`nccd="0.09"`) to `tangled` -(`nccd="16.37"`) while it had three cycles of 2, 3 and 6 files among 7,786 measured files. `tangled` now means what it -says: at least 1 in 20 of `dep_files=` sits inside an include cycle, and the number behind it rides beside it as -`cyclic_files=` (absent at 0). Otherwise `nccd` says how deep: `horizontal` below 1, `vertical` from 1. That Envoy graph now -reads `vertical cyclic_files="11"`; cpython (314 of 3,544 files in cycles) and vscode (1,277 of 15,574) stay `tangled`. A -cycle list longer than its 20 rows now lists the largest first and says so: ``; `--limit` above 20 lists more. The 1-in-20 line is a heuristic, like -the `nccd` one it replaces. diff --git a/changelog.d/docs-demote-fp.md b/changelog.d/docs-demote-fp.md deleted file mode 100644 index f95a3bc8a..000000000 --- a/changelog.d/docs-demote-fp.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — `--recall` no longer hides hand-written reference pages as "generated" - -`--recall` ranks a generated document (a command capture, a test golden, an API dump) below every other match. Its size-and-fences test (at least 5x the median document and at least 35 % of lines inside ``` fences) also caught long hand-written reference pages full of short examples, so a page could miss even when its own heading was the query. The test now also requires that the document has almost no prose of its own: under 15 % of its bytes on non-blank, non-heading lines outside fences. Over the in-sample documentation repos, every compiler golden and fence-dump fixture the old test caught stays demoted (their prose share is 0.00), and the 74 hand-written guides, reference pages and blog posts it caught (prose share 0.31 or more) are ranked by relevance again. The fence count now follows CommonMark's closing rule: only a line with the same character (backtick or tilde) and at least as many of them closes a fence, so a four-backtick fence that quotes a ```bash block stays one fence and a capture of nested or cut-short markdown output stays demoted. Stated limit: a generated document whose output is mostly tables or unfenced text, such as a table-style OpenAPI markdown export, has prose-shaped lines and is not demoted by this test; only a marker phrase demotes it. The marker test (a document that says it is generated) is unchanged. The header now names the demoted documents too: `generated_demoted_paths="a,b"` lists up to five, best-first, even when they fall past `--top-k`, and `generated_demoted_named=K` appears when it lists fewer than the tally; a space, `%`, `"` or `,` in a path is percent-encoded. Thanks to @muayyad-alsadi, whose #409 and #428 started this documentation track. diff --git a/changelog.d/docs-html-window.md b/changelog.d/docs-html-window.md deleted file mode 100644 index 3c52132f6..000000000 --- a/changelog.d/docs-html-window.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — `--recall` serves the best-matching part of an HTML page it has to cut, not its start - -A recalled document with no headings to cut by (an HTML page's text, a heading-less note) that does not fit its share of the budget used to keep its first bytes: on an HTML page, that is the navigation list and the table of contents. It now keeps the run of whole lines, of the same size, that carries the most query evidence (each line scores the query words it holds, rarer words in this document weighing more; the earliest of equal runs wins, and a document whose start scores best, or ties the best, still serves its start: the run must out-score the whole lines of the head cut as it would be served). The separator line says so: `[truncated: K of N bytes; served bytes LO-HI, best query-term window]`, where LO-HI is the byte range of the served text (the converted text for an HTML page), and the body opens with a `…` line; on an HTML page the page's title (its first line) sits above that line, so a window from deep in a long page still names its page (the title is not part of LO-HI). A window that reaches the end of the page keeps the page's last line. A window that starts inside a code fence repeats the fence's opening line (`fence_reopened`); one that ends inside a fence or table keeps its own last line and closes the fence (`fence_closed`), rather than dropping back behind the line it was chosen for; the repeated opening line is charged to the window before its lines are scored, so the lines scored are the lines served; an opening line over 64 bytes is repeated as its bare fence run plus the first word of its info string (or not at all when the run alone is over 64 bytes), and a table whose rows start with a backtick is not a fence. - -Measured on the same 48-question docs set as the 6-doc recall default change: development questions fully answered went from 13 to 16 and held-out from 4 to 5, all on HTML pages (8 development and 4 held-out HTML questions; Markdown and reStructuredText answers are byte-identical). Mean answer size on the development set rose 0.7 %, from the marker, the `…` line and the title line. diff --git a/changelog.d/docs-topk6.md b/changelog.d/docs-topk6.md deleted file mode 100644 index cc996b734..000000000 --- a/changelog.d/docs-topk6.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — `--recall` and MCP `memory_recall` serve 6 docs by default, not 8 - -With no `--top-k` (CLI) or `top_k` (MCP), recall now serves the 6 best documents instead of 8. The 8000-token body ceiling is unchanged and is still divided across the served documents, so each one gets a bigger share: the usual miss at 8 was the right file with its answering section past that file's share. The default is now one constant shared by both front doors, and the capped note, `--help` and the MCP tool description name it. - -Measured on a 48-question docs set (34 development, 14 held out; Markdown sites, Markdown references, reStructuredText and HTML from six open-source projects), graded by a mechanical anchor rule: development questions fully answered went from 11 to 13 and held-out from 3 to 4; held-out questions with none of the answer printed went from 4 to 1 (development: 11, unchanged). Mean answer size on the development set rose 2.5 % (14.2 KB to 14.6 KB). One question got worse: its second answer sat in the 8th-ranked document, a release-notes page that is no longer served by default. Ask with `--top-k=8` to get the old behaviour. diff --git a/changelog.d/edge-prelude-veto.md b/changelog.d/edge-prelude-veto.md deleted file mode 100644 index 3db9fdcc8..000000000 --- a/changelog.d/edge-prelude-veto.md +++ /dev/null @@ -1,7 +0,0 @@ -### Fixed — a Rust prelude call or a macro with no in-tree binding no longer binds to a same-named definition - -`Ok(x)`, `Err(e)`, `Some(x)` and `drop(x)` with no binding of the name written in the caller's file (a definition there, or a `use` that names it) used to bind to whatever the tree defined under that name: a serde serializer's associated `type Ok = ();` (also in the caller's own file), a `type Err` alias, a function nested in another function and reached through `use super::*;`. Only a definition a bare call can name now cancels that: a function in scope (a nested one only inside the function that holds it) or a tuple struct, never an associated item, a type alias, an enum or a braced struct. A macro invocation (`assert!`, `json!`, `format!`, `write!`, `panic!`) bound by name to a function, module or type of the same name. Those calls are now counted as calls outside the tree (`external=` in the map header) and carry no edge. A macro invocation binds to an indexed `macro_rules!` of its name, or to a `#[proc_macro]` function its file's `use` names (a procedural macro such as `sql!`; a plain or nested function of the name is no macro); a user's own `Ok` type reached through a `use`, a same-file definition, and a local `macro_rules! assert` called in its own file keep their edges. A `macro_rules!` without `#[macro_export]` is in scope only where Rust puts it: its own file, its child modules (a crate root named by Cargo's `[lib] path` owns its directory like `lib.rs`), the parent module's scope when the module is declared `#[macro_use]`, or where a `use` names it; elsewhere, including a `src/bin/` file or a `main.rs` beside the library root, a call of that name is the standard or an outside crate's macro. Not read yet: a module declared with `#[path = ..]` (its calls of a parent's macro are counted `external=`), and source order inside a module. A Clojure `defmacro` called as `(name ..)` keeps its edge. - -Five Rust member names joined the standard-member gate after a hand sample of their unproven calls: `.expect()`, `.into()`, `.join()`, `.lock()` and `.status()` (0 of 40 sampled calls on Zed bound to the right definition; 2 of 20 on Nushell). Such a call with no receiver proof is now declined and counted in `declined_calls=`; a receiver typed as an in-tree class, or an impl's own `self.expect()`, keeps its edge. - -Rust macro invocations now carry `role="macro"` instead of `role="call"`; the cache is re-parsed once (parser version 297). diff --git a/changelog.d/exemplar-family.md b/changelog.d/exemplar-family.md deleted file mode 100644 index 8d27e496c..000000000 --- a/changelog.d/exemplar-family.md +++ /dev/null @@ -1,7 +0,0 @@ -### Added — `--for` on an add-a-new-X task names X's family: the existing siblings a new one joins - -A task like "add a new builtin scalar SQL function", "I need to write a scheduler plugin that lives in the tree" or "I want to add a new dialect" now ends with an `` block before ``: how the existing X are grouped (`rule=`: implementors of one base or interface, the entries of one registry map or dispatch table, typed instances of one C/C++ struct type, items under one attribute macro or decorator, or classes whose names end in X in one directory), the shared `key=` (and, for the base and registry rules, `key_p=`, where it is defined), `matched=` (the words of the task found in `key=` or `scope=`: why this family won, and a family that matches few of them is a weak guess), the directory the family was narrowed to (`scope=`), its size (`n=`, test, sample, example and benchmark code excluded) and up to four members, the typical one first (nearest the median size). Go registries and C vtable instances are found even where the implementors list is empty (Go has no `implements`; a C `const MeterClass X_class` is not a subclass). The block is present when a family resolves, or, when none resolves, only if the scan skipped something that may have been it (``: `oversized=` counts registry candidates over 4000 lines that were not read, `unread=` the files the scan could not read; the same two attributes ride a resolved family when it skipped something); the answer before it is unchanged, a token budget holds it (`held="budget"`), and the MCP `for` tool prints the same element. New gate `test/forexemplarcheck.sh` pins one fixture per rule (C typed instances, Go and C++ registry maps, a Rust attribute macro, a TS interface, a Python subclass), the wordings that fire and the ones that stay silent. - -The same block now carries a ``: the files outside the members' own that mention at least half of the family, ranked by share — where existing siblings are registered or listed (a name constant list, a defaults list, an exhaustive `match`, a package `__init__` or barrel `index.ts`), each with `share=` (the distinct member names it mentions out of the distinct names, or with `by="stem"` the distinct file stems out of the distinct stems: a name or stem several members share counts once, so one shared word never credits every member), the line to copy (the first naming the typical member; an import or a comment only when no code line does) and its text, cut at 120 bytes with `cut="1"`. A touch list whose scan could not read some listing files (unreadable, or over 4 MiB now) says `floor="1" unread="K"` beside `total=`: the count is a floor, and a `total="0"` is not "none". Names are matched with snake, Camel, kebab and UPPER forms folded, plus each member's registered name and file stem. Test, docs, vendored, generated, frozen and baseline files are not read; more than five listing files print five rows with `total=`, `shown=` and `capped="1"`. On the kubernetes scheduler-plugin task it lists `apis/config/v1/default_plugins.go` and `plugins/names/names.go`; on the risingwave scalar-function task `scalar/mod.rs`, the frontend's `expr/pure.rs`, the optimizer's `strong.rs` and the binder's `builtin_scalar.rs`. - -The block's legend follows `--legend`: the short form by default (`--legend=compact`), the full prose with `--legend=full` and in the MCP `for` tool; both define every attribute. A decorator is matched whatever its case (`@Widget()` for a task about a widget), a function's parameter list is never read as registry entries, and the name-ending rule names classes only (a function named `execute_command` is not a sibling of a command), needs them in at least three files (one file of `*Options` interfaces is a type list) and not only in `.d.ts` declaration files, and, because names alone are the weakest grouping, prints `grouping="affix-only, weak: verify one m before copying"` on its own family line, so the row hedges itself without the legend. diff --git a/changelog.d/file-graph-csr.md b/changelog.d/file-graph-csr.md deleted file mode 100644 index fc596a391..000000000 --- a/changelog.d/file-graph-csr.md +++ /dev/null @@ -1,7 +0,0 @@ -### Changed — the file graph's include closure is one flat store, built by a fold over strongly connected components - -The file graph's direct edges are now one flat offsets-plus-targets array (`FileAdjacency`), and the transitive include closure is a chunked hybrid store (`FileSetStore`: a presence bitmap and popcount rank per row, each chunk a sorted array or a bitset, whichever is smaller, identical rows shared by hash and verified). On vscode the closure is 3.74 MB where the sorted vectors it replaced were 32.5 MB; on a 100,000-file graph of mean closure 11,098 the process peaks at 580 MB where the rows alone in sorted vectors were 4.4 GB. Answers are byte-identical; `--deps`, `--arch=FILE` and the resolver read the same sets in the same order. - -It is built by one fold over the strongly connected components, sinks first: a component's row is the OR of its successors' rows plus their ids, taken back from the store into a single working row, so a file that imports 40,000 modules costs one row and not 40,000 (a first form of the fold held a bitset per imported module: 744 MB against 11 MB at 100,000 files). The fold asks the memory guard as it goes: over `--max-memory`, it stops, and a stopped fold is a failed build: every CLI verb exits 5 with one line, except the range form `--quality-delta=A..B`, which answers from the changed files with exit 6, disclosed, an edit whose write was already made, and the `--lsp` server (below); an MCP call is refused by name (the next call retries the build); and no `--quality-delta`, `--edit-check` or `quality_baseline` snapshot is scored or written from it. Nothing answers from a partial store, on the graph build and on the second fold `--deps` and `--arch=FILE` run. A write verb whose fold stops after its write is not refused: an edit (`replace_symbol_body`, `insert_before_symbol`, `insert_after_symbol`, the CLI edit verbs, each op of `--edit-plan --apply`) answers its receipt with `post_check` "not run" and the stop named, and `quality_baseline` answers what it wrote with `_index` "not rebuilt" and `_memory_stop`, so no answer tells an agent to retry a write it already made. `--lsp` answers a request whose fold stops with -32803 RequestFailed naming the stop and keeps serving. A payload past a 32-bit offset (8 GiB) is a disclosed stop, not a wrapped offset; that limit was modelled with a lowered bound, not run at 8 GiB. - -What it does NOT mean: no answer changes, and no edge is added. `RIPWIRE_TEST_FILESET_CHUNK_BITS` (test only) forces the chunk size on every fold. `bench/FILEGRAPH.md` has the measurements, the forms that lost, and the umbrella shape. `docs/LINEAGE.md` credits the classic closure and bitmap results the store rests on. diff --git a/changelog.d/for-d2-via-inherit.md b/changelog.d/for-d2-via-inherit.md deleted file mode 100644 index 5ff26fe3d..000000000 --- a/changelog.d/for-d2-via-inherit.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — a `--for` second-hop caller reached only through a by-name caller is hedged `via="name"` - -The `` element of a `--for` answer hedged a `d="2"` row only when that row's own call into a first-hop caller was matched by name. A row whose call was resolved but whose first-hop caller was itself matched by name alone (`via="name"`) printed unhedged, though nothing proves it reaches the function: on nushell, `--for="what calls PolarsPluginObject::into_value"` printed `CustomValueSupport::to_pipeline_data d="2"` with no hedge under three by-name first-hop rows. A `d="2"` row now carries `via="name"` unless at least one resolved call joins it to a first-hop row that is not matched by name alone, including a first-hop row the 16-row cap hides; a row with one such path stays unhedged. The six caller-hop legend clauses (named, ranked and subject routes, full and compact) say so: `via=name if every path has a by-name hop`. Only `--for` answers that carry a `` element change: that element's legend clause and the `via=` attribute of the second-hop rows this rule hedges. `test/forcallerhopcheck.sh` gains arm V1 on Rust, Python, TypeScript and C++ fixtures: a row reached only through a by-name caller, a row with one proven and one by-name path, a row whose own call is by name, a row whose only proven call goes into the by-name caller (Python), and a row whose only proven path runs through a first-hop caller the cap hides (Python). diff --git a/changelog.d/for-decl-row.md b/changelog.d/for-decl-row.md deleted file mode 100644 index d7abb23bb..000000000 --- a/changelog.d/for-decl-row.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--for` lists a function's prototypes when a signature change is in question - -A `--for` task about one C, C++ or Go function's signature (signature, prototype, new argument), or the function's bare name, now carries ``: its definition and every non-test declaration the index holds, including the header prototypes `--defs` folds into the definition's row. Present only when such a declaration exists. `functions="N"` says when the rows are N separate functions (a static elsewhere, an overload, another scope): a change reaches its own definition row and the `decl_only` rows right after it. `untied="1"` marks a prototype the index could not tie to a definition (a name match: still a declaration of that name at its line, possibly of another function of the same name). A name that is both a macro and a function stays silent. A macro name is never the subject (`PyAPI_FUNC(int) f(int)` names `f`); the bare form resolves the whole spelling only (`api.c` is silent). A token budget holds the block as `held="budget"`; its `next=` is the same `--for` call without the budget, which prints the rows (`--defs` folds a tied prototype into its definition). Capped at 8 rows (`shown=`, `capped="1"`, `next=--defs=`, which lists definitions with their prototypes folded). Not listed: call sites, the implementations of an interface contract (`--lego`), and prototypes hidden behind a macro (`__P((..))`, `DECLARE_FN(name, t)`). diff --git a/changelog.d/for-doc-body.md b/changelog.d/for-doc-body.md deleted file mode 100644 index 3744af2b4..000000000 --- a/changelog.d/for-doc-body.md +++ /dev/null @@ -1,13 +0,0 @@ -### Added — `--for` prints the top-ranked doc sections for a prose question about the docs - -A `--for` task that reads as prose, whose first ten file-tail rows (read in rank order) hold two or more doc files (`.md`, `.markdown`, `.mdx`, `.rst`, `.txt`, `.html`, `.htm`), now ends with ``: the sections of the first three doc files the answer prints (a signature row or a printed tail row, which a token budget can end before its doc rows) that rank highest for the task's words, chosen the way `--recall` chooses them. Each section row names its file, source lines, heading (redacted like the text, unless `--no-redact`) and rank in that file. The block is off under `--signatures-only`, and a multi-root run serves it in XML as it does in `--json`. - -"Reads as prose" is a cue list, not a parse: the task holds no backtick, `::`, `()`, lowercase snake_case word that ends in a letter or underscore, camelCase word, capital-lowercase-capital word, or dotted call `a.b(`. A task that is code-shaped in other ways still reads as prose and fires: `handle_upload_3`, `MAX_RETRIES`, `__init__`, `HTTPServer` and `getX` all do (`fordocbodycheck` pins it, and the block's legend says so). - -The block has its own allowance of 7500 bytes of text, so the rest of the answer is unchanged, and it prices itself: `est_tokens=` on the `` root counts the block alone (the answer's `est_tokens` leaves it out). The files share the allowance by `--recall`'s water-filling; a section that does not fit its file's share is skipped and the next tried, and the unspent rest goes to the best section still left: a section too big for it is an excerpt around the densest window of the task's words (`cut="1" range=S-E/N`, bytes S to E of the section's N). A cut says `capped="1"`, `dropped=` and `next=` the `--recall` call; a file that served nothing is an `` row with its reason (`unreadable`, `allowance` or `budget`). - -A section is served only when it is about the task's subject: it must hold a third of the task's subject words, at most three (subject words are its distinct words of three or more letters that are not common English nor `doc`, `docs`, `describe`, `explain`, `show`), except that each file's top-ranked section is always kept (it can be worded around the mechanism while the task is worded around the symptom). Sections that hold fewer are counted in `below_floor=` on the root (`"below_floor"` in `--json`). `fordocbodycheck` arm q1 pins it. - -A section is a lexical match, not a checked answer, and the block says so twice: `match="lexical"` on the root and a sentence in the legend (`--json`: `"match":"lexical"` and `"pick"`). - -Under a token budget (`--token-budget`, `--max-tokens` with `--detail`, MCP `budget_tokens`) the answer is served first and the block takes the tokens left after it: the allowance shrinks until the priced block fits, and only what does not fit is held (`held="budget" held_bytes=` on the root, `` per file, `next=` the same call without the budget). When `--token-budget` and `--max-tokens` with `--detail` are both given, the block is served into the smaller of the two. A budget the answer alone spends holds the block whole (the files named, with its own `est_tokens=`), and a held-whole block does not add the redactions of text it never printed to the run's redaction summary. `dropped=` counts the sections of a file the allowance did not reach, and `held=` / `held_bytes=` are read off the sections the budget kept out, not off totals. `--json` carries the block as a `doc_body` key (`unserved` entries carry a `why`) and MCP `for` carries the same element over its own rows. `RIPWIRE_NO_FOR_DOC_BODY=1` turns it off. Gate: `test/fordocbodycheck.sh`. diff --git a/changelog.d/for-file-anchor.md b/changelog.d/for-file-anchor.md deleted file mode 100644 index fbc31e4ae..000000000 --- a/changelog.d/for-file-anchor.md +++ /dev/null @@ -1,16 +0,0 @@ -### Fixed — `--for` on a file name answers with that file (reported in #466 by @pguerrerolinares) - -A `--for` subject that is a file used to miss it in every language: a bare basename (`--for=DynamicScreen.c`, -`--for=sync_ledger.py`) was split on its dot and anchored nothing, a snake_case stem (`--for=sync_ledger`) ended in an empty -`reason="no_candidates"`, and a hyphenated stem (`--for=document-remind`) returned a `confidence="high"` answer whose head was a -test sibling or a decoy that merely shared its words. Now a bare basename with a code extension, a hyphenated or snake_case stem -(when no symbol has that exact name) and a directory anchor the file(s) they name, and a subject that is nothing but the file -(stopwords and words like "file", "package", "script" aside) gets a `` page first in the answer: every def in line -order with its last line, its indexed callers (`in=`; `in="0"` is "no caller in the index", never "unused") and their files -(`by=`), a pasteable `next=` graph query listing the defs neither `main` nor the file's own top-level code reaches within 16 calls, and for a script whose work is its top-level code -(Bash, Python `__main__`, JS/TS, Ruby, Lua) that top-level text, capped at 40 lines / 2 KB with the rest disclosed -(``). The named file's top defs rank first. A name that matches several files lists them in `` and -picks none; a file named as context in a longer question is anchored but gets no page; a dotted `Class.method` with no such -file keeps its old reading. `RIPWIRE_NO_FILE_PAGE=1` turns the page off. Gate: `test/forfileanchorcheck.sh`. A path that names a directory (`--for=pkg/foo`) is read as that -directory before any shorter suffix, so it is never answered by an unrelated same-stem file elsewhere; a shorter-suffix match is -listed as `near="1"` and never paged. The whole page stops at 16 KB at a file boundary (`bytes_capped="1"`). The note the lift leaves in the answer's header comment now says what it did to the auto-served bodies: they follow the named file's rank, a def ranked lower gets none, and the expand flag reads one by name. diff --git a/changelog.d/for-file-syms.md b/changelog.d/for-file-syms.md deleted file mode 100644 index 42734afca..000000000 --- a/changelog.d/for-file-syms.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--for` names the shown files' other symbols (``) - -A `--for` answer often shows the right file and the wrong symbol of it: a neighbour ranks on body or doc text while the function the task needs sits a few lines away, unnamed. The answer now ends with ``, a few symbols of its first six shown files that it does not name anywhere, one row each (`n=` the owner-qualified name, `p=file:line`, no body). They are leads to read, not the answer. "Does not name" is read off the bytes the run emitted before the block (every section and every trailing block: declaration rows, the graph verb, re-exports, the short-key ladder, the literal anchor, dotted strings, `--with-graph`), by the spelling the answer prints: a name with its `#`, `$`, `@` or `~` mark or its `?` or `!` ending (`#addRoute`, `$tryShow`, `valid?`) is looked up as spelled, and a symbol a shown row stands on is never offered. A C or C++ header declaration and its definition elsewhere are one row (the definition's). `by="name"`: the name shares a task word. `by="callee"`: a definition named like one of the first eight `` rows calls, in the index's graph, a definition named like it: names resolve at both ends (a row's name is what the answer gave). The row is plain only when the shown row itself calls that very definition over an evidence-bound edge; `prov="split"` marks one arm of a split call; `via="name"` every other row (a namesake's call, a call to a namesake, or one bound by name alone). A row in a test or fixture file or scope carries `test="1"`. Three rows per rule, six files, eight head rows: the legend says so, and the open tag prints `files=` / `head=` only when fewer were read, and `name_total=` / `callee_total=` / `files_total=` only when a cap cut (the cut rows are counted, not listed). Present only when a candidate scores. Outside `est_tokens`; under `--token-budget` or `--max-tokens` the block is held (`held="budget"`, `next=` the same call without the budget, with the ranking flags the call had: `--no-route`, `--anchor`, `--pack-top-n`), with a one-line legend of its own, and a run that could not buffer its bytes, or whose buffer failed to flush or read back whole, prints `` and a line on stderr (the second also exits 1: the answer may be short). The block's legend comment sits right before it and spells every attribute the block prints as `name=`, in both legend dialects (a compile-time check holds the two strings to one attribute list); `legendcoveragecheck` reads a legend comment that immediately precedes a trailing block (``, ``, ``) for that block's own elements. On the MCP `for` surface the block is the last thing before ``, as on the CLI. XML only: `--json`, columnar, multi-root and the ranking-candidates export carry no block. `RIPWIRE_NO_FILE_SYMS=1` turns it off. diff --git a/changelog.d/for-tail-wide.md b/changelog.d/for-tail-wide.md deleted file mode 100644 index 9e4b4f717..000000000 --- a/changelog.d/for-tail-wide.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — a broad `--for` answer's file tail walks the file page's first 160 rows, not 40 - -A broad `--for` answer cuts its `` of lower-ranked files. The tail used to list its rank rows and then the first 40 rows of the file page; on the 15 stored R-ladder rows the wanted file sat past that cut, at file-page rank 57-177, in 6 of the 7 items the raised-limit page recovered. The tail now keeps its rank rows unchanged and walks the first 160 rows of the file page (`kForFileTailPageWalk`; on a code question its first 160 code rows, plus the doc rows within its first 160 rows). Every row the old 40-row walk listed keeps its place and its `sym=`, and comes first; the rows past that cut follow in the page's own order, path-only (a third to a half of a full row: 46.8 B mean on the in-sample calls, 70.6 B on the largest repo), and a file the head already shows is not listed there. `p_only=` on `` (JSON: `"p_only"` with a `"p_only_why"`) counts the path-only rows shown. The cut stays disclosed (`shown=`, `total=`, `capped="1"`, and `next=` continues the page), and the tail legend gains a clause that says so, present only when the page has more than 40 rows. Under `--token-budget` the old walk's rows fill first and the `` rows are funded as before the widening; the MCP `for` twin under `budget_tokens` serves the old walk's tail. An answer whose file page fits the first page is byte-identical to before, legend included. - -Measured on the 15 stored R-ladder rows (12 `--for`, 3 map) against main: R items credited 2 to 6 of 35, the 4 gained arrive in the first answer instead of after a raised-limit call, none lost, and the 15 answers grow 12 % in bytes (10 answers grow by 1.6-6.7 KB; the 3 map answers and the 2 whose file page fits one page are byte-identical). diff --git a/changelog.d/frame-high-confidence.md b/changelog.d/frame-high-confidence.md deleted file mode 100644 index 3ab556dcf..000000000 --- a/changelog.d/frame-high-confidence.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — `--for` reads `confidence="high"` on a name-exact route only when the anchor heads the rows - -- `--for` and the MCP `for` verb: `confidence=` measures how far the scores drop below the ranked head, so on a name-exact route it vouches for the anchor only when the head row is the anchor (a symbol of the anchor's name, or a row in the file the anchor is defined in or a file subject names). When another symbol heads the rows, as a plain word's namesake does in `where concurrent.futures.ThreadPoolExecutor is defined`, confidence reads low and `route=` appends `; anchor is not the ranked head (r=1 is NAME in PATH)`. The rows themselves do not move. A conceptual route has no anchor and reads as before. "The ranked head" is the row the answer prints as r=1: code above docs moves Markdown rows below the code rows, so a Markdown heading that scored first does not make the anchor look unproven when the anchor prints first. A word that spells a symbol the index holds (`FooBar.py` as the method `py` of `FooBar`) is not read as a file subject because a file of that spelling exists. The review of #568 found both (thanks CodeRabbit); `test/subjectanchorcheck.sh` arms FH(d) to FH(f) cover them, CLI and MCP. diff --git a/changelog.d/gen-docs-merge.md b/changelog.d/gen-docs-merge.md deleted file mode 100644 index 3e9cca95f..000000000 --- a/changelog.d/gen-docs-merge.md +++ /dev/null @@ -1,26 +0,0 @@ -### Changed — open pull requests stop conflicting on generated totals - -Every landing used to rewrite the totals in `README.md`, `docs/EVALS.md`, `docs/LIMITS.md`, `docs/TUNING.md` -and the showcase deck (the gate count, the cap inventory, the class counts, the TUNING census), so every other -open pull request that had added a gate or a cap went CONFLICTING and needed a main merge plus a full CI run. -Those totals are now a dated snapshot, `docs/counts.tsv`: the generators publish them from it, every site that -prints one carries its date ("counts snapshot YYYY-MM-DD"), and they rewrite only per-item lines (the absorb -list, the per-file cap tables, the per-cap sensitivity tables), which two pull requests touch on different -lines. `python3 docs/counts_snapshot.py show` prints the live numbers beside the snapshot, `check` compares them -and `refresh` re-takes the snapshot, all or nothing. How stale it can get is bounded: a refresh rides every train -pull request, `test/gendocsmergecheck.sh` goes red once the tree is more than 10 gates or 20 caps away from the -snapshot (`check --max-drift`), and the release workflow refuses a tag unless `check` is exact. A snapshot whose -totals do not add up is refused by every generator. Replayed with `git merge-tree` against the open pull requests -of 2026-11-03, each side cut after this change: #512 and #513 against each other conflicted on `docs/LIMITS.md` -and now merge clean; #505 against main keeps only its one-time absorb-list reshape; #506 keeps only the files a -build pins (`changelog.d/versions.md`, `docs/COMMANDS.md`, two showcase captures). Pull requests opened before -this change need one more main merge, which `scripts/merge_main.sh` settles on the total lines. - -`scripts/merge_main.sh` marks main's newer showcase capture for re-recording even when it did not conflict, and -the new `scripts/merge_main.sh --regen-after-build BIN` settles the marks of every merge since its last run from -a build of the merged tree: it re-pins only the parity labels the two merged sides pinned differently (and refuses, -naming it, a label both sides agreed on that moved), re-records the newest capture's legend-dictionary `dictv=` -pair, regenerates `docs/COMMANDS.md`, and commits. A marked line where one side only re-ran a generator now takes -the other side's rewording, unless the rewording changed a hand-written figure, its sign or qualifier included -(`~210` to `210` stays a conflict). New gate `test/gendocsmergecheck.sh`; `test/mergemaincheck.sh` gains the -rewording, qualifier and re-pin arms. diff --git a/changelog.d/graph-form-endpoints.md b/changelog.d/graph-form-endpoints.md deleted file mode 100644 index 399b3b787..000000000 --- a/changelog.d/graph-form-endpoints.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--for "path from A to B"` shows both sides of a missing path - -When the path verb a `--for` task already runs (`path from A to B`) prints no directed path (`reachable="0"`: none was found, or a `route_hedged="not-proven"` refusal withheld the chain it found), a new `` block follows inside the graph block. It carries A's callees and B's callers, each the plain `--callees` / `--callers` answer cut at 10 rows (`count=` is the total, `next=` the page after) with its own legend, and says a name common to both sides is a candidate link, not a proven path. A found path prints exactly what it printed before, and the ranking and `route=` are unchanged. The block is CLI only; the MCP `for` verb does not print it. New gate `test/graphformendpointscheck.sh`. diff --git a/changelog.d/history-walk-hang.md b/changelog.d/history-walk-hang.md deleted file mode 100644 index e5acab8a3..000000000 --- a/changelog.d/history-walk-hang.md +++ /dev/null @@ -1,10 +0,0 @@ -### Fixed — a history walk no longer downloads, and no longer hangs - -- On a long real history, `--for`, `--metrics` and `--exemplar` waited on `git log --name-only` with no limit: past 3600 s on a 310 K-commit ClickHouse clone, and on a blobless clone git fetched blobs from the network as it went (`.git` 15 GB to 33 GB). Now: - - No git command of ripwire's lazy-fetches on a partial clone: each carries `GIT_NO_LAZY_FETCH=1` (git 2.45 and later) and a closed `GIT_ALLOW_PROTOCOL` (git 2.6.1 and later), as a prefix on that command only. They are not set in ripwire's own environment, so the command `--run-trace` runs, and any other child that is not git, sees the environment you gave ripwire; and the clone of a URL root uses your own `GIT_ALLOW_PROTOCOL` and `protocol.*.allow` as `git clone` would. Versions are from git's source at the tags (`environment.h`, `git.c`, `transport.c`); only git 2.50.1 was run, an older git is simulated by a shim. A verb that truly needs an absent blob says so: `--quality-delta=REF` refuses naming the partial clone and the `git archive >/dev/null` that fetches it yourself, `--pr-context` lists the changed files from the trees (a dirty tree too) and refuses when it cannot, `--stray-content` reports a branch it could not read as `unknown`, never `merged`. - - The walks carry `--no-renames`. Rename detection was the only thing that read blobs. It changes one thing: a rename commit now also lists the path it left, which matters only when that path exists again at HEAD (whole history: textual 5 of 354 such paths, hono 3 of 286, tmux 1 of 23). - - Every record walk has a budget, 20 s or 200 K commits (`RIPWIRE_GIT_WALK_SECONDS`, `RIPWIRE_GIT_WALK_COMMITS`, 0 = none, at most 1,000,000,000; a larger or malformed value keeps the default). A cut keeps the newest commits and says so: `history_unread="1"` on documents with `churn=`/`amp=`, a `history_cut` comment and stderr line on every other CLI verb, and a `_history_cut` envelope note on an MCP answer. Where the clock cut a walk, the prefix it reached moves with machine load, so two runs can differ; the cut is cached until HEAD moves except under `--no-cache`, and a clock cut under about 1,000 commits is not cached. ClickHouse `--for`: 3600 s and unfinished, now 50 s. - - On a partial clone, no git that scans the promisor pack is started. With lazy fetch off, git parses every object of the promisor pack before it fails on the first absent blob, which is minutes to hours on a clone like ClickHouse (5.2 M objects: `diff --numstat`, `archive` and `log -p` each still running at 90 s). `--pr-context=REF` lists the changed files from `git diff --name-only` there and says so on stderr (a mode-only change is not told from a content change); elsewhere `--numstat` runs under the walk budget, and a stalled or failed one falls back to the same list, which keeps the budget too: if that read stalls as well, `--pr-context` refuses and says so. `--quality-delta=REF` and `--merge-scout` first ask git, without fetching, which blobs of the tree are absent and refuse before `git archive` when any is. `--whereis --with-history` and `--naming-calibration` answer `probed="0" r="partial-clone"` without running `git log -p`, and the rename map behind `--quality-delta` reports a window of 0 commits, truncated. Reads that fail fast on an absent blob still run (`blame`, `show`, `cat-file -e`, a zero-context `diff`, and `archive` when the tree is whole), and a failed one is said, never read as an empty answer: - - Plain `--quality-delta` on a dirty partial clone used to pass its gate: short-horizon-churn needs the churn window's tree and a blame of the edited lines, and when git refused them the row vanished or read `churn="ambient"`, so the answer said `gating="0"` and exited 0. Now the root says `unread_history=` (`churn-tree`, `churn-blame`, `churn-window` for a window walk cut by its budget, `renames` for a pinned sidecar floor whose rename record was refused), in XML, `--json` and MCP, a row whose blame failed says `churn="unknown"`, and an answer with no gating row exits 6, the partial-answer code, with a stderr line. A dirty `--quality-baseline` there refuses unless `--allow-dirty`, since it cannot tell what it would absorb. A clean tree reads no history and is unchanged. - - `--slice=SYM:VAR --since=REV` said `status="file_absent_at_rev"` of a file that is in that tree when its blob was not in the clone. It now says `status="unread_at_rev"` (`comparable="0"`) when the tree lists the path, or when the path is not there and the rename record that could reach an earlier spelling was not read. - - Over MCP, an `owners` call whose history walk was ended by its budget names the budget instead of "no git history". diff --git a/changelog.d/hop-context.md b/changelog.d/hop-context.md deleted file mode 100644 index ef7253ff4..000000000 --- a/changelog.d/hop-context.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--path` and `--connect` hops print the call site, the call and the callee's signature - -A `--path` hop row and a `--connect` `` row used to name only the two definitions an edge joins, so the next step was opening the caller to find the call and the callee to read its parameters. Each hop now also prints `site="file:line"` (a call in the caller that names the callee: one whose argument count the callee accepts first, then one qualified by the callee's owner; `sites="N"` when the caller has more than one such line), `call="…"` (that call's text from its line start, or from the statement when the caller is a one-line function, whitespace collapsed, joined across up to 3 lines while its parentheses are open, at most 120 bytes) and `sig="…"` (the callee's declaration signature, at most 160 bytes). The site is a name match, not proof that this call is the one the edge binds, and the legend says so; `site_by="first"` marks a pick among equally fitting lines when the caller also calls another callee of that name, and `site_by="nofit"` a site whose argument count the callee does not accept. Call text is redacted before any cut, and no cut lands inside a token or a redaction, so a credential prints whole-redacted or not at all; a callee name inside a redacted run withholds `call=` (`hop_redacted=`). On `--path` the first reaching pair prints all three and later pairs print the site only; on `--connect` a callee's signature prints once per group and not where its join row already shows it. A call or signature whose file cannot be read, or whose length changed since indexing, is left out and counted (`hop_unread=`), and a length change leaves the site out too; a site whose text does not name the callee is counted too (`hop_notext=`); an edge with no call naming the callee in its caller prints no site (`hop_nosite=`) but still prints the callee's `sig=`. Under `--max-tokens`, `--connect` drops these attributes first (`sig=`, then `call=`, then `site=`, named by `hop_shed=`) and only then trims as before. Two `` rows that would read alike (two same-named targets reached from one caller) now print `tp=`/`fp=`, the definitions they join. The MCP `path_between` and `connect` tools print the same rows. `RIPWIRE_NO_HOP_CONTEXT=1` turns the attributes off. New gate `test/hopcontextcheck.sh` covers C, C++, Python, TypeScript, Go and Rust hops, the site pick (owner, argument count, ties), credentials a cut would split, hops with no site, unreadable and edited files, the shed order, and one reference pass for a 40-hop path. diff --git a/changelog.d/iface-peel.md b/changelog.d/iface-peel.md deleted file mode 100644 index 877bef6c0..000000000 --- a/changelog.d/iface-peel.md +++ /dev/null @@ -1,7 +0,0 @@ -### Changed — a C++ member written as a smart-pointer alias, and a Rust local filled by a trait-object call, hand an interface-typed call to the dispatch tier - -A C++ member written `Http::FilterSharedPtr handle_;` (`using FilterSharedPtr = std::shared_ptr;`, a `typedef` of `std::unique_ptr`, or up to three plain aliases of such an alias) is read as its pointee through `->`, exactly like a member written `std::shared_ptr`: an interface pointee declines `gate="iface"` with its implementors counted, in place of every same-named method as a `via="name"` guess. A Rust `let d = s.get_decl( id ); d.run()` whose one proven callee returns `&dyn Command`, `Box` or `impl Command` types `d` the same way, and an inherent method of another type that happens to be called `run` is no longer listed. - -The alias must be in scope where the member is written: its own class, an enclosing namespace or class, or the namespace the member's qualifier names. A template parameter that shares an alias's name, a name that is not visible there, an alias that is also a class, two same-scope aliases that disagree, a pointee written in `std`, and an alias of a concrete class whose virtual methods derived classes override all keep the by-name rows; a deleted or defaulted operator, a `friend` declaration or a member-template declaration never makes a class an interface. - -Floors, stated: a using-directive (`using namespace x;`) is not followed; a using-declaration of the alias's name in a nearer scope than the alias is not seen; a Rust `type` alias of a class-like name in a mixed C++/Rust tree can stop an unrelated C++ alias from peeling (the base rows stay); a local or parameter written as an alias is not peeled; Go interface-typed results are not read. diff --git a/changelog.d/impact-path.md b/changelog.d/impact-path.md deleted file mode 100644 index 5f11b2174..000000000 --- a/changelog.d/impact-path.md +++ /dev/null @@ -1,14 +0,0 @@ -### Fixed — `--impact=` lists the file's importers instead of answering `found="0"` - -The selector is a symbol, so a path matched nothing and the answer was `found="0"` with no hint, even for a file hundreds of -files include. When no symbol matches and the selector names one indexed file (root-relative, with a leading `./`, absolute -under the root however the root was typed, or a path suffix only one file has), the answer is that file's importers under -`selector="file"`, exit 0, with the importer listing's own `shown_importers=`, `importers_capped=`, `importers_next=` and -rollup. The answer names the file it used, `file=`, and a suffix match says so, `matched="suffix"`: a path to a deleted or -unindexed file that another file's path ends with answers for that other file, visibly. It has no call reach: `FILE:SYM` -still picks one symbol. A symbol match always wins, so no selector that answered before answers differently. A suffix several -files share stays a refusal whose `near=` is the first such path, with `matches=N` counting them. -When files name the path in an include or import that resolved to no indexed file (an include root the index does not know), -the answer carries `unresolved_naming=N`, so `importers="0"` on such a file reads as a possible floor, not as unused: Envoy's -`envoy/registry/registry.h` answers `importers="0" unresolved_naming="580"`. A directive that resolved to another file of the -same name (`a/util.h` beside `b/util.h`) is not counted. diff --git a/changelog.d/impact-rollup.md b/changelog.d/impact-rollup.md deleted file mode 100644 index ffcf57f4a..000000000 --- a/changelog.d/impact-rollup.md +++ /dev/null @@ -1,7 +0,0 @@ -### Added — `--impact` sums up the importers a cut listing hides - -A cut importer listing showed the 40 most-imported importers and nothing about the rest. Beside `importers_capped="1"` the -root now carries `importers_tests=` (how many importers are test, bench or fixture files) and `importers_dirs=` (the three -directories, by their first two path segments, that hold the most, as `dir:count`), both counted over every importer. On -cpython, `Lib/os.py` has 512 importers: `importers_tests="353" importers_dirs="Lib/test:346,Lib:58,Lib/idlelib:24"`. XML, -columnar, JSON and the MCP tool carry it. diff --git a/changelog.d/index-alias-const.md b/changelog.d/index-alias-const.md deleted file mode 100644 index 938f58833..000000000 --- a/changelog.d/index-alias-const.md +++ /dev/null @@ -1,7 +0,0 @@ -### Added — C++ `using` aliases, namespace aliases and function-pointer typedefs are definitions, and `--defs` prints their target - -- C++ `using X = Y;` (plain, templated `template using V = …;`, and class-scope `using value_type = …;`), `namespace L = lib;`, function-type and function-pointer typedefs (`typedef void (*Fn)(int);`, `typedef int Fn(int);`, `t="functype"`) and pointer typedefs (`typedef struct S *SP;`) are now definitions in C++ files and in every `.h` (a header reads as C++). A `.c` file reads with the C grammar and is unchanged: a function-pointer typedef there is still no definition. `--defs=ColumnPtr` on a tree with `using ColumnPtr = COW::Ptr;` used to find nothing; it now lists the alias. A `using` written inside a function body or lambda stays a local name, and `using namespace ns;` / `using ns::name;` add no definition. -- `--defs` rows for a `using` or namespace alias carry `to=` (the written target, whitespace folded and comments dropped), `to_next=` (the defs call for the target: `--defs=Owner::leaf` when that spelling binds a definition, else the last name segment, which may list namesakes) and, when the file can no longer be read, `to_unread="1"`. A target longer than 160 bytes is cut with an ellipsis and marked `to_capped="1" to_bytes=N`. The JSON form carries the same keys, and a multi-root workspace (`ripwire a b --defs=X`) prints them too. A `typedef` row keeps its old shape (no `to=`). -- Because aliases are now definitions, a name-exact `--for` on an alias lands on it (on ClickHouse, `--for=ColumnPtr` now routes name-exact and prints `IColumn_fwd.h`'s declaration), and C++ trees gain namesakes where an alias shares a name with another definition. -- A `--for` sentence around a name whose only definitions are `using` or namespace alias rows (``what does `using ColumnPtr = ...` expand to``) still gets the `` ladder, so its literal rung prints the `using` lines; a name that also has another definition stays resolved. A literal row no longer names its own alias row as `in=`; it names the definition around the alias, so a class-scope `using` keeps its class. -- TypeScript module-level `const` values are unchanged. Gate: `test/aliasdefcheck.sh`. diff --git a/changelog.d/install-hardening.md b/changelog.d/install-hardening.md deleted file mode 100644 index bfee82453..000000000 --- a/changelog.d/install-hardening.md +++ /dev/null @@ -1,35 +0,0 @@ -### Security — `skills/install.sh` takes the installer hardening list from #293 (hardening from #293 by @s0undt3ch) - -@s0undt3ch's #293 proposed moving skill and hook installation into the binary. We kept the installer a script, but -its four review rounds produced a list of ways an installer can be talked into writing or deleting the wrong thing, -and every item that applies to a shell installer now lives in `skills/install.sh` (#293, thanks @s0undt3ch): - -- **Absolute roots only.** A relative or empty `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTS_HOME`, `CODEX_HOME` or - `HERMES_HOME` is refused with status 2 instead of installing relative to the current directory (an empty `HOME` - used to mean `/.claude`). Windows drive paths still count as absolute; an explicit destination argument still - resolves against the current directory. -- **Nothing it creates is group- or world-writable.** The umask is only tightened, so `umask 000` no longer yields a - 0777 skills directory or a 0666 `settings.json`, and a stricter `umask 077` is left alone. -- **`settings.json` / `hooks.json` are merged safely.** A symlinked file (a dotfiles setup) is written through and the - link kept, the temp file is built beside the target so the final rename is atomic, and the file's mode survives the - merge (CWE-732). A first-time file is still created owner-only (0600), as before. -- **The hook command is quoted** when its path needs it, so a checkout under a path with a space registers a hook - that runs; the ordinary path is registered byte-for-byte as before, and re-runs still recognise the entry. - `ripwire --doctor --agent=claude|codex` and the hook-removal step in `INSTALL.md` read the quoted form too, so a - working hook is not reported broken and the documented removal takes it out. A path holding a single quote is - refused. -- **`jq` is found only through absolute `PATH` entries** (CWE-426): an empty or relative entry no longer runs a - `jq` planted in the current directory. -- **No stray temp files.** The installer's temp files are removed on a normal exit and on HUP, INT and TERM. A - SIGKILL cannot be trapped, so it can still leave one `.tmp.*` file beside the file being written. -- **The prune removes only what is provably ripwire's:** a symlink the previous manifest recorded, a dangling one, or - one pointing into this checkout (compared by whole path components, never a string prefix, and never through a `..`). - A live symlink of your own that happens to be named `ripwire-*`, and a regular file with such a name, are kept and - reported. Likewise a regular file of yours sitting at the name of a skill ripwire ships is kept, not replaced. - -Not carried over from #293, and why: the items that apply only to the embedded store (byte-comparing it against the -binary, `lstat` of its key and intermediate directories) have no counterpart here, because this installer links -straight to the checkout; the release-archive lookup of the `ripwire` binary does not apply, because this script never -needs the binary; the Windows copy fallback and the link-loop/link-failure reporting that #293 also added were already -here (the copy fallback and the `FAILED` report came with #334, and a loop at a shipped name is repaired). Thanks again -@s0undt3ch. Windows behaviour is unchanged. Gate: `test/skillinstallcheck.sh` sections (J)-(Q), each red against the previous script. diff --git a/changelog.d/k-decl-ident.md b/changelog.d/k-decl-ident.md deleted file mode 100644 index bf42cc4e1..000000000 --- a/changelog.d/k-decl-ident.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — a `--for` answer's `` block keeps a header declaration apart from an unrelated same-name definition (thanks CodeRabbit, #555) - -The `` block folds a bodyless C/C++ header declaration into a same-(scope, name) definition in another shown file, so the definition's row stands for both. Until now it did that with no check that the two were one function, so an unrelated namesake in another shown file could replace the header's row. A declaration now folds only when the definition's file answers its header: the definition is in a file that `#include`s the header, directly or through other headers (an umbrella header such as `tmux.h`, or a quote include written from an include root, `#include "include/api.h"` in `src/`), or the two share a path stem (`src/foo.h` and `src/foo.c`, `.cc`, `.cpp`, `.cxx`, `.m`, `.mm`). Otherwise both rows print and the header keeps its row. An angle include, or an include no rule resolves, ties nothing, so such a pair prints two rows. Measured on the 36 C-family calls of the 321 in-sample `--for` calls: 24 printed rows came from a fold, and all 24 still fold (every definition file there includes its header); one call (mf1-tmux-04) changes after the prototype rule, `name_total=` 54 to 55 (a header declaration that had folded into a source-file prototype keeps its own row), with the five printed rows identical, and the other 35 are byte-identical. `test/forfilesymscheck.sh` arms k1-k7 cover C and C++: an unrelated definition, a direct include, an umbrella header, a shared stem, a root-relative include, a source file holding only the prototypes, and an uppercase `.C` stem. The case came from a CodeRabbit review on #555; the prototype and `.C` cases from the one on #566 (thanks CodeRabbit). diff --git a/changelog.d/large-alloc.md b/changelog.d/large-alloc.md deleted file mode 100644 index 4e40996f9..000000000 --- a/changelog.d/large-alloc.md +++ /dev/null @@ -1,12 +0,0 @@ -### Fixed — macOS peak footprint about halved: freed large blocks leave the process - -The default macOS zone keeps freed large blocks resident, and an index frees many: each parse worker's reference array -doubles through 8-65 MB blocks. `operator new` of 256 KiB or more now gets its own mmap region and goes back to the OS on -delete (`src/largealloc.cpp`, macOS only, off under sanitizers). Peak footprint of `--deps --no-cache`, interleaved -runs: Envoy 1,515 to 767 MB, cockroach 1,848 to 984, vscode 2,873 to 1,482. Answers are byte-identical; CPU time is flat and wall stays within load noise. -`--max-memory` reads the footprint between phases, and the blocks the route returns early are gone from that reading, so a run -can now peak above its cap, pass every reading and finish. That is not stopped; it is said: one stderr line on any verb, -`memory_peak=` in the default map's header, `_memory_peak` in the MCP envelope, naming the peak and the cap and noting that the -cap is a rule of thumb against running out of memory, not an exact line (the 65 % default is not a measured threshold). The -disclosure is on in builds that link the route. `test/largeallocpeakcheck.sh` keeps the peak ratio and pins the disclosure. A chunked worker store removed only 0-17 % and was not kept. Linux has no route: on a generated -fixture glibc's max RSS moved 2 % with a fixed mmap threshold. Windows was not measured. diff --git a/changelog.d/lit-site-decoy.md b/changelog.d/lit-site-decoy.md deleted file mode 100644 index 25d780ec4..000000000 --- a/changelog.d/lit-site-decoy.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — `` orders a literal's sites by nearness to the answer and cuts the far ones when they do not all fit - -The literal-site block used to list a literal's byte-exact sites in an order that did not look at the question: on a widely used name (a setting read in hundreds of places) its 32 rows filled with readers in unrelated directories, CI scripts and other tables that hold the same string. Now each read site is placed against the answer above the block: its signature rows and the bodies it prints (the fold, the slot, `--detail` and the auto bodies), plus the literal's own source definitions (a symbol named its last segment that the earlier segments place; a generic last segment alone, such as `enabled`, anchors nothing). A site is near when it stands in one of their files or directories, or its function calls or is called by one of them; a hub (called from more than 16 functions, such as `getSetting`) puts no caller one call away, and when more than half of the answer's rows are themselves readers of the literal (a body match) those rows anchor nothing. Table rows still come first, and near sites come before far ones in each group. - -A block whose sites all fit is only reordered (a vendored, minified or generated copy is listed after the product sites); nothing is cut. When the sites do not all fit the rows: a vendored, minified or generated copy beside a product site is cut (an authored `static/` or `locale/` file is product code); when something stands near the answer, the far sites are cut; when a table row stands in the answer's file or one call from it, the rows of the other tables are cut; with none there and several tables, the far rows outside the home table are cut. The home table is the file with the most table rows of the literal, then the one that declares it (an X-macro `#define` list over a map or set), then the one with the most sites outside any function; a tie names no home and cuts nothing. The registrations the reserve keeps (quoted keys outside any function) are never cut. When the cut moved a row (a site the old order would have listed is gone), `lit_cut="N"` counts the sites cut and a legend clause, printed only with it, says why; a cut of sites that stood past the rows anyway is not said again (`capped="1"` and `shown=` already say it). `next=` still lists every site. A block with nothing near the answer and a single table keeps its old rows. Gate: `test/forliteralcheck.sh` arms D1-D14. diff --git a/changelog.d/manifest-listed-in.md b/changelog.d/manifest-listed-in.md deleted file mode 100644 index b0a492366..000000000 --- a/changelog.d/manifest-listed-in.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — `--for` shows the manifest lines that list a shown file when the task adds something new - -When a `--for` task is a creation task (it says new, add, register, plugin or extension as a word — not inside an identifier such as `Plan::AddTarget`) and a build, package or registry manifest at the root or in a directory above the ranked rows lists two or more of their files (a list of the exemplar's siblings, not a single `"main"` entry), the answer now carries those lines after the file tail: `` with each line byte-exact as ``, preceded by the less-indented line it sits under (automake's `dist_tmux_SOURCES = \`, package.json's `"./timing": {`, a Bazel `EXTENSIONS = {` dict). Read: Makefile.am (a configure-written Makefile beside it is skipped), Makefile, CMakeLists.txt, meson.build, BUILD/BUILD.bazel, `*.bzl`, package.json, jsr.json, deno.json, and Cargo.toml (CODEOWNERS is ownership, not a listing, and is not read). `by="path"` names the file, `by="dir"` only its directory (two or more whole path segments, so `middleware/log` never matches `middleware/logger`; make, CMake, meson and Bazel BUILD files are matched by file path only, so a dependency label is not a listing); `also=` lists the other shown files the same manifest names. Files under vendor/, third_party/, node_modules/ and bower_components/ are vendored and skipped. Comment lines, glob or variable listings and lists keyed by a command or option name (man pages, docs indexes) are not matched. Default regime and XML only; `RIPWIRE_NO_LISTED_IN=1` turns it off. On "new tmux command" the answer now prints the Makefile.am sources list (+0.5 KB), on hono's "new middleware" the package.json and jsr.json `exports` entries (+1.3 KB), on Envoy's "add a new HTTP filter extension" `extensions_build_config.bzl` and the extension's BUILD file; answers whose task has no creation word are byte-identical. - -A name counts as listed only where the manifest resolves it from its own directory, inside one stanza: `legacy/x.c` is not `x.c`, a subdirectory manifest's root-anchored `$(top_srcdir)/x.c`, `${CMAKE_SOURCE_DIR}/x.c` or Bazel `//:x.cc` is not its own `sub/x.c` (and is not listed), another package's `package.json` does not list this package's directory, an `exclude = [...]` / `list(REMOVE_ITEM ...)` list and a `#[[ ... ]]`, `/* ... */` or trailing `#` comment name nothing, and a second name counts as a sibling only in the same top-level stanza. A manifest above the crawl size ceiling is not read. The legend says the block is not exhaustive. The two caps on what is read (a directory's `*.bzl` files past the eighth, shown files past the sixteenth) are disclosed on the first `` as `files_capped="1"`; an answer with no `` makes no claim, so there is nothing to disclose. diff --git a/changelog.d/merge-main-guards.md b/changelog.d/merge-main-guards.md deleted file mode 100644 index ac384a941..000000000 --- a/changelog.d/merge-main-guards.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed — `scripts/merge_main.sh` no longer stops on an RST underline, and refuses a kParserVer both sides bumped - -The staged-marker check read the RST `=======` underline at `test/restdocscheck.sh:77` as a leftover conflict marker, so every lane's merge of main stopped on a file that was identical to main's. The check now skips a path whose staged text is `MERGE_HEAD`'s own; a real `<<<<<<<` / `=======` / `>>>>>>>` hunk in any other path is still refused and named. - -When both sides bumped `kParserVer` (`src/ingest_cache.h`) above the merge base, the merge is refused unless the merged value is above both. The message names the lane's and main's numbers and suggests max+2 (the rich file tag is `kParserVer` + 1), and reminds you to update `kIngestParserVerMirror` in `src/quality.h`. One side bumped, or neither: nothing is checked. `test/mergemaincheck.sh` has arms for both. diff --git a/changelog.d/mf1-lit-literal-anchor.md b/changelog.d/mf1-lit-literal-anchor.md deleted file mode 100644 index d1a0759c2..000000000 --- a/changelog.d/mf1-lit-literal-anchor.md +++ /dev/null @@ -1,22 +0,0 @@ -### Added — a `--for` task that names a literal carries the literal's sites and the code that handles it - -A `--for` task that carries a "double-quoted message" (at least six bytes inside the quotes, a letter, an ASCII space inside, no leading or trailing blank: `"a b"` does not qualify) or a CONST_SHAPED word (an environment variable, an -error code or a setting name such as `FZF_DEFAULT_COMMAND`, `FST_ERR_DUPLICATED_ROUTE`, `PYTHONASYNCIODEBUG`) now ends with a -`` element, just before ``: the grep verb's byte-exact scan of the index for that literal. Its `` rows are the -sites in source files, labelled by the parse (`k="string"` inside a string literal, `k="code"` outside one, `k="?"` a file not -classified) with the definition holding each (`in=`, `ink=`), sites inside a function first; comment sites, Python docstrings (a -triple-quoted string that is the first statement of a module, class or def body; the same string opening a call-argument line is a -site), test paths and doc, shell or config files are counted (`comment=`, `test=`, `doc=`), never listed as a handler. The function -holding a shown site is printed whole when it is ten lines or fewer and its body is at most 3,000 bytes (``, at most three; a short but large or minified body is an excerpt); a longer one is an excerpt (``): its -signature line and one window of the site line minus one to plus five per shown site (`l=` lists the ranges, windows that overlap or touch are merged and an ellipsis line sits -between disjoint windows only, `of=` is the body, `next=` the whole body; each window line is cut at 200 bytes, marked `cut="1"`, so a minified one-line body is never printed whole); a body the answer already printed is not repeated (`body="above"`); an owner past the three-body cap prints no body and its rows say `body="held"`. -At most eight `` rows are shown (`shown=` against `src=`, `capped="1"` when sites were left out; `next=` lists every site); `floor="1"` marks the counts as lower bounds when the scan stopped early or a source site was left unclassified (past 64 hit files, or a file the parse could not read), and the matched lines and excerpts are credential-redacted like any other body. -Several sites are always listed with counts; a literal with no source site (found only in tests, docs or comments, or not at all) -carries no block. The element carries its own legend (a 458-byte form under the default `--legend=compact`, the long text under `--legend=full`) and its size (`bytes=`, not in `est_tokens`); -everything before it is byte-identical to the answer without it (`RIPWIRE_NO_LITERAL_ANCHOR=1`); `--json`, the columnar and -candidate-list dialects, a multi-root run and an explicit `--token-budget` carry none (the default XML dialect with a single root only). A `--flag` spelling is not read as a literal. Gate: -`test/forliteralcheck.sh`. - -Two more shapes are read from the task's own words, since a stored or agent task is usually bare. `shape="dotted"`: a dotted key of three or more segments, or two with a whole word `setting`, `option`, `key`, `command` or `flag` beside it (`files.autoSave afterDelay setting flow`). Segments are identifiers; a leading dot, a file or host suffix, the name of an indexed file, a `self.` or `this.` chain and a call are not keys, a capitalised first segment (`Class.method`) needs the cue word, and a word of 4 or more segments is a key even when its last segment is a language extension (`envoy.filters.http.lua`), so a quoted file name that long reads as a key too. A row holds the key: a site inside a longer name or a child key is counted in `longer=`, never a row. Quoted sites rank first, with 2 row slots kept for a quoted string site outside any function (a registration or table; a C `#define` is not one), and a row cut at 200 bytes opens 40 bytes before the key. `shape="message"`: three or more words with a whole word `error`, `notification` or `message`: a backticked or single-quoted span, else the task minus a leading or trailing cue, else the words before a cue inside the sentence. The whole text is tried first and needs one word that is not a function word and must end in one (`must not be empty`); then fragments of the longest placeholder-free run of words, down to three words with at least two such words (`frag="1"`: `s=` is only part of the task's words). Hits stand on word edges (a hit inside a longer word is counted in `longer=`; the letter of an escape such as `\n` or of a printf verb such as `%s` right before a hit is not a word byte). A code row (`k="code"`) is never printed for a message; a `k="?"` row stays. Either shape adds a second legend comment. - -Literals are tried in order, quoted span, CONST words, dotted keys, message, until one has a source site, and `try="N"` says the anchor was the task's Nth. The literal caps: 4 scans of a quoted span, CONST word or dotted key, and up to 8 scans for a message (that second cap is silent). When the first cap skipped a literal and nothing scanned anchored, the answer ends in `` with its own legend and no ``; `next=` is a `--grep=` of the first skipped literal. Not covered: a key past byte 512 of its line stays hidden with no word, and a registration inside a function ranks among the readers by path, so it can fall past the shown rows (`capped="1"` and `next=` say rows were left out). diff --git a/changelog.d/mf1-nx-dispatch-hop.md b/changelog.d/mf1-nx-dispatch-hop.md deleted file mode 100644 index e423ceb33..000000000 --- a/changelog.d/mf1-nx-dispatch-hop.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — `--for` names the implementations that can run at an interface call the task names - -When a name-exact `--for` task names a function and a member call inside it (`Parser::Load file_reader_->ReadFile`, `MDBook::execute_build_process renderer.render`) and that call goes through an interface, trait or abstract receiver the resolver declined, the answer now carries `` after the caller hop: one `` row per in-tree implementation that can run there (`ctor=` rows name where that type is made), and, when exactly one production candidate is left, its body. Test doubles (test code, or a type only test code names) are counted in `test=` and not shown; a type that production code builds — in a registry, a factory with a production caller, a file-level registrar, `main`, a Go `init`, or a C/C++ OS entry (`wmain`, `_tmain`, `WinMain`, `wWinMain`, `DllMain`) — is never counted as a double. When the task writes a receiver (`disk_->ReadFile`), only a site with that receiver answers. Languages that type structurally carry `floor="1"` (an unlisted type can still run there) and never get a single body. Default regime and XML only; `RIPWIRE_NO_DISPATCH_HOP=1` turns it off; answers whose task names no such call are byte-identical. On ninja's "Parser::Load file_reader_->ReadFile" the answer now prints `RealDiskInterface::ReadFile`'s body (the test file system is counted as a double); on mdbook's "renderer.render" it lists the four production renderers and where they are built. - -Not covered: a task that writes the TYPE as the receiver (`Disk.ReadFile`) or a reference alias of the field gets no element. On an index the memory guard cut (the MCP `for` answers over one; the CLI refuses it), a type no kept file builds is not counted as a double, because the cut may have dropped the file that builds it; only a definition on a test path is demoted there. diff --git a/changelog.d/notfound-on-cut.md b/changelog.d/notfound-on-cut.md deleted file mode 100644 index 9462f54ba..000000000 --- a/changelog.d/notfound-on-cut.md +++ /dev/null @@ -1,24 +0,0 @@ -### Fixed — a "not found" from an index the memory guard cut no longer reads as proof of absence - -When the memory guard stops a crawl or a parse, the MCP server keeps answering from the partial index. A result from that -index has always carried `_memory_stop`. A refusal did not: `find_symbol` on a name outside the kept files, and every other -not-found or error built after the first request, said only "symbol not found", which an agent reads as "the tree does not -define it". Every error from a cut index now ends with the same disclosure, prefixed with the one thing a not-found reader -needs: the index is partial, so an absence is unproven. It names the stop, the number of files the answer covers (and how many -of them were parsed) and the override (`--max-memory`, `RIPWIRE_MAX_MEMORY`, or a smaller root). The sentence is the same on every path: the first request that built the cut index (which is often an agent's -first call), a `batch` sub-query's `err=`, and the `edit_check` / `quality_delta` refusals. A `grep` payload from a cut index -no longer says `"complete":true`, since a hit may sit in the files that were never read. An error from a whole index, -including another root served by the same process, is unchanged. The CLI needed no change: every verb except the map already -refuses a cut index with exit 5. - -The crawl also walks in a fixed order now (pre-order, each directory's entries by file name). It used to take the filesystem's -directory order, and the memory guard stops after N entries, so which files survived a cut depended on the filesystem: the same -tree under the same limit answered from different corpora on macOS and Linux (CI `memguardcheck` B6b). A walk that finishes -sees the same files in the same final order as before; only a cut walk changes, and it is now the same cut everywhere. - -One visible side effect on trees past the report cap: where a repository holds more than 500 files of types ripwire does not -index, the off-index scan of `--grep` and the `--skipped` listing keep the first 500 of them, and "first" now means file-name -order instead of the filesystem's directory order. The indexed hits are unchanged; the off-index hits and the listed rows can -differ (measured on `textual`: `--grep=Widget` off-index hits 20 to 25), and the scanned count can now show -`unindexed_files_skipped=` for files in the new set that could not be read. The cap is still disclosed -(`unindexed_candidates_capped=1`). diff --git a/changelog.d/path-from-upstream.md b/changelog.d/path-from-upstream.md deleted file mode 100644 index 94637f469..000000000 --- a/changelog.d/path-from-upstream.md +++ /dev/null @@ -1,3 +0,0 @@ -### Changed — `--for=path from A to B` keeps the path's own rows in the ranked head - -In a task that asks `path from A to B` (opening with `path from`, or `trace`, `show`, `explain` or `what is` then `the call path from`, `the code path from` or `the execution path from`, and not also asking `who calls` or `callers of`), a ranked caller of A (a resolved call, not a by-name one) that A does not itself reach through resolved calls is upstream of the asked path. It is scored below every other row, keeping its own order; files_first may still list a file's best row early. The header says how many moved (`[path from: N callers of the from endpoint scored below the path's own rows]`); pack-task prints it as its own comment, so the compact legend keeps it. A caller of A that A reaches at any depth stays (recursion, an eval loop), and so does B itself. A's reach walk stops at 4096 symbols, and then nothing moves (`path_from_capped=1`). B need not name a symbol. Before, a newly bound caller of A (`python -m django`'s `management.execute_from_command_line()`) could push a hop under A past the 40-row head. The MCP `for` and pack-task twins apply the same rule. diff --git a/changelog.d/path-norm.md b/changelog.d/path-norm.md deleted file mode 100644 index 16d2df94c..000000000 --- a/changelog.d/path-norm.md +++ /dev/null @@ -1,5 +0,0 @@ -### Changed — warm `--deps` on a large TypeScript tree uses about a fifth less CPU - -Resolving one relative TS/JS import (`./util`) tried up to 16 file names (`util.ts`, `util.tsx`, …, `util/index.jsx`), and each try built and normalized a new path string. The directory and the specifier are now joined and normalized once per import and each try is a lookup; inside one include-graph build, an import that repeats a (directory, specifier) pair takes the first answer, including a miss. On a single root, the shared join step behind the other languages' path probes also stops allocating per try; Rust, Python and Ruby still build their candidate strings, and a multi-root tree takes the previous path. Measured warm on one machine, median of three interleaved pairs against the previous build: vscode `--deps` user CPU -22 % and instructions retired -23 %. envoy (C/C++) and cockroach (Go) do little of this work: instructions -1 % and 0 %. Peak footprint (MiB) is unchanged in each of those runs. - -Answers are unchanged where it was checked: 9 warm and 3 cold base-versus-new output pairs on those three repos are byte-identical. A new case in `test/tsimportprecisecheck.sh` resolves one specifier from two directories to two files and keeps a miss a miss after another directory's hit. diff --git a/changelog.d/path-route-body-slot-target.md b/changelog.d/path-route-body-slot-target.md deleted file mode 100644 index 0e933b9de..000000000 --- a/changelog.d/path-route-body-slot-target.md +++ /dev/null @@ -1,10 +0,0 @@ -### Fixed — an unreached `--path` whose `to=` is stored in a struct field no longer prints the caller's body - -A `--path` whose target is a function stored in a struct field (`--path=real_main,ToolBrowse` on ninja's tool table, -`--path=Row_display,LinuxProcess_rowWriteField` on htop's class table) answers `reachable="0"` with a `` whose last row -carries `via="slot"`, `slot_callers=` and `slot_fns=`. Its `` used to print the `from=` function's body without -that hedge: a body holding a call through a field, not a call to `to=` (+4.0 KB on ninja, +1.1 KB on htop, against the answer -without it). The row whose only next step is the slot crossing into `to=` is now left out of the route bodies; a slot crossing in -the middle of a route keeps its hedged body, and `next=` still names `--callers=`, which lists the call through the field. -A C++ class that names a template parameter as its base (`template class M : B`) joins no in-tree class's family for -the name-template dispatch either, as it already did not for the member-pointer candidates. diff --git a/changelog.d/path-route-name-stitched.md b/changelog.d/path-route-name-stitched.md deleted file mode 100644 index 74f6e9095..000000000 --- a/changelog.d/path-route-name-stitched.md +++ /dev/null @@ -1,18 +0,0 @@ -### Fixed — a `--path` chain stitched from calls bound by name alone is no longer printed as the plain answer - -`--path` (and MCP `path_between`, and the `` block of a `--for` path subject) printed the shortest chain the search -found, `reachable="1"`, even when several of its hops were `via="name"` calls whose receiver nothing proves. On Django, -`--path=QuerySet.__iter__,SQLCompiler.execute_sql` answered a 12-hop chain with 5 such hops through the prefetch code, and -`--path=Model.save,SQLInsertCompiler.execute_sql` a 5-hop chain through `QuerySet.aggregate`: neither is how the call gets there, -and nothing on the answer said so. One shared verdict now judges the chain a single-pair answer prints: - -- 2 or more by-name hops, and a chain over proven call edges only exists between the same definitions: that proven chain is - printed instead, with `route_hedged="proven-preferred" route_len= route_by_name=` describing the shorter by-name one. A by-name hop, a markdown link, a link to a missing anchor and one arm of a resolver split are not proven edges for this search; the one-implementor edge (`via="impl"`) is, and its row says so. -- 2 or more by-name hops and no proven chain: printed as before, hedged on the root with - `route_hedged="name-stitched" route_by_name="K" route_cands="P"` (K by-name hops, P the product of their same-named candidates). -- 4 or more by-name hops and `route_cands` of 64 or more (and no proven chain): refused as the answer (3 is only hedged: on CPython, `--path=asyncio.run,Handle._run` is a true chain with 3 by-name hops, and a true TypeScript or Go dependency-injection chain through interfaces carries 2 to 16 candidates, so it is hedged and printed). The root says - `reachable="0" hops="0" route_hedged="not-proven" route_len="N"`, no hop rows print, and `next=` follows `from=`'s callees (quoted, so a path with a space pastes into a shell). The - legend says reachable=0 here means "not proven", never "no route". `route_cands` counts in-repo same-named definitions along the route; a by-name call on a builtin or external receiver is not counted, so a low count is no proof. -- 4 or more by-name hops below that count: printed with `route_hedged="unlikely"` on the root (the chain is probably not the real route, said on the answer itself, not only in the legend); `next=` names the step that verifies it. - -A chain with at most one by-name hop prints exactly as before. An answer listing several definition pairs hedges its first pair's chain (`route_hedged="name-stitched"`, or `"unlikely"` from 4 by-name hops), and is never refused. The compact `path` legend now reads "reachable=0 hops=0 when none shown", so it no longer contradicts a refusal. diff --git a/changelog.d/py-mod-call-sites.md b/changelog.d/py-mod-call-sites.md deleted file mode 100644 index a091d6f97..000000000 --- a/changelog.d/py-mod-call-sites.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — a Python call through a from-imported submodule is a call - -`from . import mod; mod.f()`, `from pkg import mod`, `from pkg import mod as m`, `from .sub import mod` and `from .. import mod` now bind `mod.f()` to the module-scope `f` of that submodule, the way `import pkg.mod as mod` already did, so `--callers`, `--impact` and the map count these callers instead of reading zero. Before, such a call was read as a call outside the tree (external=). The binding is used only when nothing else may bind the name: the package's own namespace (a def, assignment, import of another file, star import, module `__getattr__` or `globals()` write), a rebinding in the importing file (including a star import placed after the from-import whose module binds the name, or any star import of a module outside the tree placed after it; a star import placed before counts too when the from-import sits under an `if`, `else`, `try`/`except`, `with` or loop, since that from-import may not run), or a second import of the name to another module each leave the call unproven, and an unproven call is declined and counted (declined_calls=), not picked. A submodule that writes its namespace dynamically, star-imports another module, or defines `f` only under an `if`/`try`, withholds the call too, and so does any second module-scope binding of `f` there (a later `f = g`, `del f`, or `from _speedups import f`, plain or under `try`). When both `p/mod.py` and `p/mod/__init__.py` exist, the package is the module, as in Python. A from-import inside a function binds a local only, so it neither proves nor contests the module-level name. A local or parameter named like the module is never the module. Not read yet: a package that re-exports `f` from another file (declined), and `pkg.mod.f()` written in full. Also not read: an `if TYPE_CHECKING:` guard, so `if TYPE_CHECKING: from p import mod` with `else: import p.mod_rt as mod` binds `mod` to two files and the call is declined and counted (before, the runtime import was picked). Known limits, still proven though Python binds otherwise: a submodule that writes `vars()['f'] = g` or runs `exec('f = g')` at module scope, and a package that assigns `sys.modules[__name__ + '.mod']` to another module. The module form `import pkg.mod as mod` followed by a star import that binds `mod` is read as before (proven). diff --git a/changelog.d/py-rule3-external-veto.md b/changelog.d/py-rule3-external-veto.md deleted file mode 100644 index eac485c43..000000000 --- a/changelog.d/py-rule3-external-veto.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — a Python call to a builtin or an imported third-party name no longer binds to a same-named def in an imported file - -`dict(...)`, `bool(...)`, `type(...)` or `float(...)` with no local, import or same-file def of the name, and a call to a name imported from a stdlib or third-party module (`from rich.padding import Padding`), used to be narrowed by Rule 3 to a namesake in a file the caller imports before the external-name veto could refuse it: on CPython 1,383 call edges and on Textual 25 (14 `type(...)`, 11 rich imports) went to an unrelated def. The veto now decides first, so those calls carry no edge. A file with `from m import *` of an indexed module, a name imported from an indexed module, a same-file or nested def, and a parameter or local of that name all keep their edge. Left as before: member calls with an unknown receiver, calls through a local variable, and a call through a module that exists in the tree under a stdlib name (`os.stat`, `inspect.signature` with an in-tree `os.py` or `inspect.py`). diff --git a/changelog.d/readme-answer-quality.md b/changelog.d/readme-answer-quality.md deleted file mode 100644 index 6f5a7071d..000000000 --- a/changelog.d/readme-answer-quality.md +++ /dev/null @@ -1,3 +0,0 @@ -### Documented — README: an answer-quality-by-release table - -The README now shows an answer-quality-by-release table (in-sample, same questions and grader for every release). diff --git a/changelog.d/reg-reader.md b/changelog.d/reg-reader.md deleted file mode 100644 index 7c53c84c0..000000000 --- a/changelog.d/reg-reader.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — `--for` table rows: a write into a registry with a computed key, and the reads of that registry - -A `shape="table"` site can now be a write into a table with a computed key: `storage[resource+"/log"] = podStorage.Log` in Go, `handlers[name + "_svc"] = handler` in Python, ``table[`/api/${ver}`] = route`` in TypeScript. The row is listed when the hit is the key or the value of that one statement (not the table's own name, and not a later statement on the line) and the enclosing function is named `New*`, `new_*`, `Register*` or `register_*` (an `async_` prefix aside). The name is the evidence, not proof. Such a row carries `reg=` with the table and key up to the `=`, and it is a strong row, so a task with no table or registration word lists it too. A literal key (`storage["pods"] = x`, `handlers[0x10] = f`, `handlers[1_000] = f`) is left to the table rule as before, and so are a comparison (`storage[k] == x`), a line of a multi-line string that looks like a write, and a write in a function with any other name. - -When the answer above the block already names that table, the element also lists `` rows: reads of the table in the row's file and in the files of the function's callers. A read is `t[k]` that is not the left side of an `=`, or `t.get(`, in code: a read inside a comment or a string is not counted. Each read on a line counts. A local bound to the function's call (`s := p.NewRESTStorage()`, `const routes = registerRoutes(...)`) counts as the same table, in the row's own file as in a caller's; the call has to be the value itself, so `s := choose(p.NewRESTStorage())` binds nothing. Reads whose key shares a quoted part of the written key come first, and at most 2 are listed. `readers=` counts all the reads found. It is a floor, with `floor="1"`, when a file could not be read, when callers in more than 16 files were not read, or when no call graph was there to find them. When the `` rows would push the element past the table's byte cap they are left out, and `readers=` still counts every read. A read of a same-named table in a file that does not call the function is not listed. Where the answer does not name the table, there are no `` rows and the element is unchanged. A legend sentence rides only with a subscript row. Gate: `test/forregrowcheck.sh` (the subscript-write and table-reader arms). diff --git a/changelog.d/registrar-2.md b/changelog.d/registrar-2.md deleted file mode 100644 index 2bfcc1848..000000000 --- a/changelog.d/registrar-2.md +++ /dev/null @@ -1,7 +0,0 @@ -### Changed — `--for` reaches registration rows when a task asks where something is defined, built or handled - -The table block that ends a `--for` answer (``) now reads three more kinds of question and site. The words defined, declared, build, builds, built, handled, turned into and get(s) to start it, but like default and handler they print only beside a row: a table row, or now a registration row. `defined` and `declared` right after not or never (`name x is not defined`, `x was not declared`) are an error message, not a question, and start nothing. A registration row is an argument of a call that says it registers: a name with a register verb (`register`, `reg`, but not beside `read`, `write`, `get`, `set`, `peek`, `poke`, `load`, `store` or `dump`: `write_reg` and `get_registry` are lookups), an adding verb (`add`, `on`, `use`, `install`, `bind`, `map`, `connect`, `subscribe`, `mount`, `attach`) before route, command or handler (`add_route`, `addEventHandler`), or, for a decorator, a name with route, command or handler. `handler(x)`, `get_handler`, `logging.FileHandler`, `run_command` and `ParseCommandLine` register nothing and are no row. That covers a Python decorator `@websocket_command({"type": "get_states"})`, `component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")` spread over several lines inside a function, `registry::register_handler(x)`, and `async_reg(hass, handle_get_states)`, where the name is inside a longer one. Such a row carries `reg=` (the call up to its paren) instead of `tbl=`, because it is not a table opener and the callee's name does not prove the call registers anything. Not a call, and so no row: a declaration of the same name (a C++ constructor, a GNU-style `Registry::RegisterHandler(` definition, a JavaScript method `registerHandler(h) {`), a member of a type outside a decorator (a Rust enum's tuple variant, a TypeScript interface member), a C or C++ member-initializer list, and a statement inside a closure body (a JavaScript arrow function, a Go func literal) that sits in the arguments of a registration call. A Rust `$macro! {` inside a `macro_rules!` arm is now a table opener (risingwave's `for_all_params`). An unquoted dotted name that finds no site (`pydantic.ValidationError`) is tried again by its last part when that part is a symbol of the index (a backticked or quoted one is tried again either way); the retry lists only the rows where the last part is a quoted key (`'ValidationError': (...)`, `.method('redirect')`) and its own definition, and `tail=N` counts the sites it leaves out. Two dotted names with one last part read it once. An identifier the index has no symbol for (a type generated from a `.proto` file) is tried too. Both run after the task's other names, so they never use up those names' scans. Rows in generated and JSON files are ranked after source rows of the same nearness. - -Measured on the target questions (the same arguments as the gold-blind Stage 0 rule, unsealed copies): the registration rows that rule found now appear in the answer on risingwave-03 (both natural wordings), risingwave-04, homeassistant-03, homeassistant-15 and pydantic-03, and kubernetes-01 prints no block. Gate `forregrowcheck` covers Rust, Python, TypeScript, Go, C++ and JavaScript fixtures: the declarations, members, closure bodies and callee names above (each with a twin that must stay a row), a definition named like a handler, a closed call, calls not named for a registration, the dotted-tail retry on a common word and on a quoted key, and the error-message cue. - -What it does NOT do: it does not prove a call registers anything (the row quotes the call, nothing more). It does not follow a registration through a constant (`SERVICE_TURN_ON = "turn_on"`) or a Go map index assignment (`storage[resource+"/log"] = …`). It reads at most 4 lines and 512 bytes back for the open paren, so a registration call opened further up is no row, and the table legend says so; a missing `reg=` is no claim. An unquoted dotted name whose last part is no symbol of the index is not retried (a backticked or quoted one is). Parens inside strings and comments are skipped, which is a read of the nearest lines, not a parser. diff --git a/changelog.d/registrar-reg-rows.md b/changelog.d/registrar-reg-rows.md deleted file mode 100644 index ed6ae210d..000000000 --- a/changelog.d/registrar-reg-rows.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — `--for` joins a registry key to what is registered under it (Python signals, TypeScript providers) - -A compact `--for` answer now carries a `` row with up to four `` registrations when the question is about a registry: Django-style `request_finished.connect( close_caches )` / `signals.request_finished.connect( … )` for a module variable `K = Signal()` whose `connect` stores its parameter into a container of `self` (the registrar is checked structurally, never matched by the name `connect`), and Angular/NestJS provider records `{ provide: TOKEN, useClass: X }` (`as=` names the field: `useClass`, `useExisting`, `useFactory`, `useValue`). The key is resolved through the site file's imports (and up to three re-export hops) to one definition, or there is no row. A row prints only when the question names the key or a registered name, or a shown ranked row consumes the key (`signals.request_finished.send( … )`'s function, a constructor with `@Inject( TOKEN )` or its class), and only with two or more non-test registrations; test registrations are counted in `tests=`, not listed. The legend states it is a static fact at the site (the call as written; conditional or dynamic registration may exist; `sender=`/`weak=`/`dispatch_uid=` are not read; not proof a receiver ran), that `n=` is a floor (only exact shapes are listed, so an absent row or site proves nothing; decorator, keyword-argument, `forwardRef`, unresolved-import, function-local-import, bound-method, conditional and non-literal provider registrations are not counted), and defines `consumers_more=`, `reg_more=` (keys past the two-row cap) and `cut=1` (a re-export chain over three hops was not followed). A name a function writes locally (a parameter, a loop variable, an assignment) is never keyed to the module-level definition, `import pkg` keys to the package's own file, a stored `sender=None`-style option is never listed as a receiver, and a provider value is paired only within one object literal. On "which receivers run when request_finished is sent" (Django) the answer now names `close_caches`, `reset_urlconf` and `close_old_connections` with their sites and the send sites (+0.7 KB); on "which event plugin handles (keydown.enter)" (Angular) it names the `DomEventsPlugin` and `KeyEventsPlugin` providers in browser.ts (+0.7 KB). Python files are re-parsed once (parser version bump). `RIPWIRE_NO_REG_ROWS=1` turns it off. Listing is exact-shape only, and an absent row (or an absent site) does not prove nothing is registered: a registration is listed only in a shape the index can read, and a shape it cannot read is left out and counted nowhere. Listed shapes: an aliased import is looked up under the written name; a Python registrar is a method whose one stored parameter is the registered value, whose other required parameters appear only as the subscript key of the store (`self.d[ name ] = handler`), and whose defaulted parameters are never themselves stored; a provider value pairs with the `provide` of the same object literal, and only when that literal is an element of an array (a call argument, a `const` initialiser or a `providers` / `viewProviders` property). Left out, with no row or no site: a function-local import, a lambda parameter / comprehension target / module-level `for`, `with` or `except` target named like the key, a bound method, a decorator or keyword-argument registration, a provider object in a ternary, `||`, `return`, reassignment or `let`, a registrar whose other parameter is validated, logged or stored, and an inline literal or arrow value. Round 4: a Python site counts only when its value resolves to a function (an event class, a label, a constant or a class registry such as `REG.register( PluginA )` is not a receiver, and class-registry rows are no longer listed); a TS provider literal counts only as an element of an array (a call argument, a `const` initialiser or a `providers` / `viewProviders` property) holding plain unique pairs, so a spread, computed, duplicate or accessor entry, `reg.remove( { … } )`, `console.log( { … } )`, a bare `register( { … } )` and a `const X = { provide: … }` object are not counted; a function-local `function K(){}` in a file that imports `K` gives no row. The legend lists what is not counted, what is counted without proof it ran (TS decorator providers, `if ( flag ) register( [ … ] )`) and what stays unproven. - -The registrar facts are no name evidence for the by-name call gate: a Python file holding a registrar method no longer reads as naming a class called `g` (the fact's internal scope letter), so a call such as `sys.path.insert(...)` is not bound by name to a class `g` defined in another file. diff --git a/changelog.d/rename-mine-comments.md b/changelog.d/rename-mine-comments.md deleted file mode 100644 index af1252f43..000000000 --- a/changelog.d/rename-mine-comments.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — `--naming-calibration` no longer mines a reworded comment as a rename - -The rename miner paired a removed and an added line that differ at exactly one identifier. A comment is such a pair whenever one of its words changes (`// the rows are read` → `// the rows is read`), so a grammar fix in prose was credited as a rename `are → is` and joined to any function called `is`, which moved the `naming-short` score of a pull request that touched no name. A pair whose two lines are both comment-only (a `//` or `/* … */` line, a `#` line where `#` opens a comment, or a `*` continuation line inside a block comment) is no longer a rename site. A comment after code on the same line leaves the line code, so a rename there still counts. A file in a language ripwire cannot tell comments from code in (Markdown, an unknown extension) keeps every pair. Measured on this repository's own history, the labelled pairs fall from 215 to 172; the `naming-short` rule goes from 10 fires (3 on the abandoned name, 7 on the chosen one) to 9 (3 and 6), `naming-case` and `naming-predicate` lose their one or two fires, and no rule has enough fires to be judged against its floor either way. diff --git a/changelog.d/rule3-name-proof.md b/changelog.d/rule3-name-proof.md deleted file mode 100644 index da7e30e0a..000000000 --- a/changelog.d/rule3-name-proof.md +++ /dev/null @@ -1,13 +0,0 @@ -### Fixed — a Rust or Python call Rule 3 narrowed no longer reads as proven without a written binding - -Rule 3 keeps the one imported file that defines a call's name. Member calls already took that answer as a hedge; a bare or qualified -call did not, so Rust `Ok(..)`, `assert!(..)` and Python's `conv()` through a local printed as plain edges to an unrelated namesake. -Those languages write what a name is bound to, so the writing is now the proof: a Rust `use` of the name into the target's crate, a glob -of the target's module, a qualifier (read whole from the root it is written with) naming its module or type; a Python import binding -that no local or parameter of the caller or an enclosing function shadows, or a star import whose chain reaches the target's file. -Without it the edge stays, marked `via="name"`, and only Rule 3's own file and the caller's own file are offered: no directory namesakes. -Which item a binding names is not compared with Rule 3's target, so a `use` or `from b import conv` that reaches a different definition -of the name still proves it. A Python call through a local or parameter holds a value, not a definition, so it gets no edge and counts -under `declined=`, also when the file imports the same name. A Rust qualifier that provably leaves the tree, read from its written root -(`oneshot::channel()`, `String::new()`, `std::io::stdout()`), reads as outside it; a path rooted at `crate`, `super`, `self`, a crate of -the tree or a package directory of the tree does not. `test/rule3memberguardcheck.sh`. diff --git a/changelog.d/rust-mod-decl.md b/changelog.d/rust-mod-decl.md deleted file mode 100644 index 9e989b5da..000000000 --- a/changelog.d/rust-mod-decl.md +++ /dev/null @@ -1,14 +0,0 @@ -### Fixed — `--deps` and `--report` stop reading Rust module declarations as dependency cycles - -A parent's `mod x;` and the child's `use crate::…` or `use super::…` drew edges both ways, so every parent and child read as a -cycle and a crate read as one tangle: with true edges injected, nushell's largest cycle held 228 files. The parent-to-child -edge of a `mod x;` pair, with the child at the module-file rule's own location (beside a `mod.rs` or a crate root, or under -`//`), is now containment when the declaration is its only reason: it leaves `--deps`' cycles, -`ccd`/`acd`/`nccd`/`shape` and a row's `transitive=`, and `--report`'s cycle list, and stays in `afferent=`, `instab=`, -`godfiles` and `stabledeps`. A parent that also uses its child (`pub use child::Thing;`, `use self::child::…`, a call -`child::f()`) depends on it, so that edge stays and a cycle through it is still reported. `` and -the `--report` heading count the pairs left out, and `--report` says "acyclic without the containment pairs" rather than a bare -"acyclic". On this build's own edges nushell leaves out 725 pairs and keeps three cycles of 3, 2 and 2 files, each a parent -that re-exports a child which uses the parent's items; zed leaves out 289 and has none. A crate root is `lib.rs`, `main.rs`, or a -file no `mod` declares (a `[lib] path`, a `[[bin]]`, an integration test). Not covered yet: `#[path]` modules and a `mod` inside -an inline `mod { }` (both read as the parent's own `mod x;`), and `--arch`'s `propagation_cost`, which still walks these pairs. diff --git a/changelog.d/rust-roots-a.md b/changelog.d/rust-roots-a.md deleted file mode 100644 index 4c77c9696..000000000 --- a/changelog.d/rust-roots-a.md +++ /dev/null @@ -1,30 +0,0 @@ -### Fixed — Rust file edges: `crate::`, `super::`, `self::` and `mod x;` resolve in the includer's own crate and module - -The Rust import probe anchored every `crate::` path at the first `lib.rs`/`main.rs` of the tree, so in a workspace most -`use crate::…` lines either drew nothing or drew an edge into another crate. It now reads, for each file, the crate it is -compiled in: the nearest `Cargo.toml` above it and that manifest's target (`[lib] path`, `src/main.rs`, `[[bin]] path`, -`build.rs`, and Cargo's `src/bin/`, `tests/`, `examples/`, `benches/` directories). Alongside: - -- `mod b;` in a file `a.rs` (not a `mod.rs` or a crate root) is `a/b.rs` or `a/b/mod.rs`; the includer's own directory is - read only when neither exists (the `#[path = "b.rs"]` shape, whose attribute is not captured). -- `super::` in a plain file `a/x.rs` is module `a`, `super::super::` climbs twice and never above the crate root, and - `self::` is the file's own module; a one-segment `super::x` reaches the parent module's own file. -- `use crate::a::b` where `a/b.rs` exists draws `a/b.rs` even when `a.rs` exists too: the full path as a module file is - read first and the parent module's file only when it misses, each step unique-or-degrade on its own. -- A brace group (`use crate::{a::b, c::{self, D}}`) resolves each member, and an `as` rename resolves the path it renames. -- A directive inside an inline `mod x { }` reads from that module, not from the file's: in `#[cfg(test)] mod tests { use - super::*; }` the first `super` is the file's own module (no edge), a `super` past the inline depth climbs from the file's - module, `self::` is the inline module itself, and `mod y;` is `x/y.rs` below the module directory. - -Measured on three in-sample trees (distinct file pairs from `.rs` includers, base main 292a26dec vs this change): -mdbook 64 → 108 (+44, none removed), axum 139 → 331 (+208; 16 removed: 15 edges into the `axum-core` crate from `axum` -files and one `self::rejection` that names an inline module), nushell 1,870 → 3,001 (+1,136; 5 removed, all into -another crate). The call narrow (the import evidence Rust call resolution reads) takes the same edges except brace-group -members, which it still leaves unresolved: there it adds 30 / 64 / 405 pairs and removes the same 0 / 16 / 5. Read as a -member call's namesakes by name, a group's edges turned two declined calls into name-only edges (card RULE3-MEMBER-GUARD). -The Rust crate-root table is built once per run and shared by the file graph, the call narrow and the containment cut -(each used to re-read every `Cargo.toml`). - -What it does NOT do: a workspace crate named by its package (`use nu_protocol::…`) still draws no file edge (card -RUST-ROOTS-B), a one-segment `crate::Item` does not reach the crate root file, and `#[path]` values other than the -module's own name are not read. diff --git a/changelog.d/self-hook.md b/changelog.d/self-hook.md deleted file mode 100644 index 0cc5f1acd..000000000 --- a/changelog.d/self-hook.md +++ /dev/null @@ -1,8 +0,0 @@ -### Added — `--for` names where a base class reaches a served Python override through `self` - -A served Python method whose name has two or more definitions in its inheritance cone now carries a `` element: the override -relation (`overrides=` / `op=` when the inheritance edges prove which base definition wins, `candidates=` when two unrelated bases define it, or `hook="1"` when only subclasses define it) and the `self.x()` / `cls.x()` sites in its base classes, -with a `hasattr` / `getattr` guard said `guarded="1"` and a reference without a call said `form="ref"`. `--for="what calls -SessionMiddleware.process_request"` now names `MiddlewareMixin.__call__` and `__acall__`, which `--callers` cannot bind. The list is -hedged (a call through `self` reaches this override only for instances of its class) and adds no edge, no count and nothing to `--impact`. -Every site is read from the parse: a docstring or a string is no site, and a rebound `self`, a nested def, a lambda or a static method is no site and is counted in `skipped=`; a method whose class or an ancestor has a by-name base beside a proven one is not listed (the by-name base may come first in the resolution order); a base file that cannot be read again is said in `unread=` on the row, not counted as `skipped=`; `next=` runs a verb that lists the rest; under `--token-budget` only `` is printed. Other answers are byte-identical. `RIPWIRE_NO_SELF_HOOK` turns it off. diff --git a/changelog.d/short-key-sentence.md b/changelog.d/short-key-sentence.md deleted file mode 100644 index 4b3c0fe7b..000000000 --- a/changelog.d/short-key-sentence.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — `--for` gives an unresolved code-shaped name inside a sentence the same fallback ladder a bare name gets - -- `--for`: a task the route reads as words (`definition of ChunkPtr, the lookup is empty`, `where does Wrong.formatDate come from`) now prints the `` fallback ladder when it holds exactly one code-shaped name and no definition of that name resolved. Code-shaped means a qualified spelling (`Q.leaf`, `Q::leaf`, every segment starting like an identifier, a leaf that is no file or host suffix) or a camelCase / snake_case word; a plain capitalised word (`Widget`, `Python`) is not one, and a bare word that is a segment of the qualified spelling beside it counts as that name. Unresolved means a bare name that no symbol in the index carries, or a qualified name whose fully proven definition the answer did not print; a qualified spelling that names a file gets no ladder: an indexed file's basename, its module path (`http.server` for `Lib/http/server.py`) or a module path down to a dotted basename (`app.module` for `src/app/app.module.ts`), or a file at the root the index does not hold (`go.mod`, `build.ninja`). A span that ends in a file suffix (`CMakeLists.txt`, `src/net/HttpClient.cpp`) is a file name, so none of its words is a name. The block carries `from="sentence"`; its rungs (`leaf`, `substring`, `literal`), the qualifier proofs, `exact=`, `next=`, the row caps, the held marker under `--token-budget` and `RIPWIRE_NO_SHORT_KEY` are those of the bare-name ladder. A task with two or more code-shaped names, a name that resolves, or no code-shaped name prints no block. An empty rung means the index holds no such definition; a file the index skips can still define the name, and `next=` searches the tree. Gate: `test/barrelshortkeycheck.sh` arms D1-D14. diff --git a/changelog.d/string-import.md b/changelog.d/string-import.md deleted file mode 100644 index 1595b670e..000000000 --- a/changelog.d/string-import.md +++ /dev/null @@ -1,11 +0,0 @@ -### Added — a dotted string that names a shown Python definition - -`--for` and `--callers` now print `` when a Python string literal whose whole value is a dotted path resolves to a row -the answer shows: `_builtin_context_processors = ("django.template.context_processors.csrf",)` beside `csrf`, with the function in which -the constant's name reaches an import sink's argument through that function's own assignments. A `%`-template handed to an import sink (`import_module("%s.base" % name)`) gets a hedged -row when the directories of three or more shown files fill it. The string is matched through its whole path, never a bare name; whether it -reaches the sink is not traced, so nothing enters a count, `reaches=` or `--impact`. `RIPWIRE_NO_DOTTED_STRINGS=1` turns it off. - -The compact legend entry for a tests-only `` no longer contains a double hyphen, which made the whole `--callers` answer ill-formed XML (an XML comment cannot hold it) when the only site of a string was in a test path. - -A sink call whose flow tokens are over the size cap, or that names nothing, joined no constant and was dropped without a trace, so `loaders=` could read as a complete count. The row of a constant such a call could name (same file, or a file importing the name from the constant's file) now says `loaders_floor="1"`, defined in both legends. diff --git a/changelog.d/subject-anchor.md b/changelog.d/subject-anchor.md deleted file mode 100644 index df4089c6a..000000000 --- a/changelog.d/subject-anchor.md +++ /dev/null @@ -1,4 +0,0 @@ -### Fixed — `--for` anchors the symbol a subject names, and says when no source defines it - -- `--for`: a descriptive subject that names ONE function can anchor it. Where the answer on that function alone prints its body and its callers (one function in the whole index, a body that fits the single-anchor allowance, at least one caller), the route trades its ranked map for the function, and `route=` lists the plain words beside it under `dropped:` (`transformNodes new parameter`, `how does X work`). Every other call keeps the route and answer it had, the low-confidence note below aside: a subject that names several symbols, a type, or a function that fails that test, and a call that prints no body or no callers (`--json`, `--detail`, `--signatures-only`, `--token-budget`, `--format=candidates`, the `--limit`/`--offset` file page, an explicit `--pack-budget-bytes`). A conceptual route whose descriptive subject names a function carries `` on top. The MCP `for` verb, which serves signatures only, keeps its map. -- `--for`: `confidence="high"` on a name-exact route needs a source definition behind the anchor. An anchor only a test, doc or data file defines reads low, and `route=` says why. A plain word never vouches for a subject that spells a name: `max_concurrent_queries read`, whose key lives in a data file, reads low although `read` is defined in source. diff --git a/changelog.d/subject-callers-natural.md b/changelog.d/subject-callers-natural.md deleted file mode 100644 index 38f159ba7..000000000 --- a/changelog.d/subject-callers-natural.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed — `--for` prints the callers of the one function a sentence asks about - -- `--for`: a sentence that names one function and asks what depends on it now gets that function's `` block, as the short subject already did. Examples: "I want to add a new parameter to transformNodes; what would that affect?", "who calls async_prepare_setup_platform and would break if its signature changed?", "I need to change how decompress_data works; what depends on it?". The plain-word limit now leaves out the task frame: question leads, pronouns, helpers ("I need to"), conjunctions and prepositions. The sentence also needs a dependents cue (change, add, affected, breaks, depends, calls, used, update, modify, invokes, references, rename, exercise), read in its dependents sense, and exactly one named function. A sentence without one reads as before, for example "how does X work", "where is X defined" or "how do A and B relate". So does a cue that asks what the function itself does ("what does X depend on", "what does X modify"), a noun or purpose ("API calls", "used to"), a negated cue ("I changed nothing in X"), and a sentence naming two functions ("when X changes, who calls Y?"). This change does not alter the route: only a short descriptive subject can take the name-exact route, through the existing flip checks, and the rest of the answer is byte-identical. Gate: `test/subjectanchorcheck.sh` arms SC. - -Answers, against main with the same arguments: of the 60 practice questions, the stored wording is unchanged (26 Complete, 118 items); the first natural wording goes from 25 to 27 Complete and 114 to 118 items, the second from 20 to 22 Complete and 105 to 108 items, and no question loses a tier or an item. Every question that moved gains the new block. Of 321 in-sample calls, 316 print the same answer as main (timestamps aside) and 5 that already printed the block gain its legend clause "(several: the first with callers)". diff --git a/changelog.d/table-const-v2.md b/changelog.d/table-const-v2.md deleted file mode 100644 index 6afae7a69..000000000 --- a/changelog.d/table-const-v2.md +++ /dev/null @@ -1,5 +0,0 @@ -### Added — a `--for` task about a table or a registration reaches the sites of the identifier it names - -When no other literal of the task has a source site, `` makes a last try, `shape="table"`. It needs one of these in the task's own words, outside backticks: a registration word (`register`, `registered`, `registry`, `registrar` and their forms), `table`, `compile-time` or `compile time`, `add a new` or `implement another`; or `default` or `handler`, which print only when a site reads as a table row. Its anchors are the task's backticked spans, then identifier-shaped words, then a plain word after `like`, `alongside` or `following`: any word that is not backticked must be the name of a symbol in the index. A backticked span or a word after `like` is the thing to read and prints its sites; a bare identifier-shaped word prints only beside a real table row, so `add a new test for run_it` or `fix register allocation in run_it` print nothing, and so does a backticked span right after `to` in an add task (``add a new argument to `run_it` ``, the thing changed). A hit stands on word edges; a hit inside a longer word is counted in `longer=`. - -Each anchor with a source site prints its own ``, the legend on the first only. A site that reads as a table row ranks first, wherever it stands: a map or dict key, a list member, a `{ name, ...` row or a call beside a sibling of the same shape at its indent, under an opener the allowlist knows, or a macro or ALLCAPS call outside any function (a Go map entry, a Rust macro row, a C X-macro line). The allowlist's openers are data: an assignment, a `return` or a key whose value is a bracket (a `(` only in Python, where it is a tuple; in a TS or JS function body a bare `{` is an object literal, not a table), a typed composite (`[]T{`, `map[K]V{`, `new T[] {`), a macro's bracket or a named wrapper (`Object.freeze({`, `new Map([`, `frozenset({`, `Map.of(`); `export default {`; an enum's members; a `#define`; a struct literal outside a function. Anything else makes no claim: a call's arguments, an arrow function's parameters (`export const f = (`), an options object a function builds (`const opts = {`), a destructuring import (`const {`), a callback, a statement, a `struct`, `interface` or `type` declaration, a Java `static {` block. Such a row carries `tbl=`, the line that opens the table (`item` for a top level call), and the first four carry `lines=a-b`, the lines after them. This is a reading of the line's text, not of the parse; a row with no `tbl=` is no claim either way. Inside a function body a call is a statement and never a row. Past 4,096 source sites the rest are not read for a table row: `read=` says so, and an anchor that needs a row and has none in those read is counted by ``. Rows stop at 32 per anchor and all the elements of a task at 40,000 bytes, cut on a row boundary with `capped="1"` and `next=`. At most 3 anchors print and 4 are scanned (an anchor with no site costs a scan); every anchor past a cap, or left without room, is counted by ``. Everything before the element stays byte-identical. Gate: `test/forliteralcheck.sh`. diff --git a/changelog.d/unused-in-file.md b/changelog.d/unused-in-file.md deleted file mode 100644 index 466caf104..000000000 --- a/changelog.d/unused-in-file.md +++ /dev/null @@ -1,3 +0,0 @@ -### Added — `--for` answers "which public X in are unused" - -A `--for` subject with an unused cue (`unused functions in util/network.py`, `quoteString.h unused functions`, `which exports of src/path.ts are never called`) that names a file or directory now carries an `` table: one row per public def, with a verdict that says what was proven. `used` means a non-test caller elsewhere (or an in-file chain from one); `test-only` that every reference found is in a test; `file-only` that only the same file refers to it; `unused` that the name appears nowhere in the tree outside its definition, declarations and comments. Anything an index cannot follow reads `unknown`, with `why=` naming it: a word no edge explains, a string lookup, a built name, a re-export, a star barrel, dynamic dispatch, a decorator or attribute, a surface consumers outside the tree may use, a build file. `--dead-code` keeps its internal-linkage meaning; a `--dead-code=PATH` root now names the table on `next=`. diff --git a/changelog.d/versions.md b/changelog.d/versions.md deleted file mode 100644 index d896342d6..000000000 --- a/changelog.d/versions.md +++ /dev/null @@ -1,14 +0,0 @@ -### Changed — the versions this change moves, stated once - -`kParserVer` 259 → 283 (file tag 284). 283 (train 43b): Python registrar facts and TS/JS provider-record literal marks (their branch -carried 273–281), Python dotted-string and import-sink facts (their branch carried 274–275). 271 (file tag 272): the C++ member-pointer slot records (a `&Cls::m` value reference, a -`( o.*x.f )( … )` slot call, a C nested designated path or macro-cast slot call, a class body fact's scope path, and a C++ -base that is a template parameter of the deriving class; 269 on main), the interface-receiver peel's records (a C++ -smart-pointer alias carries its pointee, a member whose written type is a template parameter is marked, a Rust `&dyn T` / -`Box` / `impl T` result is recorded) and the TS/JS re-export specifier records each carried lower numbers on their -branches (257–262), so a cache written before them would replay the older rows. 271 sits above every live branch and -pull-request number (the string/literal records carry 263, 265 and 267 on theirs). `kCacheVersion` 46 → 48: a re-export -specifier is a new binding kind appended to the cache record (its branch carried 47). A cache written by 0.6.9 is re-parsed -once. The quality-snapshot scheme moves 24 → 25 (#472: `isTestPath` reads more shell test names, which moves the dead set's test-script exemption; no snapshot record changes shape, and a v24 blob is rebuilt once). The session legend dictionary is `dictv=c43b9c72ce2a139a entries=1050`. - -The history-walk cache's `kQChurnCacheScheme` moves 3 → 4 (#433: a blob written by a pre-fix binary holds the old line-frame reader's stream, which for the repositories the framing fix targets is the corrupted one, and no key component can tell that blob from a fresh read; every repository's history is walked once more after the upgrade, and on trees with ordinary names the walk produces the same stream, so the cost is one cold walk and no changed answer). diff --git a/changelog.d/warm-direct.md b/changelog.d/warm-direct.md deleted file mode 100644 index 16b1992ed..000000000 --- a/changelog.d/warm-direct.md +++ /dev/null @@ -1,3 +0,0 @@ -### Changed — a warm run no longer holds extra copies of the cached facts - -A warm run used to decode every cache record into one path-keyed map before the parse workers started, move each file's facts into a worker's arrays and move them again in the merge, so the parse-pool join held up to three sets of fact arrays. On envoy that put the warm `--deps` peak memory footprint 45 % over the cold run that wrote the cache. Now the cache load reads only each record's head (its key, hash and family counts), and the parse worker that takes a hit reads that one record, checks its checksum and key, and decodes it straight into its own arrays. The merge moves one fact family at a time and frees each worker's array as soon as its rows have moved. On envoy the warm peak drops 37 % and sits 3 % under the cold one; the answers are byte-identical to a cold run and to `--no-cache`. A record that fails its read, checksum, decode or key check in the worker is parsed from source after the pool joins, so a torn or colliding record still costs only time. The cache format moves to 49 (the record head gains the eight family counts), so an older cache is rebuilt once. New gate `test/warmpeakcheck.sh` pins the warm peak against the cold one. diff --git a/changelog.d/warm-rewrite.md b/changelog.d/warm-rewrite.md deleted file mode 100644 index a11611213..000000000 --- a/changelog.d/warm-rewrite.md +++ /dev/null @@ -1,5 +0,0 @@ -### Fixed — a refused file no longer rewrites the whole cache on every warm run - -A file the nesting guard refuses (a JSON, YAML, Markdown or Kotlin file nested past its ceiling) is stored in the cache as unknown on purpose, so every warm run re-reads it, re-refuses it and lists it again under `--skipped`. That re-read also marked the cache changed, so the whole cache was serialized and written again on every warm run of a tree holding one such file; on envoy that was a 146 MB write per run. A cache miss that ends before the parse (unreadable, binary or refused) now marks the cache changed only when the cache's record for that file differs from the one this run would write. A file that becomes refused, or stops being refused, still updates the cache. The refusal note and the `--skipped` row are unchanged, and the cache format is unchanged. `RIPWIRE_CACHE_STATS`' `reparsed=` no longer counts an unchanged refused file. New arms (w1)-(w5) in `test/savecachecheck.sh` pin both directions. - -Not covered yet: a file whose extraction was partial is still stored as unknown with its facts, so a warm run re-parses it and rewrites the cache as before. None of the seven large repositories measured has one. diff --git a/changelog.d/xf-alias-hop.md b/changelog.d/xf-alias-hop.md deleted file mode 100644 index f66dae94f..000000000 --- a/changelog.d/xf-alias-hop.md +++ /dev/null @@ -1,7 +0,0 @@ -### Added — an alias leads to what it names: `--for` follows the chain, `--uses` lists the sites written through it - -`--for=ColumnPtr` printed `using ColumnPtr = COW::Ptr` and stopped there: what the alias stands for (`COW::Ptr`, then `using Ptr = immutable_ptr`, then the class `immutable_ptr`) and the class it points at (`IColumn`) each took another call. A `--for` answer now ends with an `` block when the name the task spells (the route's name, or a sentence's one code-shaped name) has an alias definition: one `` per distinct walk, one `` row per definition reached with its declaration text and `d=` hops, the template argument of the first target as `arg="1"` (the pointee of a smart-pointer alias) with its member count and first names, and a `` for a spelling no definition in the tree binds (`std::shared_ptr`, a template parameter's member). The walk is bounded (4 hops, 3 definitions per spelling, 3 chains, `cycle="1"` / `depth_capped="1"` / `capped="1"` when they cut), sits outside `est_tokens`, and under a token budget prints a one-line held marker. Under `--max-tokens` with `--detail=N` (the one case `--for` honors that ceiling; a bare `--max-tokens` is noticed as ignored and the block prints whole) it leaves out the member names (`m=`) and the chains that reach only leaves, and says so with `shed=`. Its legend explains only the attributes the block prints. A written qualifier is checked against the namespaces and classes around each definition, read from the file's braces (a C++ class's own scope is just its name): `using WQ = N2::Widget` binds the class in `N2` and never a namesake in `N1`. A leading `::` (`using W = ::Widget`) binds the global definition, and a space beside `::` (`N :: Widget`) is still one name. When a bind cannot be proven (the braces do not balance, at the target or at the alias itself), the row says `bound="name"` instead of claiming the target is not in the tree. A string literal in the target (`Tok<"…">`) is no name, and the declaration text, `to=` and leaves are redacted like every other printed source line (`--no-redact` passes them through). `--defs to=` is redacted the same way. A type's `members=` counts its own member functions only. The block is printed by the CLI in XML only: the MCP `for` tool, `--json` and `--format=columnar` carry none (`--help` says so). `RIPWIRE_NO_ALIAS_HOP` turns it off. - -`--uses=IColumn` now also lists the sites written as an alias whose chain reaches `IColumn` (`ColumnPtr`, an alias of that alias, `std::shared_ptr`), each marked `via="alias" alias="ColumnPtr"`, after the direct rows. A site counts in `count=` and `via_alias=` only when the alias in scope there (the innermost one of that name visible from the site) is one that reaches `IColumn`; other sites written with the same name are listed last with `hedge="name"`, counted in `alias_hedged=` and not in `count=`. `alias_namesakes=` says how many other definitions share an alias's name, and `alias_depth_capped="1"` says the reverse walk stopped at 4 hops with an alias still to follow (one hop per round, whatever the declaration order). An alias declared later in the same file is not the one in scope at an earlier site, and a site whose braces cannot be read is hedged when the alias name has namesakes. A `file:Name` or `Scope::Name` selector lists the alias sites of the definitions it chose. The columnar form counts them as `alias_sites_held=` instead of listing them. A definition inside another translation unit's source file (an example program's own `class IColumn`) is not what a header alias names while another definition of the name remains. - -C++ only for now (headers included): its alias definitions are the ones that record a target. A TypeScript or Rust `type X = Y`, a Rust `use … as`, and a Python module alias get no block yet. The MCP and LSP `uses` answers list no alias sites. Gate: `test/xfaliashopcheck.sh`. diff --git a/changelog.d/xf-qual-endpoints.md b/changelog.d/xf-qual-endpoints.md deleted file mode 100644 index 47457fe5a..000000000 --- a/changelog.d/xf-qual-endpoints.md +++ /dev/null @@ -1,3 +0,0 @@ -### Fixed — a qualified `--connect` terminal and `--edit-check` selector answer about the definition they name - -`--connect=ProcessList::insert,InterpreterFactory::get` already searched only the two qualified definitions, but each terminal row printed the bare name's definition count (`defs="196"`, `defs="645"`) under a legend saying all of them were searched, which read as the qualifier being dropped. A terminal spelled with an owner, module or file (`A::m`, `Class.method`, `ns.fn`, `pkg.mod.fn`, `file:name`) now reports the definitions its spelling searched (omitted when one). On TS/JS, which store no owner scope, `--edit-check=Alpha.run` (and the MCP `edit_check`) unioned a same-file `Beta.run`, and `ns.make` a sibling namespace's `make`, so the contract listed the other definition's callers; a selector that names an owner now keeps only the definitions it resolves to. The edit preview (`--edit-check=SYM --edit-payload=FILE --dry-run`, MCP `edit_check` with `new_body`) narrows the same way, so it agrees with the answer it predicts. The connect legend now says `defs=` counts the definitions the terminal's spelling matched, and when two spellings pick one definition (`--connect=useAlpha,run,Alpha.run`) their shared row prints the wider count. Bare names get the same answers; only the connect legend text changes. New gate `test/qualendpointcheck.sh` pins every endpoint verb's resolution of these spellings in C++, Python and TS, and the refusal of an unknown qualifier. diff --git a/docs/EVALS.md b/docs/EVALS.md index 4c0f79076..361507336 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` | 773 gate scripts (counts snapshot 2026-11-07) plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 776 gate scripts (counts snapshot 2026-11-07) 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) @@ -5888,7 +5888,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 -(**773 gate scripts** at the counts snapshot 2026-11-07; `test/manifestcheck.sh` proves every listed name exists on disk). +(**776 gate scripts** at the counts snapshot 2026-11-07; `test/manifestcheck.sh` proves every listed name exists 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`. @@ -7008,7 +7008,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 773 at the counts snapshot 2026-11-07. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 776 at the counts snapshot 2026-11-07. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere, and since 2026-11-02 it is the dated counts snapshot `docs/counts.tsv` (so two open changes that each add a gate never conflict on it): `docs/gatecount_build.py` rewrites diff --git a/docs/LIMITS.md b/docs/LIMITS.md index d668d9a9e..36c6ffe24 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -35,10 +35,10 @@ cap do not both rewrite the same line; the per-file tables are re-read from `src | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 396 | 136 | 261 | **135** | +| 403 | 137 | 267 | **136** | -Plus 8 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 396 + 8 is the 404 constants this generator parses out of `src/`. +Plus 9 ranking and apportionment parameters, in their own table below: they are not caps, they +are not counted as caps, and 403 + 9 is the 412 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -56,8 +56,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **142 of 396 caps are classified -(50 INDEXING, 46 OUTPUT, 46 BOUNDARY); the remaining 254 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **142 of 403 caps are classified +(50 INDEXING, 46 OUTPUT, 46 BOUNDARY); the remaining 261 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that diff --git a/docs/TUNING.md b/docs/TUNING.md index efdbaa532..a501cc5d0 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -19,25 +19,25 @@ both rewrite them; the per-cap tables are re-read from `src/` on every run. | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 285 | 284 | 112 | 12 | **37** | 75 | +| 291 | 290 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **285 cap -declarations** under **284 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **291 cap +declarations** under **290 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 284 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **161 declared since the sweep was +every run, which makes them a third population again. Enumerated over the 290 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **167 declared since the sweep was prepared, which no measurement has touched** (`kAliasHopDepth`, `kAliasPeelDepth`, -`kAliasTargetCap`, `kBackHopWidth`, `kBaseScopeScanCap`, `kBridgeMaxHops`, +155 more). 111 + 12 + -161 = 284, and `emit` refuses to render a partition that does not add up. The `tunable` column above +`kAliasTargetCap`, `kBackHopWidth`, `kBaseScopeScanCap`, `kBridgeMaxHops`, +161 more). 111 + 12 + +167 = 290, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer declared in `src/` at -all: `kSituTestRowsShown`. Quoting "113 of 285" would be wrong in both halves at once, which is the +all: `kSituTestRowsShown`. Quoting "113 of 291" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 285-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 291-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/docs/counts.tsv b/docs/counts.tsv index 1c856f102..824ec3ea4 100644 --- a/docs/counts.tsv +++ b/docs/counts.tsv @@ -3,22 +3,22 @@ # re-takes it from the tree; `check` compares it with the tree. A feature PR leaves it alone, which is what keeps # two open PRs that each add a gate or a cap from conflicting on a total. taken 2026-11-07 -gates.scripts 773 +gates.scripts 776 limits.boundary 46 -limits.caps 396 -limits.constants 404 -limits.disclosing 261 -limits.files 136 +limits.caps 403 +limits.constants 412 +limits.disclosing 267 +limits.files 137 limits.indexing 50 limits.output 46 -limits.parameters 8 -limits.silent 135 +limits.parameters 9 +limits.silent 136 tuning.constexpr_live 12 -tuning.declarations 285 +tuning.declarations 291 tuning.duplicated `kRowCap` tuning.gone 1 tuning.gone_shown `kSituTestRowsShown` -tuning.names 284 +tuning.names 290 tuning.tunable_live 111 -tuning.unswept 161 -tuning.unswept_shown `kAliasHopDepth`, `kAliasPeelDepth`, `kAliasTargetCap`, `kBackHopWidth`, `kBaseScopeScanCap`, `kBridgeMaxHops`, +155 more +tuning.unswept 167 +tuning.unswept_shown `kAliasHopDepth`, `kAliasPeelDepth`, `kAliasTargetCap`, `kBackHopWidth`, `kBaseScopeScanCap`, `kBridgeMaxHops`, +161 more diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 33303aca3..6576de8e0 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["773 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is a dated snapshot of the runner's own loop (counts snapshot 2026-11-07)"], // gatecount + ["776 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is a dated snapshot of the runner's own loop (counts snapshot 2026-11-07)"], // 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."], @@ -1238,7 +1238,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, "773", "gate scripts named by test/regression.sh — and the COUNT is the counts snapshot 2026-11-07 of that loop, checked exact at every refresh and release", // gatecount + stat(s, "776", "gate scripts named by test/regression.sh — and the COUNT is the counts snapshot 2026-11-07 of that loop, checked exact at every refresh and release", // 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", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["193 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["773 gate scripts (counts snapshot 2026-11-07)", "bash test/manifestcheck.sh"], // gatecount + ["776 gate scripts (counts snapshot 2026-11-07)", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 76 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/"],