From c758e716b07e830d435130a4fe65667eb7b9f19d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 05:32:51 -0400 Subject: [PATCH 001/229] =?UTF-8?q?test:=20receiverevidencecheck=20?= =?UTF-8?q?=E2=80=94=20a=20call=20bound=20by=20name=20alone=20never=20show?= =?UTF-8?q?s=20as=20a=20proven=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new gate with one fixture root per language (JS, TS, Python, Go, and the implicit-receiver languages Java, Kotlin, C#, C++, Swift, Ruby). Each root holds paraphrased repros of graded false rows where a member call (or a bare call under an implicit receiver) was bound to a same-named in-repo definition with nothing proving the receiver's class: a request context's onerror drawn to the caller file's Application.onerror, a typed Go struct field drawn to Merger.Get, WeakMap/Promise/stream/dict/list receivers in a file that names the class, a `done` parameter drawn to another file's closure, `read = os.read` / `feed = parser.feed` aliases, and a bare flush()/render() drawn to an unrelated class. The contract: such a call either resolves with receiver evidence (self/this and the class cone, a construction in scope, a constructor-assigned field, a type annotation or declaration, an import) or keeps its row with an on-row hedge, via="name" (MCP: "via":"name"), identical on --callees, --callers, --impact, --path, --connect, --expand , --for and MCP. A true target is never dropped. Near-miss arms pin the evidence edges a new rule could kill: this/self calls, constructor-assigned fields, typed parameters, import aliases, same-class and superclass calls in Java/Kotlin/C#/C++/Swift, Ruby implicit self, included modules and top-level defs. The positive arms are red on the integration/train-25 binary; the fix follows. Registered in regression.sh and the shard weights; gate count 664 -> 665. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/receiverevidencecheck.sh | 548 ++++++++++++++++++ test/receiverevidencefix/README.md | 34 ++ test/receiverevidencefix/cpp/src/exporter.cpp | 5 + test/receiverevidencefix/cpp/src/exporter.hpp | 8 + test/receiverevidencefix/cpp/src/logger.cpp | 10 + test/receiverevidencefix/cpp/src/logger.hpp | 17 + test/receiverevidencefix/cpp/src/plain.cpp | 4 + test/receiverevidencefix/cpp/src/plain.hpp | 8 + test/receiverevidencefix/cs/App/Exporter.cs | 8 + test/receiverevidencefix/cs/App/Logger.cs | 24 + test/receiverevidencefix/go/go.mod | 6 + test/receiverevidencefix/go/src/embed.go | 20 + test/receiverevidencefix/go/src/item.go | 7 + test/receiverevidencefix/go/src/merger.go | 19 + test/receiverevidencefix/go/src/result.go | 21 + test/receiverevidencefix/go/src/tui/tcell.go | 37 ++ test/receiverevidencefix/go/src/util/chars.go | 18 + .../java/src/main/java/app/Buffer.java | 14 + .../java/src/main/java/app/Exporter.java | 12 + .../java/src/main/java/app/FileBuffer.java | 8 + .../java/src/main/java/app/Logger.java | 20 + .../receiverevidencefix/js/lib/application.js | 43 ++ test/receiverevidencefix/js/lib/context.js | 6 + test/receiverevidencefix/js/lib/handle.js | 15 + test/receiverevidencefix/js/lib/parser.js | 8 + test/receiverevidencefix/js/lib/reply.js | 43 ++ test/receiverevidencefix/js/lib/request.js | 26 + test/receiverevidencefix/js/lib/response.js | 8 + test/receiverevidencefix/js/lib/routes.js | 19 + test/receiverevidencefix/js/lib/schemas.js | 11 + test/receiverevidencefix/js/lib/search.js | 8 + test/receiverevidencefix/js/lib/trailer.js | 9 + test/receiverevidencefix/js/lib/uses.js | 19 + test/receiverevidencefix/js/package.json | 1 + .../js/test/server.test.js | 7 + .../kt/src/app/Exporter.kt | 9 + test/receiverevidencefix/kt/src/app/Logger.kt | 23 + test/receiverevidencefix/kt/src/app/Plain.kt | 8 + .../py/src/tui/__init__.py | 0 test/receiverevidencefix/py/src/tui/app.py | 25 + .../py/src/tui/byte_stream.py | 3 + .../py/src/tui/callback.py | 15 + .../py/src/tui/compositor.py | 10 + .../receiverevidencefix/py/src/tui/content.py | 12 + .../py/src/tui/css/__init__.py | 0 .../py/src/tui/css/parse.py | 3 + .../py/src/tui/css/styles.py | 20 + .../py/src/tui/css/stylesheet.py | 44 ++ .../py/src/tui/drivers/__init__.py | 0 .../py/src/tui/drivers/linux.py | 20 + .../py/src/tui/geometry.py | 7 + test/receiverevidencefix/py/src/tui/markup.py | 8 + .../py/src/tui/message_pump.py | 3 + .../py/src/tui/reactive.py | 15 + test/receiverevidencefix/py/src/tui/screen.py | 11 + test/receiverevidencefix/py/src/tui/uses.py | 24 + test/receiverevidencefix/py/src/tui/worker.py | 3 + .../py/src/tui/xterm_parser.py | 8 + .../py/src/web/__init__.py | 0 .../py/src/web/datastructures.py | 3 + test/receiverevidencefix/py/src/web/errors.py | 9 + .../py/src/web/formparsers.py | 8 + .../py/src/web/requests.py | 10 + .../rb/lib/app/exporter.rb | 11 + .../receiverevidencefix/rb/lib/app/helpers.rb | 7 + test/receiverevidencefix/rb/lib/app/logger.rb | 30 + test/receiverevidencefix/rb/lib/app/notes.rb | 4 + test/receiverevidencefix/rb/lib/app/plain.rb | 11 + .../swift/Sources/App/Exporter.swift | 9 + .../swift/Sources/App/Logger.swift | 25 + .../swift/Sources/App/Plain.swift | 8 + test/receiverevidencefix/ts/package.json | 1 + test/receiverevidencefix/ts/src/cache.ts | 11 + test/receiverevidencefix/ts/src/context.ts | 16 + .../ts/src/middleware/alias.ts | 14 + .../ts/src/middleware/auth.ts | 12 + .../ts/src/router/router.ts | 4 + .../ts/src/router/smart.ts | 25 + .../receiverevidencefix/ts/src/router/trie.ts | 13 + test/regression.sh | 2 +- 84 files changed, 1583 insertions(+), 9 deletions(-) create mode 100755 test/receiverevidencecheck.sh create mode 100644 test/receiverevidencefix/README.md create mode 100644 test/receiverevidencefix/cpp/src/exporter.cpp create mode 100644 test/receiverevidencefix/cpp/src/exporter.hpp create mode 100644 test/receiverevidencefix/cpp/src/logger.cpp create mode 100644 test/receiverevidencefix/cpp/src/logger.hpp create mode 100644 test/receiverevidencefix/cpp/src/plain.cpp create mode 100644 test/receiverevidencefix/cpp/src/plain.hpp create mode 100644 test/receiverevidencefix/cs/App/Exporter.cs create mode 100644 test/receiverevidencefix/cs/App/Logger.cs create mode 100644 test/receiverevidencefix/go/go.mod create mode 100644 test/receiverevidencefix/go/src/embed.go create mode 100644 test/receiverevidencefix/go/src/item.go create mode 100644 test/receiverevidencefix/go/src/merger.go create mode 100644 test/receiverevidencefix/go/src/result.go create mode 100644 test/receiverevidencefix/go/src/tui/tcell.go create mode 100644 test/receiverevidencefix/go/src/util/chars.go create mode 100644 test/receiverevidencefix/java/src/main/java/app/Buffer.java create mode 100644 test/receiverevidencefix/java/src/main/java/app/Exporter.java create mode 100644 test/receiverevidencefix/java/src/main/java/app/FileBuffer.java create mode 100644 test/receiverevidencefix/java/src/main/java/app/Logger.java create mode 100644 test/receiverevidencefix/js/lib/application.js create mode 100644 test/receiverevidencefix/js/lib/context.js create mode 100644 test/receiverevidencefix/js/lib/handle.js create mode 100644 test/receiverevidencefix/js/lib/parser.js create mode 100644 test/receiverevidencefix/js/lib/reply.js create mode 100644 test/receiverevidencefix/js/lib/request.js create mode 100644 test/receiverevidencefix/js/lib/response.js create mode 100644 test/receiverevidencefix/js/lib/routes.js create mode 100644 test/receiverevidencefix/js/lib/schemas.js create mode 100644 test/receiverevidencefix/js/lib/search.js create mode 100644 test/receiverevidencefix/js/lib/trailer.js create mode 100644 test/receiverevidencefix/js/lib/uses.js create mode 100644 test/receiverevidencefix/js/package.json create mode 100644 test/receiverevidencefix/js/test/server.test.js create mode 100644 test/receiverevidencefix/kt/src/app/Exporter.kt create mode 100644 test/receiverevidencefix/kt/src/app/Logger.kt create mode 100644 test/receiverevidencefix/kt/src/app/Plain.kt create mode 100644 test/receiverevidencefix/py/src/tui/__init__.py create mode 100644 test/receiverevidencefix/py/src/tui/app.py create mode 100644 test/receiverevidencefix/py/src/tui/byte_stream.py create mode 100644 test/receiverevidencefix/py/src/tui/callback.py create mode 100644 test/receiverevidencefix/py/src/tui/compositor.py create mode 100644 test/receiverevidencefix/py/src/tui/content.py create mode 100644 test/receiverevidencefix/py/src/tui/css/__init__.py create mode 100644 test/receiverevidencefix/py/src/tui/css/parse.py create mode 100644 test/receiverevidencefix/py/src/tui/css/styles.py create mode 100644 test/receiverevidencefix/py/src/tui/css/stylesheet.py create mode 100644 test/receiverevidencefix/py/src/tui/drivers/__init__.py create mode 100644 test/receiverevidencefix/py/src/tui/drivers/linux.py create mode 100644 test/receiverevidencefix/py/src/tui/geometry.py create mode 100644 test/receiverevidencefix/py/src/tui/markup.py create mode 100644 test/receiverevidencefix/py/src/tui/message_pump.py create mode 100644 test/receiverevidencefix/py/src/tui/reactive.py create mode 100644 test/receiverevidencefix/py/src/tui/screen.py create mode 100644 test/receiverevidencefix/py/src/tui/uses.py create mode 100644 test/receiverevidencefix/py/src/tui/worker.py create mode 100644 test/receiverevidencefix/py/src/tui/xterm_parser.py create mode 100644 test/receiverevidencefix/py/src/web/__init__.py create mode 100644 test/receiverevidencefix/py/src/web/datastructures.py create mode 100644 test/receiverevidencefix/py/src/web/errors.py create mode 100644 test/receiverevidencefix/py/src/web/formparsers.py create mode 100644 test/receiverevidencefix/py/src/web/requests.py create mode 100644 test/receiverevidencefix/rb/lib/app/exporter.rb create mode 100644 test/receiverevidencefix/rb/lib/app/helpers.rb create mode 100644 test/receiverevidencefix/rb/lib/app/logger.rb create mode 100644 test/receiverevidencefix/rb/lib/app/notes.rb create mode 100644 test/receiverevidencefix/rb/lib/app/plain.rb create mode 100644 test/receiverevidencefix/swift/Sources/App/Exporter.swift create mode 100644 test/receiverevidencefix/swift/Sources/App/Logger.swift create mode 100644 test/receiverevidencefix/swift/Sources/App/Plain.swift create mode 100644 test/receiverevidencefix/ts/package.json create mode 100644 test/receiverevidencefix/ts/src/cache.ts create mode 100644 test/receiverevidencefix/ts/src/context.ts create mode 100644 test/receiverevidencefix/ts/src/middleware/alias.ts create mode 100644 test/receiverevidencefix/ts/src/middleware/auth.ts create mode 100644 test/receiverevidencefix/ts/src/router/router.ts create mode 100644 test/receiverevidencefix/ts/src/router/smart.ts create mode 100644 test/receiverevidencefix/ts/src/router/trie.ts diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 46492fcb1..35c05e75d 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -463,6 +463,7 @@ "recalltablecheck.sh": 0.2, "recalltotalcheck.sh": 24.7, "receiptpostcheck.sh": 20.4, +"receiverevidencecheck.sh": 36.0, "redactcheck.sh": 2.2, "redactfixcheck.sh": 0.3, "refusaltailcheck.sh": 3.4, diff --git a/README.md b/README.md index 26dd08715..1e1af4329 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-664 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +665 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **664 gate scripts** and is the authoritative list; +`test/regression.sh` names **665 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..30956ac0c 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` | 664 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **664 gate scripts**, all of which exist on disk. +naming **665 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 664. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 665. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..3868b0a5c 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 = [ - ["664 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "664", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh new file mode 100755 index 000000000..29e0e4f2e --- /dev/null +++ b/test/receiverevidencecheck.sh @@ -0,0 +1,548 @@ +#!/usr/bin/env bash +# receiverevidencecheck.sh — a call matched to a definition by NAME ALONE never presents as a single proven edge. +# +# test/receiverevidencecheck.sh # uses build/ripwire on test/receiverevidencefix +# RIPWIRE_BIN=asan/ripwire test/receiverevidencecheck.sh +# test/receiverevidencecheck.sh build_base/ripwire # the RED run (a pre-change binary) +# +# THE DEFECT. A member call `x.m()` (or, in a language with an implicit receiver, a bare `m()`) was bound to an +# in-repo definition of `m` because the NAME matched, with nothing proving the receiver is that definition's +# class. The answer then showed one confident row — and agents adopt a confident wrong row. Each shape below was +# a graded false row (paraphrased; the code is minimal and never built): +# (Bm) a dynamic receiver bound to the wrong in-repo class: a request context's `ctx.onerror()` drawn to the +# Application.onerror its file defines; an item's typed text field `item.text.Get()` drawn to Merger.Get; +# a stored ASGI callable `self._send()` drawn to another class's nested `_send`; a constructor-assigned +# field `this.bucket.listSchemas()` drawn to the caller's own class; a loop variable over inner routers +# drawn to SmartRouter.add; `ctx.req.raw.headers.get()` drawn to Context.get because the file imports +# Context; a member call `context.handler()` drawn to a same-named FREE function. +# (Bd) a builtin/library object behind a variable, in a file that names the class: WeakMap `.set/.get` → the +# same file's accessors, Promise `.then` → Reply.prototype.then, a stream's `.on` → a TEST double, +# `dict.get/keys/items` → StylesBase in a file that imports Styles, `list.append`/`str.split` → +# Content.append/split, URLSearchParams `.append`, Set `.delete`, an outside router's `.all`, an asyncio +# handle's `.cancel`, an outside parser's `.write`, an outside tcell screen's `.Size`. +# (Bp) a parameter or local callable bound to a def elsewhere: `done()` (a parameter) → another file's +# closure `done`; `read = os.read; read( fd )` → Stylesheet.read and `feed = parser.feed` → the base +# class's feed when the parser is a constructed XTermParser (the rows FE-A's local-Keep rule restored); +# `add_widget = widgets.append; add_widget( n )` → another class's nested add_widget. +# (Bi) an implicit receiver (Java, Kotlin, C#, C++, Swift, Ruby): a bare `render()` / `flush()` inside a class +# bound to an UNRELATED class's method, though the language reaches only the class's own members, its +# superclasses and mixins, a free/top-level function, or an import. +# +# THE CONTRACT. Such a call either (a) RESOLVES WITH EVIDENCE — the receiver is `this`/`self`/`cls` inside the +# class or its cone, a construction in scope (`x = new Cls()`, `x = Cls()`, `m := &Merger{}`), a constructor- +# assigned field, a type annotation or declaration on the parameter/local (Go struct field, embedded field, +# aliased import), an import — and then shows one plain row; or (b) KEEPS ITS ROW WITH AN ON-ROW HEDGE: +# `via="name"` on the row itself (XML) / `"via":"name"` on the entry (MCP JSON), listing the by-name candidates +# — never a legend-only hedge, never a silent drop. Every surface shows the same row with the same hedge: +# --callees, --callers, --impact, --path, --connect, --expand , --for , and MCP find_symbol / +# find_referencing_symbols. A wrong candidate may also disappear (resolved elsewhere, or counted external/ +# unresolved), but a TRUE target must stay visible, hedged or not. +# +# PREDICATES (each over one answer; an answer about no symbol is NOROOT and never a pass; rows compare +# (kind, name, file[:line]) — the line only where a spec carries one): +# notproven — the named row is absent, or present WITH the hedge (the positive arms: red when unhedged) +# proven — the named row is present WITHOUT the hedge (evidence: a near miss a new rule could kill) +# exactproven— the set of unhedged rows is exactly the given set (no false candidate rides along unhedged) +# visible — the named row is present, hedged or not (a true target is never dropped) +# marked — the named row is present WITH the hedge (a name-only true edge is kept and labelled) +# +# ARMS (one fixture root per language under test/receiverevidencefix/; each root is indexed on its own). +# (J) JS: Bm ctx.onerror (plain, optional-chained), constructor-assigned field, member call → free function; +# Bd WeakMap/Promise/stream/router/Set/URLSearchParams; Bp a `done` parameter. Near misses: this.onerror, +# same-file bare call, relative module receiver, a closure called in its own function, construction in +# scope (`new Application()` / `new Schemas()` decide between same-named methods), a chained name-only +# call kept visible; the name-only `reply.send()` on a parameter is kept and marked. +# (T) TS: Bm a loop variable over Router[] → SmartRouter.add; WHATWG Headers.get → Context.get/Cache.get. +# Near misses: an annotated parameter, construction through an import alias, a constructed local. +# (P) Python: Bd dict/set/list/str in a file that imports the class; an argument's .animate; asyncio handle; +# an outside package's parser; Bm self._send → a nested _send, a non-self receiver in the class's own +# file (mode_screen.refresh, self.screen.refresh); Bp read = os.read, feed = parser.feed (base class), +# add_widget = widgets.append. Near misses: self.update, a constructor-assigned field (+ cone), +# a typed parameter (+ cone), construction, an import alias, an imported function beside same-named +# methods, cls.method(), the XTermParser.feed the alias really reaches; a name-only parameter call +# kept and marked. +# (G) Go: Bm typed struct field → Merger.Get/Length (the true util.Chars rows visible), a typed parameter, a +# typed local through an aliased import, an embedded field's promoted method; Bd an outside screen's +# Size, an outside value's Runes. Near misses: a pointer-literal local, the method receiver itself. +# (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE +# the tree (or that imports the name from outside) never proves an unrelated class's method. Near +# misses: own members (private too), an in-repo superclass's member (the cone), a free / top-level +# function, a Ruby included module and a Ruby top-level def. +# (H) propagation and parity: the name-only witness `respondWith → reply.send()` is marked in --callees, +# --callers, --impact (d=1), --path, --connect, --expand , --for , MCP find_symbol and +# find_referencing_symbols; the false witness `respond → ctx.onerror()` is absent-or-marked on every +# one of them; and the hedge bit is VALUE-EQUAL between CLI --callees and MCP calls, CLI --callers and +# MCP calledBy, and --callees and --expand , over a set of selectors. +# (C) conservation: each root's census dispositions still sum to calls= with unaccounted=0 (no silent drop). +# (K) the predicates can fail: a planted unhedged false row fails notproven, a planted hedged row passes it and +# fails proven, marked rejects an unhedged row, a rootless or not-found answer is NOROOT, the MCP reader +# reads the hedge, and a non-zero or missing exit status fails the arm. +# +# FLOORS — named, not gated here: +# * Objective-C (a method is only ever sent `[recv msg]`; a bare call is a C function — FE-A's ladder), PHP, Lua, +# Zig, Dart, Elixir: no arm. +# * a computed member `ctx['onerror']()` records no call at all (pinned notproven, trivially). +# * return-type inference (`mode_screen = self.get_screen(); mode_screen.refresh()` → Screen.refresh) is not +# required: only the false single row is gated. +# * --impact rows at d>=2 reached only through a hedged edge: presence is gated, their hedge is not. +# * --uses / --dead-code / --safe-delete and F2's --for hop expansion beyond the block: not gated here. +# * a Python `cls()` construction inside a classmethod is polymorphic (a subclass's override is a legitimate +# candidate): no arm. +# +# Exits non-zero on any failure. + +set -u +export PYTHONDONTWRITEBYTECODE=1 +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # a gate that indexes a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary +CORPUS="$ROOT/test/receiverevidencefix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$CORPUS" ] || { echo "fixture missing: $CORPUS"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +echo "receiverevidencecheck: BIN=$BIN CORPUS=$CORPUS" + +# rw ROOT ARGS… — run against one fixture root (every selector below is relative to that root's `.`) +rw(){ local r="$1"; shift; ( cd "$CORPUS/$r" && "$BIN" . --no-cache "$@" 2>/dev/null ); } + +# ── the one reader every predicate shares ───────────────────────────────────────────────────────────────────── +# parse.py MODE FILE [ARGS] — prints one line per row; NOROOT when the document has no answer about a symbol. +# rows rows of a callees/callers/impact/path answer: "t n path line H d" (H = name | -, d = depth or -) +# ecalls rows inside (all of them, or only those inside the / body named ARG for --for): +# "n line H" +# edges rows of a connect answer: "f t H" +# mcp the entries of one array (ARG) of an MCP tools/call result: "name file line H" +cat >"$TMP/parse.py" <<'PY' +import json, re, sys +mode, path = sys.argv[ 1 ], sys.argv[ 2 ] +arg = sys.argv[ 3 ] if len( sys.argv ) > 3 else "" +doc = open( path, encoding="utf-8", errors="replace" ).read() +def attr( tag, name ): + m = re.search( r'\s' + name + r'="([^"]*)"', tag ) + return m.group( 1 ) if m else None +def hedge( tag ): + return "name" if attr( tag, "via" ) == "name" else "-" +if mode == "rows": + root = re.search( r"<(callees|callers|impact|path) [^>]*>", doc ) + if not root: + print( "NOROOT" ); sys.exit( 0 ) + head = root.group( 0 ) + found = attr( head, "defs" ) or attr( head, "to_defs" ) + if root.group( 1 ) != "path" and ( not found or int( found ) < 1 ): + print( "NOROOT" ); sys.exit( 0 ) + depth = "-" + for s in re.findall( r"]*/>", doc[ root.start(): ] ): + t, n, p = attr( s, "t" ), attr( s, "n" ), attr( s, "p" ) + if not ( t and n and p ): + continue + if attr( s, "d" ): + depth = attr( s, "d" ) + f, _, l = p.rpartition( ":" ) + print( t, n, f, l, hedge( s ), depth ) +elif mode == "ecalls": + scope = doc + if arg: + m = re.search( r']*\sn="' + re.escape( arg ) + r'"[^>]*>(.*?)', doc, re.S ) + if not m: + print( "NOROOT" ); sys.exit( 0 ) + scope = m.group( 1 ) + blocks = re.findall( r"].*?", scope, re.S ) + if not blocks: + print( "NOROOT" ); sys.exit( 0 ) + for b in blocks: + for c in re.findall( r"]*>", b ): + print( attr( c, "n" ), attr( c, "l" ) or "-", hedge( c ) ) +elif mode == "edges": + root = re.search( r"]*>", doc ) + if not root: + print( "NOROOT" ); sys.exit( 0 ) + for e in re.findall( r"]*/>", doc[ root.start(): ] ): + print( attr( e, "f" ), attr( e, "t" ), hedge( e ) ) +elif mode == "mcp": + try: + r = json.loads( doc.strip().splitlines()[ -1 ] ) + d = json.loads( r[ "result" ][ "content" ][ 0 ][ "text" ] ) + except Exception: + print( "NOROOT" ); sys.exit( 0 ) + if d.get( arg ) is None: + print( "NOROOT" ); sys.exit( 0 ) + for e in d[ arg ]: + print( e.get( "name" ), e.get( "file" ), e.get( "line" ), "name" if e.get( "via" ) == "name" else "-" ) +PY +parse(){ python3 "$TMP/parse.py" "$@"; } + +# answer ROOT VERB SEL [EXTRA…] — write the verb's answer to a per-call file, its exit status beside it (FILE.rc) +answer(){ + local r="$1" v="$2" s="$3"; shift 3 + local f; f="$TMP/$r.$v.$( printf '%s' "$s$*" | tr '/:.,= ' '______' ).xml" + rw "$r" "--$v=$s" "$@" >"$f"; printf '%s' "$?" >"$f.rc" + printf '%s' "$f" +} +# ran_ok LABEL FILE — the run behind FILE exited 0 (a non-zero or missing exit status FAILs the arm) +ran_ok(){ + local rc; rc="$( cat "$2.rc" 2>/dev/null )" + if [ "$rc" = "0" ]; then return 0; fi + no "$1 exited rc=${rc:-unknown}"; return 1 +} +# getrows ROOT VERB SEL — rows of the verb's answer into $GOT ("" on a failed run, NOROOT when about no symbol) +getrows(){ + local f; f="$( answer "$1" "$2" "$3" )"; GOT="" + ran_ok "($1) --$2=$3" "$f" || return 1 + GOT="$( parse rows "$f" )" + if [ "$GOT" = "NOROOT" ]; then no "($1) --$2=$3 produced no <$2> answer about a symbol"; return 1; fi + return 0 +} +# match SPEC — the awk condition for "n path[:line]" over a rows line "t n path line H d" +specmatch(){ # prints the matching rows of $GOT for SPEC + local n="${1%% *}" p="${1#* }" l="" + case "$p" in *:[0-9]*) l="${p##*:}"; p="${p%:*}";; esac + printf '%s\n' "$GOT" | awk -v n="$n" -v p="$p" -v l="$l" 'NF >= 5 && $2 == n && $3 == p && ( l == "" || $4 == l )' +} +show(){ printf '%s' "$GOT" | awk 'NF >= 5 { printf "%s %s %s:%s%s; ", $1, $2, $3, $4, ( $5 == "name" ? " via=name" : "" ) }'; } + +notproven(){ + local r="$1" v="$2" s="$3"; shift 3 + getrows "$r" "$v" "$s" || return + local spec hits + for spec in "$@"; do + hits="$( specmatch "$spec" | awk '$5 != "name"' )" + if [ -n "$hits" ]; then no "($r) --$v=$s shows [$spec] as a PROVEN edge (no receiver evidence, no via=\"name\"): $( show )" + else ok "($r) --$v=$s never proves [$spec] (absent or hedged)"; fi + done +} +proven(){ + local r="$1" v="$2" s="$3"; shift 3 + getrows "$r" "$v" "$s" || return + local spec t hits + for spec in "$@"; do + t="${spec%% *}"; hits="$( specmatch "${spec#* }" | awk -v t="$t" '$1 == t && $5 != "name"' )" + if [ -n "$hits" ]; then ok "($r) --$v=$s keeps the proven row [$spec]" + else no "($r) --$v=$s lost or hedged the proven row [$spec]: $( show )"; fi + done +} +exactproven(){ + local r="$1" v="$2" s="$3" want="$4" got + getrows "$r" "$v" "$s" || return + got="$( printf '%s\n' "$GOT" | awk 'NF >= 5 && $5 != "name" { print $1 " " $2 " " $3 }' | sort -u | tr '\n' ';' | sed 's/;$//' )" + want="$( printf '%s' "$want" | tr ';' '\n' | sort -u | tr '\n' ';' | sed 's/;$//' )" + if [ "$got" = "$want" ]; then ok "($r) --$v=$s unhedged rows are exactly [${want:-none}]" + else no "($r) --$v=$s unhedged rows are [${got:-none}], want [${want:-none}]"; fi +} +visible(){ + local r="$1" v="$2" s="$3"; shift 3 + getrows "$r" "$v" "$s" || return + local spec + for spec in "$@"; do + if [ -n "$( specmatch "$spec" )" ]; then ok "($r) --$v=$s shows the true target [$spec] (hedged or not)" + else no "($r) --$v=$s does not show the true target [$spec]: $( show )"; fi + done +} +marked(){ + local r="$1" v="$2" s="$3"; shift 3 + getrows "$r" "$v" "$s" || return + local spec + for spec in "$@"; do + if [ -n "$( specmatch "$spec" | awk '$5 == "name"' )" ]; then ok "($r) --$v=$s keeps [$spec] marked via=\"name\"" + elif [ -n "$( specmatch "$spec" )" ]; then no "($r) --$v=$s shows the name-only [$spec] with no via=\"name\": $( show )" + else no "($r) --$v=$s dropped the name-only true edge [$spec]: $( show )"; fi + done +} + +echo "=== (J) JS: receivers with no evidence never prove a same-named method ===" +notproven js callees lib/application.js:respond "onerror lib/application.js" +visible js callees lib/application.js:respond "onerror lib/context.js" +notproven js callees lib/application.js:respondSafely "onerror lib/application.js" +notproven js callees lib/application.js:respondComputed "onerror lib/application.js" +notproven js callees lib/application.js:schemas "listSchemas lib/application.js" +visible js callees lib/application.js:schemas "listSchemas lib/schemas.js" +notproven js callees lib/handle.js:run "handler lib/handle.js" +notproven js callees lib/request.js:compile "get lib/request.js" "set lib/request.js" +notproven js callees lib/reply.js:awaitResult "then lib/reply.js" +notproven js callees lib/reply.js:pipePayload "on test/server.test.js" +notproven js callees lib/routes.js:fallback "all lib/routes.js" +notproven js callees lib/routes.js:forget "delete lib/routes.js" +notproven js callees lib/search.js:encode "append lib/response.js" +notproven js callees lib/reply.js:writePayload "done lib/parser.js" +echo "--- (J) evidence decides between same-named methods" +exactproven js callees lib/uses.js:build "cls Application lib/application.js;method onerror lib/application.js;cls Schemas lib/schemas.js;method listSchemas lib/schemas.js" +echo "--- (J) near misses: true edges kept" +exactproven js callees lib/application.js:handle "method onerror lib/application.js;fn respond lib/application.js" +exactproven js callees lib/handle.js:wrapped "fn handler lib/handle.js" +exactproven js callees lib/uses.js:viaModule "fn respondWith lib/reply.js" +exactproven js callees lib/trailer.js:sendTrailer "fn send lib/trailer.js" +notproven js callees lib/reply.js:notFound "send lib/trailer.js" +visible js callees lib/reply.js:notFound "send lib/reply.js" "code lib/reply.js" +marked js callees lib/reply.js:respondWith "send lib/reply.js" + +echo "=== (T) TS ===" +notproven ts callees src/router/smart.ts:match "add src/router/smart.ts" +notproven ts callees src/middleware/auth.ts:bearer "get src/context.ts" "get src/cache.ts" +echo "--- (T) near misses: annotation, alias construction, constructed local" +exactproven ts callees src/middleware/auth.ts:readUser "method get src/context.ts" +exactproven ts callees src/middleware/alias.ts:remember "cls Context src/context.ts;method set src/context.ts" +exactproven ts callees src/middleware/alias.ts:single "cls TrieRouter src/router/trie.ts;method add src/router/trie.ts;method match src/router/trie.ts" + +echo "=== (P) Python ===" +notproven py callees src/tui/css/stylesheet.py:reparse "get src/tui/css/styles.py" "keys src/tui/css/styles.py" "items src/tui/css/styles.py" "update src/tui/css/stylesheet.py" +notproven py callees src/tui/css/stylesheet.py:refresh_rules "animate src/tui/css/styles.py" +notproven py callees src/tui/reactive.py:watch "append src/tui/content.py" +notproven py callees src/tui/reactive.py:toggle "split src/tui/content.py" "split src/tui/geometry.py" +notproven py callees src/tui/callback.py:invoke "call_later src/tui/message_pump.py" "cancel src/tui/worker.py" +notproven py callees src/web/formparsers.py:parse_form "write src/web/datastructures.py" +notproven py callees src/web/requests.py:send_json "_send src/web/errors.py" +notproven py callees src/tui/app.py:switch_mode "refresh src/tui/app.py" +notproven py callees src/tui/app.py:repaint_screen "refresh src/tui/app.py" +visible py callees src/tui/app.py:repaint_screen "refresh src/tui/screen.py" +notproven py callees src/tui/drivers/linux.py:process_events "read src/tui/css/stylesheet.py" "feed src/tui/xterm_parser.py:2" +notproven py callees src/tui/screen.py:collect "add_widget src/tui/compositor.py" +echo "--- (P) near misses: self, constructor-assigned field, typed parameter, construction, alias, import, cls" +proven py callees src/tui/drivers/linux.py:process_events "fn feed src/tui/xterm_parser.py:7" +exactproven py callees src/tui/css/stylesheet.py:own "fn update src/tui/css/stylesheet.py;fn get src/tui/css/styles.py" +exactproven py callees src/tui/app.py:repaint "fn refresh src/tui/app.py" +exactproven py callees src/tui/uses.py:sync "fn update src/tui/css/stylesheet.py" +exactproven py callees src/tui/uses.py:apply "fn animate src/tui/css/styles.py" +exactproven py callees src/tui/uses.py:build "cls Stylesheet src/tui/css/stylesheet.py;fn update src/tui/css/stylesheet.py;fn read src/tui/css/stylesheet.py" +exactproven py callees src/tui/css/stylesheet.py:parse_rules "fn parse src/tui/css/parse.py" +exactproven py callees src/tui/css/stylesheet.py:blank "fn default_rules src/tui/css/stylesheet.py" +marked py callees src/tui/callback.py:schedule "call_later src/tui/message_pump.py" + +echo "=== (G) Go ===" +notproven go callees src/result.go:buildResult "Get src/merger.go" "Length src/merger.go" +visible go callees src/result.go:buildResult "Get src/util/chars.go" "Length src/util/chars.go" +exactproven go callees src/result.go:fromValue "method Get src/util/chars.go" +exactproven go callees src/embed.go:aliased "method Length src/util/chars.go" +notproven go callees src/embed.go:firstRune "Get src/merger.go" +visible go callees src/embed.go:firstRune "Get src/util/chars.go" +notproven go callees src/tui/tcell.go:MaxY "Size src/tui/tcell.go" +notproven go callees src/tui/tcell.go:Graphemes "Runes src/tui/tcell.go" +echo "--- (G) near misses: a pointer-literal local, the method receiver itself" +exactproven go callees src/result.go:merged "method Length src/merger.go" +exactproven go callees src/merger.go:First "method Get src/merger.go" +exactproven go callees src/tui/tcell.go:Lines "method Size src/tui/tcell.go" + +echo "=== (I) implicit receiver: a bare call never proves an unrelated class's method ===" +notproven java callees src/main/java/app/Logger.java:line "render src/main/java/app/Exporter.java" +notproven java callees src/main/java/app/Logger.java:close "flush src/main/java/app/Buffer.java" "flush src/main/java/app/Exporter.java" +notproven java callees src/main/java/app/FileBuffer.java:sync "flush src/main/java/app/Exporter.java" +notproven kt callees src/app/Plain.kt:finish "flush src/app/Exporter.kt" "flush src/app/Logger.kt" +notproven cs callees App/Logger.cs:Line "Render App/Exporter.cs" +notproven cs callees App/Logger.cs:Finish "Flush App/Logger.cs" "Flush App/Exporter.cs" +notproven cpp callees src/plain.cpp:finish "flush src/exporter.cpp" "flush src/logger.cpp" +notproven swift callees Sources/App/Plain.swift:finish "flush Sources/App/Exporter.swift" "flush Sources/App/Logger.swift" +notproven rb callees lib/app/plain.rb:finish "render lib/app/exporter.rb" "flush lib/app/logger.rb" +echo "--- (I) near misses: own members, the in-repo superclass (cone), free/top-level functions, mixins" +proven java callees src/main/java/app/Logger.java:close "method reset src/main/java/app/Logger.java" +proven java callees src/main/java/app/FileBuffer.java:sync "method flush src/main/java/app/Buffer.java" +exactproven java callees src/main/java/app/Buffer.java:write "method flush src/main/java/app/Buffer.java" +exactproven kt callees src/app/Logger.kt:line "fn render src/app/Logger.kt" +exactproven kt callees src/app/Logger.kt:close "fn flush src/app/Logger.kt;fn reset src/app/Logger.kt" +exactproven cs callees App/Logger.cs:Close "method Flush App/Logger.cs;method Reset App/Logger.cs" +exactproven cpp callees src/logger.cpp:line "fn render src/logger.cpp" +exactproven cpp callees src/logger.cpp:close "method flush src/logger.cpp;method reset src/logger.cpp" +exactproven swift callees Sources/App/Logger.swift:line "fn render Sources/App/Logger.swift" +exactproven swift callees Sources/App/Logger.swift:close "fn flush Sources/App/Logger.swift;fn reset Sources/App/Logger.swift" +exactproven rb callees lib/app/logger.rb:line "method stamp lib/app/helpers.rb" +exactproven rb callees lib/app/logger.rb:close "method flush lib/app/logger.rb;method reset lib/app/logger.rb;method top_level_note lib/app/notes.rb" + +echo "=== (H) propagation: one hedge, every surface ===" +# the name-only TRUE witness respondWith → reply.send(): kept and marked everywhere +marked js callers lib/reply.js:send "respondWith lib/reply.js" +f="$( answer js impact lib/reply.js:send )" +if ran_ok "(js) --impact=lib/reply.js:send" "$f"; then + GOT="$( parse rows "$f" )" + if [ "$GOT" = "NOROOT" ]; then no "(H) --impact=lib/reply.js:send produced no answer about a symbol" + else + d1="$( printf '%s\n' "$GOT" | awk '$2 == "respondWith" && $6 == "1" { print $5 }' )" + if [ "$d1" = "name" ]; then ok "(H) --impact=lib/reply.js:send: the d=1 row respondWith is marked via=\"name\"" + else no "(H) --impact=lib/reply.js:send: the d=1 row respondWith is [${d1:-absent}], want via=\"name\": $( show )"; fi + if [ -n "$( printf '%s\n' "$GOT" | awk '$2 == "viaModule"' )" ]; then ok "(H) --impact=lib/reply.js:send keeps viaModule (reached through the hedged edge)" + else no "(H) --impact=lib/reply.js:send dropped viaModule: $( show )"; fi + fi +fi +f="$( answer js path lib/uses.js:viaModule,lib/reply.js:send )" +if ran_ok "(js) --path=viaModule,send" "$f"; then + GOT="$( parse rows "$f" )" + last="$( printf '%s\n' "$GOT" | awk 'NF >= 5' | tail -1 )" + case "$last" in + "method send lib/reply.js 7 name"*) ok "(H) --path=viaModule,send: the hop into Reply.prototype.send is marked via=\"name\"";; + *) no "(H) --path=viaModule,send: last hop is [${last:-none}], want Reply.prototype.send marked via=\"name\": $( show )";; + esac +fi +f="$( answer js connect lib/reply.js:respondWith,lib/reply.js:send )" +if ran_ok "(js) --connect=respondWith,send" "$f"; then + e="$( parse edges "$f" | awk '$1 == "respondWith" && $2 == "send" { print $3 }' )" + if [ "$e" = "name" ]; then ok "(H) --connect: the edge respondWith → send is marked via=\"name\"" + else no "(H) --connect: the edge respondWith → send is [${e:-absent}], want via=\"name\": $( parse edges "$f" | tr '\n' ';' )"; fi +fi +f="$( answer js expand lib/reply.js:respondWith --top-k=0 --legend=full )" +if ran_ok "(js) --expand=respondWith" "$f"; then + c="$( parse ecalls "$f" | awk '$1 == "send" { print $3 }' )" + if [ "$c" = "name" ]; then ok "(H) --expand=respondWith: row send is marked via=\"name\"" + else no "(H) --expand=respondWith: row send is [${c:-absent}], want via=\"name\": $( parse ecalls "$f" | tr '\n' ';' )"; fi +fi +f="$( answer js for "how does respondWith send the reply" --legend=full )" +if ran_ok "(js) --for=respondWith" "$f"; then + c="$( parse ecalls "$f" respondWith | awk '$1 == "send" { print $3 }' )" + if [ "$c" = "name" ]; then ok "(H) --for: respondWith's body row send is marked via=\"name\"" + else no "(H) --for: respondWith's body row send is [${c:-absent}], want via=\"name\": $( parse ecalls "$f" respondWith | tr '\n' ';' )"; fi +fi +# the FALSE witness respond → ctx.onerror(): absent or marked on every surface +notproven js callers lib/application.js:onerror "respond lib/application.js" "respondSafely lib/application.js" +f="$( answer js impact lib/application.js:onerror )" +if ran_ok "(js) --impact=lib/application.js:onerror" "$f"; then + GOT="$( parse rows "$f" )" + if [ "$GOT" = "NOROOT" ]; then no "(H) --impact=lib/application.js:onerror produced no answer about a symbol" + elif [ -n "$( printf '%s\n' "$GOT" | awk '$2 == "respond" && $6 == "1" && $5 != "name"' )" ]; then + no "(H) --impact=lib/application.js:onerror proves respond at d=1: $( show )" + else ok "(H) --impact=lib/application.js:onerror never proves respond at d=1"; fi +fi +f="$( answer js path lib/application.js:respond,lib/application.js:onerror )" +if ran_ok "(js) --path=respond,onerror" "$f"; then + GOT="$( parse rows "$f" )" + last="$( printf '%s\n' "$GOT" | awk 'NF >= 5' | tail -1 )" + reach="$( grep -oE ']*>' "$f" | grep -oE ' reachable="[0-9]+"' | grep -oE '[0-9]+' )" + if [ -z "$reach" ]; then no "(H) --path=respond,onerror carries no reachable= (no answer)" + elif [ "$reach" = "0" ]; then ok "(H) --path=respond,onerror: unreachable (the false edge is gone)" + else case "$last" in + *" name "*|*" name") ok "(H) --path=respond,onerror: the hop into Application.onerror is marked via=\"name\"";; + *) no "(H) --path=respond,onerror proves the hop into Application.onerror: [$last]";; + esac; fi +fi +f="$( answer js expand lib/application.js:respond --top-k=0 --legend=full )" +if ran_ok "(js) --expand=respond" "$f"; then + rows="$( parse ecalls "$f" )" + if [ "$rows" = "NOROOT" ]; then no "(H) --expand=respond emitted no block" + elif [ -n "$( printf '%s\n' "$rows" | awk '$1 == "onerror" && $2 == "12" && $3 != "name"' )" ]; then + no "(H) --expand=respond: proves onerror at application.js:12: $( printf '%s' "$rows" | tr '\n' ';' )" + else ok "(H) --expand=respond: never proves Application.onerror"; fi +fi + +echo "--- (H) MCP twins (fresh TMPDIR: the MCP cache lives there)" +mkdir -p "$TMP/mcp" +# mcp ROOT TOOL SELECTOR OUT — one tools/call; the raw stream into OUT, the server's exit status into OUT.rc +mcp(){ + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + "$( printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s","symbol":"%s"}}}' "$2" "$CORPUS/$1" "$3" )" \ + | TMPDIR="$TMP/mcp" "$BIN" --mcp >"$4" 2>/dev/null + printf '%s' "$?" >"$4.rc" +} +# mcp_hedge ROOT TOOL SEL FIELD NAME FILE WANT — WANT = name (marked) | notproven (absent or marked) +mcp_hedge(){ + local r="$1" tool="$2" sel="$3" field="$4" n="$5" file="$6" want="$7" out h + out="$TMP/mcp.$r.$tool.$( printf '%s' "$sel" | tr '/:.' '___' )" + mcp "$r" "$tool" "$sel" "$out" + ran_ok "(H) MCP $tool $sel" "$out" || return + if [ "$( parse mcp "$out" "$field" )" = "NOROOT" ]; then no "(H) MCP $tool $sel: no JSON answer with $field"; return; fi + h="$( parse mcp "$out" "$field" | awk -v n="$n" -v f="$file" '$1 == n && $2 == f { print $4 }' | sort -u | tr '\n' ' ' | sed 's/ $//' )" + if [ "$want" = "name" ]; then + if [ "$h" = "name" ]; then ok "(H) MCP $tool $sel: $field entry $n@$file is marked via=name" + else no "(H) MCP $tool $sel: $field entry $n@$file is [${h:-absent}], want via=name"; fi + else + case " $h " in *" - "*) no "(H) MCP $tool $sel: $field entry $n@$file is PROVEN (no via=name)";; + *) ok "(H) MCP $tool $sel: $field entry $n@$file is absent or marked";; esac + fi +} +mcp_hedge js find_symbol lib/reply.js:respondWith calls send lib/reply.js name +mcp_hedge js find_referencing_symbols lib/reply.js:send calledBy respondWith lib/reply.js name +mcp_hedge js find_symbol lib/application.js:respond calls onerror lib/application.js notproven +mcp_hedge js find_referencing_symbols lib/application.js:onerror calledBy respond lib/application.js notproven +mcp_hedge py find_symbol src/tui/callback.py:schedule calls call_later src/tui/message_pump.py name + +echo "--- (H) parity: the hedge bit is value-equal across CLI and MCP, and across --callees and --expand" +# cli_set ROOT VERB SEL — "name@file#H" lines of the CLI answer; mcp_set ROOT TOOL SEL FIELD — the same from MCP +cli_set(){ local f rc; f="$( answer "$1" "$2" "$3" )"; rc="$( cat "$f.rc" 2>/dev/null )" + if [ "$rc" != "0" ]; then echo "RUNFAIL --$2=$3 rc=${rc:-unknown}"; return; fi + parse rows "$f" | awk 'NF >= 5 { print $2 "@" $3 "#" $5 } $0 == "NOROOT" { print }' | sort -u; } +mcp_set(){ local out rc; out="$TMP/mcpset.$1.$2.$( printf '%s' "$3" | tr '/:.' '___' )"; mcp "$1" "$2" "$3" "$out" + rc="$( cat "$out.rc" 2>/dev/null )" + if [ "$rc" != "0" ]; then echo "RUNFAIL MCP $2 $3 rc=${rc:-unknown}"; return; fi + parse mcp "$out" "$4" | awk 'NF >= 4 { print $1 "@" $2 "#" $4 } $0 == "NOROOT" { print }' | sort -u; } +parity(){ + local label="$1" a="$2" b="$3" + case "$a$b" in *RUNFAIL*) no "(H) parity $label: a run exited non-zero ([$a] [$b])"; return;; esac + if [ -z "$a" ] || [ "$a" = "NOROOT" ] || [ "$b" = "NOROOT" ]; then no "(H) parity $label: an empty or rootless side ([$a] vs [$b])"; return; fi + if [ "$a" = "$b" ]; then ok "(H) parity $label: $( printf '%s' "$a" | tr '\n' ' ' )" + else no "(H) parity $label: CLI [$( printf '%s' "$a" | tr '\n' ' ' )] vs [$( printf '%s' "$b" | tr '\n' ' ' )]"; fi +} +for sel in lib/application.js:respond lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile lib/uses.js:build lib/application.js:handle; do + parity "callees/find_symbol.calls $sel" "$( cli_set js callees "$sel" )" "$( mcp_set js find_symbol "$sel" calls )" +done +for sel in lib/reply.js:send lib/application.js:onerror lib/schemas.js:listSchemas; do + parity "callers/find_referencing_symbols.calledBy $sel" "$( cli_set js callers "$sel" )" "$( mcp_set js find_referencing_symbols "$sel" calledBy )" +done +# --callees vs --expand : compared by name + hedge (a row names no file) +for sel in lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile; do + fc="$( answer js callees "$sel" )"; fe="$( answer js expand "$sel" --top-k=0 --legend=full )" + if ran_ok "(H) --callees=$sel" "$fc" && ran_ok "(H) --expand=$sel" "$fe"; then + a="$( parse rows "$fc" | awk 'NF >= 5 { print $2 "#" $5 }' | sort -u )" + b="$( parse ecalls "$fe" | awk 'NF >= 3 { print $1 "#" $3 }' | sort -u )" + parity "callees/expand-calls $sel" "$a" "$b" + fi +done + +echo "=== (C) conservation: no call silently disappears ===" +ROOTS="js ts py go java kt cs cpp swift rb" +for r in $ROOTS; do + if ! rw "$r" --pin-census="$TMP/$r.tsv" >"$TMP/$r.map.xml"; then no "(C) ($r) the census run exited non-zero"; continue; fi + DL="$( grep -m1 '^# dispositions ' "$TMP/$r.tsv" 2>/dev/null )" + if [ -z "$DL" ]; then no "(C) ($r) the census carries no '# dispositions' line"; continue; fi + if python3 - "$DL" <<'PY' +import re, sys +kv = dict( ( k, int( v ) ) for k, v in re.findall( r"(\w+)=(\d+)", sys.argv[ 1 ] ) ) +if "calls" not in kv or "unaccounted" not in kv or kv[ "calls" ] < 1: + sys.exit( 1 ) +total = kv.pop( "calls" ) +sys.exit( 0 if kv[ "unaccounted" ] == 0 and sum( kv.values() ) == total else 1 ) +PY + then ok "(C) ($r) dispositions sum to calls= with unaccounted=0" + else no "(C) ($r) conservation broken: $DL"; fi +done + +echo "=== (K) the predicates can fail ===" +GOT="$( printf '' >"$TMP/k1.xml"; parse rows "$TMP/k1.xml" )" +if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then ok "(K) an unhedged planted row reads as PROVEN (notproven would fail it)" +else no "(K) the reader missed an unhedged planted row: [$GOT]"; fi +GOT="$( printf '' >"$TMP/k2.xml"; parse rows "$TMP/k2.xml" )" +if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 == "name"' )" ] && [ -z "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then + ok "(K) a hedged planted row reads as marked (notproven passes it, proven fails it)" +else no "(K) the reader did not read via=\"name\": [$GOT]"; fi +GOT="$( printf '' >"$TMP/k3.xml"; parse rows "$TMP/k3.xml" )" +if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then ok "(K) via=\"import\" is not the hedge" +else no "(K) the reader took via=\"import\" for the hedge: [$GOT]"; fi +printf 'nothing here' >"$TMP/k4.xml" +if [ "$( parse rows "$TMP/k4.xml" )" = "NOROOT" ]; then ok "(K) an answer with no root element is NOROOT" +else no "(K) a rootless document read as an answer"; fi +printf '' >"$TMP/k5.xml" +if [ "$( parse rows "$TMP/k5.xml" )" = "NOROOT" ]; then ok "(K) an answer about no symbol (no defs=) is NOROOT" +else no "(K) a not-found answer read as an empty answer"; fi +printf 'sig' >"$TMP/k6.xml" +if [ "$( parse ecalls "$TMP/k6.xml" respondWith )" = "NOROOT" ]; then ok "(K) a --for answer without the named body is NOROOT, never an empty pass" +else no "(K) ecalls read another body's as respondWith's"; fi +printf 'sig' >"$TMP/k7.xml" +if [ "$( parse ecalls "$TMP/k7.xml" respondWith )" = "send 7 name" ]; then ok "(K) ecalls reads a marked row" +else no "(K) ecalls missed a marked row: [$( parse ecalls "$TMP/k7.xml" respondWith )]"; fi +printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"calls\":[{\"name\":\"send\",\"file\":\"lib/reply.js\",\"line\":7,\"via\":\"name\"},{\"name\":\"code\",\"file\":\"lib/reply.js\",\"line\":12}]}"}]}}' >"$TMP/k8.json" +if [ "$( parse mcp "$TMP/k8.json" calls | tr '\n' ';' )" = "send lib/reply.js 7 name;code lib/reply.js 12 -;" ]; then ok "(K) the MCP reader reads the hedge bit per entry" +else no "(K) the MCP reader misread: [$( parse mcp "$TMP/k8.json" calls | tr '\n' ';' )]"; fi +if [ "$( parse mcp "$TMP/k8.json" calledBy )" = "NOROOT" ]; then ok "(K) an MCP answer without the field is NOROOT" +else no "(K) the MCP reader invented a missing field"; fi +printf '1' >"$TMP/k9.rc" +if ( ran_ok k "$TMP/k9" ) >/dev/null; then no "(K) ran_ok accepted a run that exited 1" +else ok "(K) ran_ok fails a run that exited non-zero"; fi +rm -f "$TMP/k10.rc" +if ( ran_ok k "$TMP/k10" ) >/dev/null; then no "(K) ran_ok accepted a run with no recorded exit status" +else ok "(K) ran_ok fails a run with no recorded exit status"; fi +if ( parity k "a#-" "a#name" ) | grep -q FAIL; then ok "(K) parity fails when the hedge bit differs" +else no "(K) parity accepted differing hedge bits"; fi +if ( parity k "" "" ) | grep -q FAIL; then ok "(K) parity fails on two empty sides (never a vacuous pass)" +else no "(K) parity accepted two empty sides"; fi +if ( parity k "RUNFAIL --callees=x rc=1" "RUNFAIL --callees=x rc=1" ) | grep -q FAIL; then ok "(K) parity fails when a run exited non-zero" +else no "(K) parity accepted a failed run"; fi + +if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAILURES ABOVE"; exit 1; fi diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md new file mode 100644 index 000000000..203cb560f --- /dev/null +++ b/test/receiverevidencefix/README.md @@ -0,0 +1,34 @@ +# receiverevidencefix — calls bound by name alone (test/receiverevidencecheck.sh) + +Each directory is its own root (the gate indexes one at a time). Every root holds member calls (or, in an +implicit-receiver language, bare calls) whose receiver nothing proves to be the class of the same-named +definition beside them, plus near-miss calls whose receiver IS proven and whose edges must stay plain. + +- `js/` — `ctx.onerror()` (plain and optional-chained) in the file that defines `Application.onerror`; a + constructor-assigned `this.bucket` (a `Schemas`) beside the caller's own `listSchemas`; WeakMap `.set/.get` + beside same-file accessors; Promise `.then` beside `Reply.prototype.then`; a stream's `.on` beside a test + double; an outside router's `.all` and a Set's `.delete` beside instance shorthands; URLSearchParams `.append` + beside `Response.append`; a `done` parameter beside another file's closure; `context.handler()` beside a free + `handler`. Kept: `this.onerror()`, a same-file bare call, a relative module receiver, a closure called in its + own function, `new Application()` / `new Schemas()` deciding between same-named methods; `reply.send()` on a + parameter is the name-only true edge the gate wants kept AND marked. +- `ts/` — a loop variable over `Router[]` beside `SmartRouter.add`; WHATWG `headers.get()` beside + `Context.get` / `Cache.get`. Kept: an annotated parameter, construction through an import alias, a + constructed local. +- `py/` (src layout) — dict/set/list/str receivers in files that import `Styles` / `Content`; an argument's + `.animate`; an asyncio handle's `.cancel`; an outside package's parser `.write`; `self._send` holding a passed + callable beside a nested `_send`; non-self receivers in `App`'s own file; `read = os.read` and + `feed = parser.feed` called from a nested def; `add_widget = widgets.append` beside a nested `add_widget`. + Kept: `self.update()`, a constructor-assigned field through the class cone, a typed parameter, construction, + an import alias, an imported function beside same-named methods, `cls.default_rules()`, the + `XTermParser.feed` the alias really reaches; `pump.call_later()` on a parameter is kept and marked. +- `go/` — an item's typed `text util.Chars` field, a typed parameter, a typed local through an aliased import + and an embedded field, all beside `Merger.Get/Length` in the caller's package; an outside `tcell.Screen`'s + `Size` and an outside value's `Runes` beside a renderer's methods. Kept: `m := &Merger{}`, the method + receiver itself. +- `java/ kt/ cs/ cpp/ swift/ rb/` — a bare call inside a class whose base is outside the tree (or whose name + comes from an outside import) beside an unrelated class's method of that name. Kept: own members (private + too), an in-repo superclass's member, a free / top-level function, a Ruby included module, a Ruby top-level + def. + +The names are paraphrases of graded false rows; the code is minimal and is never built. diff --git a/test/receiverevidencefix/cpp/src/exporter.cpp b/test/receiverevidencefix/cpp/src/exporter.cpp new file mode 100644 index 000000000..8955d6a81 --- /dev/null +++ b/test/receiverevidencefix/cpp/src/exporter.cpp @@ -0,0 +1,5 @@ +#include "exporter.hpp" +#include + +void Exporter::flush() { std::puts("exporter flush"); } +std::string Exporter::render(const std::string& s) { return "<" + s + ">"; } diff --git a/test/receiverevidencefix/cpp/src/exporter.hpp b/test/receiverevidencefix/cpp/src/exporter.hpp new file mode 100644 index 000000000..18f0d9837 --- /dev/null +++ b/test/receiverevidencefix/cpp/src/exporter.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +class Exporter { +public: + void flush(); + std::string render(const std::string& s); +}; diff --git a/test/receiverevidencefix/cpp/src/logger.cpp b/test/receiverevidencefix/cpp/src/logger.cpp new file mode 100644 index 000000000..456f04f62 --- /dev/null +++ b/test/receiverevidencefix/cpp/src/logger.cpp @@ -0,0 +1,10 @@ +#include "logger.hpp" +#include + +std::string render(const std::string& s) { return "[" + s + "]"; } +void Base::flush() { std::puts("base flush"); } + +// A bare call inside a member function: an own/inherited member or a free function, never an unrelated class's member. +std::string Logger::line(const std::string& s) { return render(s); } +void Logger::close() { flush(); reset(); } +void Logger::reset() { std::puts("reset"); } diff --git a/test/receiverevidencefix/cpp/src/logger.hpp b/test/receiverevidencefix/cpp/src/logger.hpp new file mode 100644 index 000000000..29ce5c9c9 --- /dev/null +++ b/test/receiverevidencefix/cpp/src/logger.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +std::string render(const std::string& s); + +class Base { +public: + void flush(); +}; + +class Logger : public Base { +public: + std::string line(const std::string& s); + void close(); +private: + void reset(); +}; diff --git a/test/receiverevidencefix/cpp/src/plain.cpp b/test/receiverevidencefix/cpp/src/plain.cpp new file mode 100644 index 000000000..a39fb560b --- /dev/null +++ b/test/receiverevidencefix/cpp/src/plain.cpp @@ -0,0 +1,4 @@ +#include "plain.hpp" + +// A bare flush() inside a member: the outside base's member, never Base::flush or Exporter::flush. +void Plain::finish() { flush(); } diff --git a/test/receiverevidencefix/cpp/src/plain.hpp b/test/receiverevidencefix/cpp/src/plain.hpp new file mode 100644 index 000000000..3ef02f9c9 --- /dev/null +++ b/test/receiverevidencefix/cpp/src/plain.hpp @@ -0,0 +1,8 @@ +#pragma once +#include + +// Plain inherits flush() from an OUTSIDE class. +class Plain : public std::ostream { +public: + void finish(); +}; diff --git a/test/receiverevidencefix/cs/App/Exporter.cs b/test/receiverevidencefix/cs/App/Exporter.cs new file mode 100644 index 000000000..97e29b413 --- /dev/null +++ b/test/receiverevidencefix/cs/App/Exporter.cs @@ -0,0 +1,8 @@ +namespace App +{ + public class Exporter + { + public void Flush() { System.Console.WriteLine("exporter flush"); } + public string Render(string s) { return "<" + s + ">"; } + } +} diff --git a/test/receiverevidencefix/cs/App/Logger.cs b/test/receiverevidencefix/cs/App/Logger.cs new file mode 100644 index 000000000..99b710f7d --- /dev/null +++ b/test/receiverevidencefix/cs/App/Logger.cs @@ -0,0 +1,24 @@ +using static Markup.Html; + +namespace App +{ + public class Base + { + public void Flush() { System.Console.WriteLine("base flush"); } + } + + // Inherited: a bare Flush() reaches Base.Flush through the class cone; Reset is its own. + public class Child : Base + { + public void Close() { Flush(); Reset(); } + private void Reset() { System.Console.WriteLine("reset"); } + } + + // A bare call is this.Method() or a using-static import: Logger inherits Flush() from an OUTSIDE class + // and imports Render from an outside type; Exporter and Base are unrelated. + public class Logger : System.IO.StringWriter + { + public string Line(string s) { return Render(s); } + public void Finish() { Flush(); } + } +} diff --git a/test/receiverevidencefix/go/go.mod b/test/receiverevidencefix/go/go.mod new file mode 100644 index 000000000..d88fd0224 --- /dev/null +++ b/test/receiverevidencefix/go/go.mod @@ -0,0 +1,6 @@ +module example.com/fzfish + +go 1.22 + +require github.com/gdamore/tcell/v2 v2.7.0 +require github.com/rivo/uniseg v0.4.7 diff --git a/test/receiverevidencefix/go/src/embed.go b/test/receiverevidencefix/go/src/embed.go new file mode 100644 index 000000000..0d2bdcdcc --- /dev/null +++ b/test/receiverevidencefix/go/src/embed.go @@ -0,0 +1,20 @@ +package fzf + +import u "example.com/fzfish/src/util" + +// FileItem embeds Chars: its promoted Get is Chars.Get. +type FileItem struct { + u.Chars + path string +} + +// firstRune: a promoted method through an embedded field. +func firstRune(fi *FileItem) rune { + return fi.Get(0) +} + +// aliased: a local typed through an aliased import. +func aliased() int { + var c u.Chars + return c.Length() +} diff --git a/test/receiverevidencefix/go/src/item.go b/test/receiverevidencefix/go/src/item.go new file mode 100644 index 000000000..657d9f0e9 --- /dev/null +++ b/test/receiverevidencefix/go/src/item.go @@ -0,0 +1,7 @@ +package fzf + +import "example.com/fzfish/src/util" + +type Item struct { + text util.Chars +} diff --git a/test/receiverevidencefix/go/src/merger.go b/test/receiverevidencefix/go/src/merger.go new file mode 100644 index 000000000..cfc332a98 --- /dev/null +++ b/test/receiverevidencefix/go/src/merger.go @@ -0,0 +1,19 @@ +package fzf + +// Merger has Get and Length too; an item's text field is never a Merger. +type Merger struct { + lists [][]int + count int +} + +func (mg *Merger) Get(idx int) int { + return mg.lists[0][idx] +} + +func (mg *Merger) Length() int { + return mg.count +} + +func (mg *Merger) First() int { + return mg.Get(0) +} diff --git a/test/receiverevidencefix/go/src/result.go b/test/receiverevidencefix/go/src/result.go new file mode 100644 index 000000000..410b26298 --- /dev/null +++ b/test/receiverevidencefix/go/src/result.go @@ -0,0 +1,21 @@ +package fzf + +import "example.com/fzfish/src/util" + +// buildResult reads an item's text through its typed field. +func buildResult(item *Item, b int) int { + numChars := item.text.Length() + last := item.text.Get(b - 1) + return numChars + int(last) +} + +// merged: a local whose type is spelled at its declaration. +func merged(n int) int { + m := &Merger{count: n} + return m.Length() +} + +// fromValue: a local declared with an in-repo type from another package. +func fromValue(c util.Chars) rune { + return c.Get(0) +} diff --git a/test/receiverevidencefix/go/src/tui/tcell.go b/test/receiverevidencefix/go/src/tui/tcell.go new file mode 100644 index 000000000..573ab66eb --- /dev/null +++ b/test/receiverevidencefix/go/src/tui/tcell.go @@ -0,0 +1,37 @@ +package tui + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/uniseg" +) + +var _screen tcell.Screen + +type FullscreenRenderer struct { + height int +} + +func (r *FullscreenRenderer) Size() int { + return r.height +} + +func (r *FullscreenRenderer) Runes() []rune { + return nil +} + +// MaxY asks the outside screen, not this renderer. +func (r *FullscreenRenderer) MaxY() int { + _, nlines := _screen.Size() + return nlines +} + +// Lines asks the renderer itself: the method receiver is the type. +func (r *FullscreenRenderer) Lines() int { + return r.Size() +} + +// Graphemes: a value returned by an outside package. +func Graphemes(input string) int { + gr := uniseg.NewGraphemes(input) + return len(gr.Runes()) +} diff --git a/test/receiverevidencefix/go/src/util/chars.go b/test/receiverevidencefix/go/src/util/chars.go new file mode 100644 index 000000000..c453c1461 --- /dev/null +++ b/test/receiverevidencefix/go/src/util/chars.go @@ -0,0 +1,18 @@ +package util + +// Chars holds an item's text. +type Chars struct { + runes []rune +} + +func (c *Chars) Get(i int) rune { + return c.runes[i] +} + +func (c *Chars) Length() int { + return len(c.runes) +} + +func (c *Chars) Runes() []rune { + return c.runes +} diff --git a/test/receiverevidencefix/java/src/main/java/app/Buffer.java b/test/receiverevidencefix/java/src/main/java/app/Buffer.java new file mode 100644 index 000000000..a62ac43d8 --- /dev/null +++ b/test/receiverevidencefix/java/src/main/java/app/Buffer.java @@ -0,0 +1,14 @@ +package app; + +public class Buffer { + protected StringBuilder data = new StringBuilder(); + + public void flush() { + data.setLength(0); + } + + public void write(String s) { + data.append(s); + flush(); + } +} diff --git a/test/receiverevidencefix/java/src/main/java/app/Exporter.java b/test/receiverevidencefix/java/src/main/java/app/Exporter.java new file mode 100644 index 000000000..f9f328848 --- /dev/null +++ b/test/receiverevidencefix/java/src/main/java/app/Exporter.java @@ -0,0 +1,12 @@ +package app; + +// An unrelated class that happens to define flush() and render(). +public class Exporter { + public void flush() { + System.out.println("exporter flush"); + } + + public String render(String s) { + return "<" + s + ">"; + } +} diff --git a/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java b/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java new file mode 100644 index 000000000..68c9953cc --- /dev/null +++ b/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java @@ -0,0 +1,8 @@ +package app; + +// Inherited: a bare flush() reaches Buffer.flush through the class cone. +public class FileBuffer extends Buffer { + public void sync() { + flush(); + } +} diff --git a/test/receiverevidencefix/java/src/main/java/app/Logger.java b/test/receiverevidencefix/java/src/main/java/app/Logger.java new file mode 100644 index 000000000..ffb5024f0 --- /dev/null +++ b/test/receiverevidencefix/java/src/main/java/app/Logger.java @@ -0,0 +1,20 @@ +package app; + +import static org.markup.Html.render; + +// A bare call is this.method() or a static import: Logger inherits flush() from an OUTSIDE class and +// imports render() from an outside package; Exporter and Buffer are unrelated. +public class Logger extends java.io.Writer { + public String line(String s) { + return render(s); + } + + public void close() { + flush(); + reset(); + } + + private void reset() { + System.out.println("logger reset"); + } +} diff --git a/test/receiverevidencefix/js/lib/application.js b/test/receiverevidencefix/js/lib/application.js new file mode 100644 index 000000000..dbba98fd1 --- /dev/null +++ b/test/receiverevidencefix/js/lib/application.js @@ -0,0 +1,43 @@ +'use strict' +// Bm: `ctx` is a request context, never an Application; this file defining Application is no evidence. +const proto = require('./context') +const Schemas = require('./schemas') + +class Application { + constructor () { + this.context = Object.create(proto) + this.bucket = new Schemas() + } + + onerror (err) { + console.error(err) + } + + handle (ctx) { + this.onerror(new Error('local')) + return respond(ctx) + } + + schemas () { + return this.bucket.listSchemas() + } + + listSchemas () { + return [] + } +} + +function respond (ctx) { + if (!ctx.writable) ctx.onerror(new Error('closed')) +} + +// Siblings of the receiver shape: optional chaining and a computed member, on the same context object. +function respondSafely (ctx) { + ctx?.onerror?.(new Error('maybe')) +} + +function respondComputed (ctx) { + ctx['onerror'](new Error('computed')) +} + +module.exports = Application diff --git a/test/receiverevidencefix/js/lib/context.js b/test/receiverevidencefix/js/lib/context.js new file mode 100644 index 000000000..6300780d6 --- /dev/null +++ b/test/receiverevidencefix/js/lib/context.js @@ -0,0 +1,6 @@ +'use strict' +module.exports = { + onerror (err) { + this.app.emit('error', err, this) + } +} diff --git a/test/receiverevidencefix/js/lib/handle.js b/test/receiverevidencefix/js/lib/handle.js new file mode 100644 index 000000000..779e5385e --- /dev/null +++ b/test/receiverevidencefix/js/lib/handle.js @@ -0,0 +1,15 @@ +'use strict' +// A member call on a route context reaches the user's handler, not this module's own function of that name. +function handler (request, reply) { + return reply +} + +function run (context, request, reply) { + return context.handler(request, reply) +} + +function wrapped (request, reply) { + return handler(request, reply) +} + +module.exports = { handler, run, wrapped } diff --git a/test/receiverevidencefix/js/lib/parser.js b/test/receiverevidencefix/js/lib/parser.js new file mode 100644 index 000000000..0e3964778 --- /dev/null +++ b/test/receiverevidencefix/js/lib/parser.js @@ -0,0 +1,8 @@ +'use strict' +function runParser (req, body) { + function done (error, parsed) { + req.body = error ? null : parsed + } + done(null, body) +} +module.exports = { runParser } diff --git a/test/receiverevidencefix/js/lib/reply.js b/test/receiverevidencefix/js/lib/reply.js new file mode 100644 index 000000000..b51cf28c7 --- /dev/null +++ b/test/receiverevidencefix/js/lib/reply.js @@ -0,0 +1,43 @@ +'use strict' +// Promise .then, stream .on, a parameter callback and a chained send, beside in-repo names they never reach. +function Reply (res) { + this.res = res +} + +Reply.prototype.send = function (payload) { + this.res.end(payload) + return this +} + +Reply.prototype.code = function (n) { + this.res.statusCode = n + return this +} + +Reply.prototype.then = function (fulfilled, rejected) { + return Promise.resolve(this).then(fulfilled, rejected) +} + +function awaitResult (result, cb) { + return result.then((v) => cb(null, v), cb) +} + +function pipePayload (payload, onEnd) { + payload.on('error', onEnd) + payload.on('end', onEnd) +} + +function writePayload (payload, done) { + payload.write('x') + done() +} + +function notFound (reply) { + reply.code(404).send('missing') +} + +function respondWith (reply, body) { + reply.send(body) +} + +module.exports = { Reply, awaitResult, pipePayload, writePayload, notFound, respondWith } diff --git a/test/receiverevidencefix/js/lib/request.js b/test/receiverevidencefix/js/lib/request.js new file mode 100644 index 000000000..6eb4531ee --- /dev/null +++ b/test/receiverevidencefix/js/lib/request.js @@ -0,0 +1,26 @@ +'use strict' +// A dynamic builtin receiver in the defining file: WeakMap .set/.get never reach the accessors below. +const kCache = Symbol('cache') + +function Request (headers) { + this.headersStore = headers + this[kCache] = new WeakMap() +} + +Object.defineProperties(Request.prototype, { + headers: { + get () { + return this.headersStore + }, + set (headers) { + this.headersStore = headers + } + } +}) + +Request.prototype.compile = function (schema, fn) { + this[kCache].set(schema, fn) + return this[kCache].get(schema) +} + +module.exports = Request diff --git a/test/receiverevidencefix/js/lib/response.js b/test/receiverevidencefix/js/lib/response.js new file mode 100644 index 000000000..43badf5e6 --- /dev/null +++ b/test/receiverevidencefix/js/lib/response.js @@ -0,0 +1,8 @@ +'use strict' +class Response { + append (field, val) { + this.headers = this.headers || {} + this.headers[field] = val + } +} +module.exports = Response diff --git a/test/receiverevidencefix/js/lib/routes.js b/test/receiverevidencefix/js/lib/routes.js new file mode 100644 index 000000000..7df0efc3d --- /dev/null +++ b/test/receiverevidencefix/js/lib/routes.js @@ -0,0 +1,19 @@ +'use strict' +// A router from an outside package and a builtin Set, beside the instance shorthands of those names. +const FindMyWay = require('find-my-way') + +const instance = { + all: function _all (url, fn) { return fn }, + delete: function _delete (url, fn) { return fn } +} + +function fallback (handler) { + const router = FindMyWay({ defaultRoute: handler }) + router.all('/*', handler) +} + +function forget (connections, conn) { + connections.delete(conn) +} + +module.exports = { instance, fallback, forget } diff --git a/test/receiverevidencefix/js/lib/schemas.js b/test/receiverevidencefix/js/lib/schemas.js new file mode 100644 index 000000000..fdec5ddbc --- /dev/null +++ b/test/receiverevidencefix/js/lib/schemas.js @@ -0,0 +1,11 @@ +'use strict' +class Schemas { + constructor () { + this.store = {} + } + + listSchemas () { + return Object.keys(this.store) + } +} +module.exports = Schemas diff --git a/test/receiverevidencefix/js/lib/search.js b/test/receiverevidencefix/js/lib/search.js new file mode 100644 index 000000000..f5e9dda64 --- /dev/null +++ b/test/receiverevidencefix/js/lib/search.js @@ -0,0 +1,8 @@ +'use strict' +// URLSearchParams.append beside an in-repo append method. +function encode (obj) { + const params = new URLSearchParams() + for (const k of Object.keys(obj)) params.append(k, obj[k]) + return params.toString() +} +module.exports = { encode } diff --git a/test/receiverevidencefix/js/lib/trailer.js b/test/receiverevidencefix/js/lib/trailer.js new file mode 100644 index 000000000..a91c73c10 --- /dev/null +++ b/test/receiverevidencefix/js/lib/trailer.js @@ -0,0 +1,9 @@ +'use strict' +// A closure named like Reply.prototype.send: only its own enclosing function can call it. +function sendTrailer (reply, value) { + function send (v) { + reply.res.addTrailers(v) + } + send(value) +} +module.exports = { sendTrailer } diff --git a/test/receiverevidencefix/js/lib/uses.js b/test/receiverevidencefix/js/lib/uses.js new file mode 100644 index 000000000..2b8cbd73a --- /dev/null +++ b/test/receiverevidencefix/js/lib/uses.js @@ -0,0 +1,19 @@ +'use strict' +// Near misses: receivers proven by construction, an import alias, and a relative module object. +const { Schemas: Store } = { Schemas: require('./schemas') } +const Schemas = require('./schemas') +const Application = require('./application') +const replies = require('./reply') + +function build () { + const app = new Application() + app.onerror(new Error('built')) + const s = new Schemas() + return s.listSchemas() +} + +function viaModule (reply) { + return replies.respondWith(reply, 'ok') +} + +module.exports = { build, viaModule, Store } diff --git a/test/receiverevidencefix/js/package.json b/test/receiverevidencefix/js/package.json new file mode 100644 index 000000000..f2793e83b --- /dev/null +++ b/test/receiverevidencefix/js/package.json @@ -0,0 +1 @@ +{ "name": "rxjs-fixture", "private": true } diff --git a/test/receiverevidencefix/js/test/server.test.js b/test/receiverevidencefix/js/test/server.test.js new file mode 100644 index 000000000..af0c6e9ff --- /dev/null +++ b/test/receiverevidencefix/js/test/server.test.js @@ -0,0 +1,7 @@ +'use strict' +// A test double whose `on` no production file can reach. +const fakeServer = { + on () {}, + listen () {} +} +module.exports = fakeServer diff --git a/test/receiverevidencefix/kt/src/app/Exporter.kt b/test/receiverevidencefix/kt/src/app/Exporter.kt new file mode 100644 index 000000000..497cd9bcd --- /dev/null +++ b/test/receiverevidencefix/kt/src/app/Exporter.kt @@ -0,0 +1,9 @@ +package app + +class Exporter { + fun flush() { + println("exporter flush") + } + + fun render(s: String): String = "<$s>" +} diff --git a/test/receiverevidencefix/kt/src/app/Logger.kt b/test/receiverevidencefix/kt/src/app/Logger.kt new file mode 100644 index 000000000..09d27df7a --- /dev/null +++ b/test/receiverevidencefix/kt/src/app/Logger.kt @@ -0,0 +1,23 @@ +package app + +fun render(s: String): String = "[$s]" + +open class Base { + fun flush() { + println("base flush") + } +} + +// A bare call is this.method() or a top-level function; never an unrelated class's method. +class Logger : Base() { + fun line(s: String): String = render(s) + + fun close() { + flush() + reset() + } + + private fun reset() { + println("reset") + } +} diff --git a/test/receiverevidencefix/kt/src/app/Plain.kt b/test/receiverevidencefix/kt/src/app/Plain.kt new file mode 100644 index 000000000..8e08b4399 --- /dev/null +++ b/test/receiverevidencefix/kt/src/app/Plain.kt @@ -0,0 +1,8 @@ +package app + +// Plain inherits flush() from an OUTSIDE class: Base and Exporter are unrelated. +class Plain : java.io.Writer() { + fun finish() { + flush() + } +} diff --git a/test/receiverevidencefix/py/src/tui/__init__.py b/test/receiverevidencefix/py/src/tui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/receiverevidencefix/py/src/tui/app.py b/test/receiverevidencefix/py/src/tui/app.py new file mode 100644 index 000000000..14f2823ef --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/app.py @@ -0,0 +1,25 @@ +from .screen import Screen + + +class App: + """A non-self receiver in the class's own file: the file defining App is no evidence.""" + + def __init__(self): + self.screen = Screen() + + def refresh(self): + return False + + def get_screen(self, name): + return Screen() + + def switch_mode(self, name): + mode_screen = self.get_screen(name) + mode_screen.refresh() + + def repaint(self): + self.refresh() + self.screen.refresh() + + def repaint_screen(self): + self.screen.refresh() diff --git a/test/receiverevidencefix/py/src/tui/byte_stream.py b/test/receiverevidencefix/py/src/tui/byte_stream.py new file mode 100644 index 000000000..d28f94746 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/byte_stream.py @@ -0,0 +1,3 @@ +class ByteStreamParser: + def feed(self, data): + return list(data) diff --git a/test/receiverevidencefix/py/src/tui/callback.py b/test/receiverevidencefix/py/src/tui/callback.py new file mode 100644 index 000000000..37475f032 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/callback.py @@ -0,0 +1,15 @@ +import asyncio + + +def invoke(callback, log_slow): + """An asyncio loop's call_later and its TimerHandle's cancel, beside in-repo methods of those names.""" + handle = asyncio.get_running_loop().call_later(5.0, log_slow) + try: + return callback() + finally: + handle.cancel() + + +def schedule(pump, callback): + """A parameter receiver with one in-repo candidate: name-only, true here, and must stay visible.""" + return pump.call_later(1.0, callback) diff --git a/test/receiverevidencefix/py/src/tui/compositor.py b/test/receiverevidencefix/py/src/tui/compositor.py new file mode 100644 index 000000000..28e8d0b95 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/compositor.py @@ -0,0 +1,10 @@ +class Compositor: + def arrange_root(self, root): + placed = [] + + def add_widget(widget): + placed.append(widget) + + for child in root: + add_widget(child) + return placed diff --git a/test/receiverevidencefix/py/src/tui/content.py b/test/receiverevidencefix/py/src/tui/content.py new file mode 100644 index 000000000..d9f6f53cd --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/content.py @@ -0,0 +1,12 @@ +class Content: + def __init__(self, text=""): + self.text = text + + def append(self, other): + return Content(self.text + str(other)) + + def split(self, sep=" "): + return [Content(t) for t in self.text.split(sep)] + + def join(self, parts): + return Content(self.text.join(str(p) for p in parts)) diff --git a/test/receiverevidencefix/py/src/tui/css/__init__.py b/test/receiverevidencefix/py/src/tui/css/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/receiverevidencefix/py/src/tui/css/parse.py b/test/receiverevidencefix/py/src/tui/css/parse.py new file mode 100644 index 000000000..0cccc679b --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/css/parse.py @@ -0,0 +1,3 @@ +def parse(css): + """The module function the stylesheet imports.""" + return [rule.strip() for rule in css.split(";") if rule.strip()] diff --git a/test/receiverevidencefix/py/src/tui/css/styles.py b/test/receiverevidencefix/py/src/tui/css/styles.py new file mode 100644 index 000000000..7fa133151 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/css/styles.py @@ -0,0 +1,20 @@ +class StylesBase: + def __init__(self): + self._rules = {} + + def get(self, key, default=None): + return self._rules.get(key, default) + + def keys(self): + return list(self._rules) + + def items(self): + return list(self._rules.items()) + + def animate(self, attribute, value): + self._rules[attribute] = value + + +class Styles(StylesBase): + def copy(self): + return Styles() diff --git a/test/receiverevidencefix/py/src/tui/css/stylesheet.py b/test/receiverevidencefix/py/src/tui/css/stylesheet.py new file mode 100644 index 000000000..70d068fd4 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/css/stylesheet.py @@ -0,0 +1,44 @@ +from .parse import parse +from .styles import Styles + + +class Stylesheet: + """Builtin dict/set receivers in a file that imports Styles: the import is no evidence about them.""" + + def __init__(self): + self.source = {} + self._invalid_css = set() + self.styles = Styles() + + def update(self, other): + self.source.update(other) + + def read(self, path): + return open(path).read() + + def reparse(self, other): + cache = {} + for key, value in self.source.items(): + cache[key] = value + self._invalid_css.update(other._invalid_css) + rules_map = {"a": 1} + names = sorted(rules_map.keys()) + return cache.get(names[0]) + + def refresh_rules(self, animator): + animator.animate(self, "opacity") + + def own(self, other): + self.update(other) + return self.styles.get("color") + + def parse_rules(self, css): + return list(parse(css)) + + @classmethod + def blank(cls): + return cls.default_rules() + + @classmethod + def default_rules(cls): + return [] diff --git a/test/receiverevidencefix/py/src/tui/drivers/__init__.py b/test/receiverevidencefix/py/src/tui/drivers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/receiverevidencefix/py/src/tui/drivers/linux.py b/test/receiverevidencefix/py/src/tui/drivers/linux.py new file mode 100644 index 000000000..5d4e9b411 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/drivers/linux.py @@ -0,0 +1,20 @@ +import os + +from ..xterm_parser import XTermParser + + +class Driver: + def run_input_thread(self, fd): + """Aliases bound in the method and called from a nested def: an outside function and a constructed + object's bound method.""" + parser = XTermParser() + feed = parser.feed + read = os.read + + def process_events(ready): + for _ in ready: + data = read(fd, 1024) + for event in feed(data): + print(event) + + process_events([fd]) diff --git a/test/receiverevidencefix/py/src/tui/geometry.py b/test/receiverevidencefix/py/src/tui/geometry.py new file mode 100644 index 000000000..a6becefaf --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/geometry.py @@ -0,0 +1,7 @@ +class Region: + def __init__(self, x, y): + self.x = x + self.y = y + + def split(self, cut): + return Region(self.x, cut), Region(cut, self.y) diff --git a/test/receiverevidencefix/py/src/tui/markup.py b/test/receiverevidencefix/py/src/tui/markup.py new file mode 100644 index 000000000..2f56f22eb --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/markup.py @@ -0,0 +1,8 @@ +class MarkupParser: + def parse(self, text): + return [text] + + +class TokenParser: + def parse(self, text): + return text.split() diff --git a/test/receiverevidencefix/py/src/tui/message_pump.py b/test/receiverevidencefix/py/src/tui/message_pump.py new file mode 100644 index 000000000..c3b7cbe35 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/message_pump.py @@ -0,0 +1,3 @@ +class MessagePump: + def call_later(self, delay, callback): + return (delay, callback) diff --git a/test/receiverevidencefix/py/src/tui/reactive.py b/test/receiverevidencefix/py/src/tui/reactive.py new file mode 100644 index 000000000..5728013e7 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/reactive.py @@ -0,0 +1,15 @@ +from .content import Content + + +def watch(watchers, name, node, callback): + """list.append / str.split on plain values, in a file that imports Content.""" + watcher_list = watchers.setdefault(name, []) + watcher_list.append((node, callback)) + + +def toggle(obj, toggle_class: str): + obj.set_class(True, *toggle_class.split()) + + +def label() -> Content: + return Content("label") diff --git a/test/receiverevidencefix/py/src/tui/screen.py b/test/receiverevidencefix/py/src/tui/screen.py new file mode 100644 index 000000000..82cbe7785 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/screen.py @@ -0,0 +1,11 @@ +class Screen: + def refresh(self): + return True + + def collect(self, nodes): + """A local alias of a list's bound append beside another class's nested add_widget.""" + widgets = [] + add_widget = widgets.append + for node in nodes: + add_widget(node) + return widgets diff --git a/test/receiverevidencefix/py/src/tui/uses.py b/test/receiverevidencefix/py/src/tui/uses.py new file mode 100644 index 000000000..6488425b2 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/uses.py @@ -0,0 +1,24 @@ +from .css.styles import Styles +from .css.stylesheet import Stylesheet as Sheet +from .css.stylesheet import Stylesheet + + +class Holder: + """Near misses: constructor-assigned field, typed parameter, construction, import alias.""" + + def __init__(self): + self.sheet = Stylesheet() + + def sync(self, other): + self.sheet.update(other) + + +def apply(styles: Styles): + return styles.animate("color", 1) + + +def build(path): + sheet = Stylesheet() + sheet.update({}) + alias = Sheet() + return alias.read(path) diff --git a/test/receiverevidencefix/py/src/tui/worker.py b/test/receiverevidencefix/py/src/tui/worker.py new file mode 100644 index 000000000..5eb05641d --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/worker.py @@ -0,0 +1,3 @@ +class Worker: + def cancel(self): + return True diff --git a/test/receiverevidencefix/py/src/tui/xterm_parser.py b/test/receiverevidencefix/py/src/tui/xterm_parser.py new file mode 100644 index 000000000..497f0f226 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/xterm_parser.py @@ -0,0 +1,8 @@ +class Parser: + def feed(self, data): + return list(data) + + +class XTermParser(Parser): + def feed(self, data): + return [chr(b) for b in data] diff --git a/test/receiverevidencefix/py/src/web/__init__.py b/test/receiverevidencefix/py/src/web/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/receiverevidencefix/py/src/web/datastructures.py b/test/receiverevidencefix/py/src/web/datastructures.py new file mode 100644 index 000000000..617f92cf7 --- /dev/null +++ b/test/receiverevidencefix/py/src/web/datastructures.py @@ -0,0 +1,3 @@ +class UploadFile: + def write(self, data): + return len(data) diff --git a/test/receiverevidencefix/py/src/web/errors.py b/test/receiverevidencefix/py/src/web/errors.py new file mode 100644 index 000000000..d1ea032bd --- /dev/null +++ b/test/receiverevidencefix/py/src/web/errors.py @@ -0,0 +1,9 @@ +class ServerErrorMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + async def _send(message): + await send(message) + + await self.app(scope, receive, _send) diff --git a/test/receiverevidencefix/py/src/web/formparsers.py b/test/receiverevidencefix/py/src/web/formparsers.py new file mode 100644 index 000000000..fcc93c710 --- /dev/null +++ b/test/receiverevidencefix/py/src/web/formparsers.py @@ -0,0 +1,8 @@ +import multipart + + +def parse_form(callbacks, chunks): + """A parser object built by an outside package beside UploadFile.write.""" + parser = multipart.QuerystringParser(callbacks) + for chunk in chunks: + parser.write(chunk) diff --git a/test/receiverevidencefix/py/src/web/requests.py b/test/receiverevidencefix/py/src/web/requests.py new file mode 100644 index 000000000..804e2f563 --- /dev/null +++ b/test/receiverevidencefix/py/src/web/requests.py @@ -0,0 +1,10 @@ +class HTTPConnection: + """self._send holds the ASGI send callable passed in; the nested _send elsewhere is never it.""" + + def __init__(self, scope, receive, send): + self.scope = scope + self._receive = receive + self._send = send + + async def send_json(self, message): + await self._send(message) diff --git a/test/receiverevidencefix/rb/lib/app/exporter.rb b/test/receiverevidencefix/rb/lib/app/exporter.rb new file mode 100644 index 000000000..7a2ac6a86 --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/exporter.rb @@ -0,0 +1,11 @@ +module App + class Exporter + def flush + puts "exporter flush" + end + + def render(s) + "<#{s}>" + end + end +end diff --git a/test/receiverevidencefix/rb/lib/app/helpers.rb b/test/receiverevidencefix/rb/lib/app/helpers.rb new file mode 100644 index 000000000..df0b884bc --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/helpers.rb @@ -0,0 +1,7 @@ +module App + module Helpers + def stamp(s) + "#{Time.now} #{s}" + end + end +end diff --git a/test/receiverevidencefix/rb/lib/app/logger.rb b/test/receiverevidencefix/rb/lib/app/logger.rb new file mode 100644 index 000000000..821607f4e --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/logger.rb @@ -0,0 +1,30 @@ +require_relative "helpers" + +module App + class Base + def flush() + puts "base flush" + end + end + + # A bare call is self.method: its own class, a superclass, an included module. Never an unrelated class. + class Logger < Base + include Helpers + + def line(s) + stamp(s) + end + + def close + flush() + reset() + top_level_note("closed") + end + + private + + def reset() + puts "reset" + end + end +end diff --git a/test/receiverevidencefix/rb/lib/app/notes.rb b/test/receiverevidencefix/rb/lib/app/notes.rb new file mode 100644 index 000000000..ea33adc8a --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/notes.rb @@ -0,0 +1,4 @@ +# A top-level def is a private method of Object: every bare call can reach it. +def top_level_note(s) + warn s +end diff --git a/test/receiverevidencefix/rb/lib/app/plain.rb b/test/receiverevidencefix/rb/lib/app/plain.rb new file mode 100644 index 000000000..4ee559113 --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/plain.rb @@ -0,0 +1,11 @@ +require "logger" + +module App + # Plain inherits from an OUTSIDE class (the stdlib Logger): a bare render or flush never reaches Exporter or Base. + class Plain < ::Logger + def finish(s) + flush() + render(s) + end + end +end diff --git a/test/receiverevidencefix/swift/Sources/App/Exporter.swift b/test/receiverevidencefix/swift/Sources/App/Exporter.swift new file mode 100644 index 000000000..f10887387 --- /dev/null +++ b/test/receiverevidencefix/swift/Sources/App/Exporter.swift @@ -0,0 +1,9 @@ +class Exporter { + func flush() { + print("exporter flush") + } + + func render(_ s: String) -> String { + return "<" + s + ">" + } +} diff --git a/test/receiverevidencefix/swift/Sources/App/Logger.swift b/test/receiverevidencefix/swift/Sources/App/Logger.swift new file mode 100644 index 000000000..86aad001c --- /dev/null +++ b/test/receiverevidencefix/swift/Sources/App/Logger.swift @@ -0,0 +1,25 @@ +func render(_ s: String) -> String { + return "[" + s + "]" +} + +class Base { + func flush() { + print("base flush") + } +} + +// A bare call is self.method() or a free function; never an unrelated class's method. +class Logger: Base { + func line(_ s: String) -> String { + return render(s) + } + + func close() { + flush() + reset() + } + + private func reset() { + print("reset") + } +} diff --git a/test/receiverevidencefix/swift/Sources/App/Plain.swift b/test/receiverevidencefix/swift/Sources/App/Plain.swift new file mode 100644 index 000000000..dbe844a89 --- /dev/null +++ b/test/receiverevidencefix/swift/Sources/App/Plain.swift @@ -0,0 +1,8 @@ +import Foundation + +// Plain inherits from an OUTSIDE class: a bare flush() never reaches Base or Exporter. +class Plain: FileHandle { + func finish() { + flush() + } +} diff --git a/test/receiverevidencefix/ts/package.json b/test/receiverevidencefix/ts/package.json new file mode 100644 index 000000000..06bbcf967 --- /dev/null +++ b/test/receiverevidencefix/ts/package.json @@ -0,0 +1 @@ +{ "name": "rxts-fixture", "private": true } diff --git a/test/receiverevidencefix/ts/src/cache.ts b/test/receiverevidencefix/ts/src/cache.ts new file mode 100644 index 000000000..5a1a01d31 --- /dev/null +++ b/test/receiverevidencefix/ts/src/cache.ts @@ -0,0 +1,11 @@ +export class Cache { + private entries: Record = {} + + get(key: string): unknown { + return this.entries[key] + } + + set(key: string, value: unknown): void { + this.entries[key] = value + } +} diff --git a/test/receiverevidencefix/ts/src/context.ts b/test/receiverevidencefix/ts/src/context.ts new file mode 100644 index 000000000..35474c3d1 --- /dev/null +++ b/test/receiverevidencefix/ts/src/context.ts @@ -0,0 +1,16 @@ +export class Context { + private vars = new Map() + req: { raw: Request } + + constructor(raw: Request) { + this.req = { raw } + } + + get(key: string): unknown { + return this.vars.get(key) + } + + set(key: string, value: unknown): void { + this.vars.set(key, value) + } +} diff --git a/test/receiverevidencefix/ts/src/middleware/alias.ts b/test/receiverevidencefix/ts/src/middleware/alias.ts new file mode 100644 index 000000000..133969693 --- /dev/null +++ b/test/receiverevidencefix/ts/src/middleware/alias.ts @@ -0,0 +1,14 @@ +import { Context as Ctx } from '../context' +import { TrieRouter } from '../router/trie' + +// Near misses: construction through an import alias, and a constructed local. +export function remember(raw: Request): void { + const c = new Ctx(raw) + c.set('seen', true) +} + +export function single(): string[] { + const r = new TrieRouter() + r.add('GET', '/', 'root') + return r.match('GET', '/') +} diff --git a/test/receiverevidencefix/ts/src/middleware/auth.ts b/test/receiverevidencefix/ts/src/middleware/auth.ts new file mode 100644 index 000000000..c2cb27873 --- /dev/null +++ b/test/receiverevidencefix/ts/src/middleware/auth.ts @@ -0,0 +1,12 @@ +import { Context } from '../context' + +// Bm: a WHATWG Headers .get on the raw request, beside Context.get in the imported class. +export function bearer(ctx: Context, headerName: string): string | null { + const credentials = ctx.req.raw.headers.get(headerName) + return typeof credentials === 'string' ? credentials : null +} + +// Near miss: an annotated parameter IS evidence. +export function readUser(c: Context): unknown { + return c.get('user') +} diff --git a/test/receiverevidencefix/ts/src/router/router.ts b/test/receiverevidencefix/ts/src/router/router.ts new file mode 100644 index 000000000..1b92ea6e1 --- /dev/null +++ b/test/receiverevidencefix/ts/src/router/router.ts @@ -0,0 +1,4 @@ +export interface Router { + add(method: string, path: string, handler: T): void + match(method: string, path: string): T[] +} diff --git a/test/receiverevidencefix/ts/src/router/smart.ts b/test/receiverevidencefix/ts/src/router/smart.ts new file mode 100644 index 000000000..cfff1aad4 --- /dev/null +++ b/test/receiverevidencefix/ts/src/router/smart.ts @@ -0,0 +1,25 @@ +import type { Router } from './router' + +// Bm: `router` walks the inner routers; SmartRouter's own add is never what it calls. +export class SmartRouter implements Router { + private routers: Router[] = [] + private pending: [string, string, T][] = [] + + constructor(init: { routers: Router[] }) { + this.routers = init.routers + } + + add(method: string, path: string, handler: T): void { + this.pending.push([method, path, handler]) + } + + match(method: string, path: string): T[] { + for (const router of this.routers) { + for (const p of this.pending) { + router.add(...p) + } + return router.match(method, path) + } + return [] + } +} diff --git a/test/receiverevidencefix/ts/src/router/trie.ts b/test/receiverevidencefix/ts/src/router/trie.ts new file mode 100644 index 000000000..f01af971e --- /dev/null +++ b/test/receiverevidencefix/ts/src/router/trie.ts @@ -0,0 +1,13 @@ +import type { Router } from './router' + +export class TrieRouter implements Router { + private routes: [string, string, T][] = [] + + add(method: string, path: string, handler: T): void { + this.routes.push([method, path, handler]) + } + + match(method: string, path: string): T[] { + return this.routes.filter((r) => r[0] === method && r[1] === path).map((r) => r[2]) + } +} diff --git a/test/regression.sh b/test/regression.sh index 6ebb7fd68..8816037b6 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From c8e2a9c84aa7faf567b2cf803b325faebc4e3393 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 09:18:26 -0400 Subject: [PATCH 002/229] =?UTF-8?q?test:=20receiverevidencecheck=20?= =?UTF-8?q?=E2=80=94=20gates=20review=20changes=20before=20the=20fix=20lan?= =?UTF-8?q?ds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader's exit status is now checked at every call site: a crash or a non-numeric defs= FAILs the arm instead of reading as "no rows" (a planted defs="x" K arm pins it). App.screen becomes a property, so self.screen carries no evidence and repaint doubles as the proven-plus-name-only dedup near miss. The --for HOP blocks () are gated for encode, respond, pipePayload and an object-literal module's back (this.ctx.get). Evidence that must RESOLVE is pinned exactly: a constructor-assigned JS field, Go typed struct fields and embedded fields. notFound's reply.code()/send() are marked like respondWith's send(). --impact inherits the hedge at d=2 (viaModule, serve) unless an all-proven path reaches the row (both, through new Reply().send). The legend of every surface, and the MCP tool description or answer, must define via="name" with what it does NOT mean. Sibling shapes: a JS alias and a destructured binding of ctx.onerror, super.m() in JS and Java, class-name receivers in JS and Python, globals Reflect/Atomics beside in-repo get/add, a Python outside from-import, a Go field or local named log beside the in-repo log package. Repros of earlier graded false callees: exec Cmd.Output, Date#getTime, list.append (red), with fmt.Errorf, strings.Split and the global Response pinned clean. Arms already green on the pre-change binary are labelled as guards. Co-Authored-By: Claude Opus 5.5 --- test/receiverevidencecheck.sh | 508 +++++++++++++----- test/receiverevidencefix/README.md | 27 +- test/receiverevidencefix/go/go.mod | 1 + test/receiverevidencefix/go/src/cmd/run.go | 8 + test/receiverevidencefix/go/src/gotool.go | 37 ++ .../go/src/internal/log/log.go | 8 + test/receiverevidencefix/go/src/service.go | 19 + .../java/src/main/java/app/FileBuffer.java | 4 + .../receiverevidencefix/js/lib/application.js | 21 + .../js/lib/delegate/request.js | 8 + .../js/lib/delegate/response.js | 11 + test/receiverevidencefix/js/lib/listing.js | 10 + test/receiverevidencefix/js/lib/meta.js | 19 + test/receiverevidencefix/js/lib/mirror.js | 8 + test/receiverevidencefix/js/lib/reflect.js | 6 + test/receiverevidencefix/js/lib/reply.js | 13 +- test/receiverevidencefix/js/lib/uses.js | 6 +- test/receiverevidencefix/py/src/tui/app.py | 7 +- .../receiverevidencefix/py/src/tui/listing.py | 15 + test/receiverevidencefix/py/src/tui/uses.py | 5 + .../py/src/web/jsonutil.py | 7 + test/receiverevidencefix/py/src/web/render.py | 7 + test/receiverevidencefix/ts/src/atomics.ts | 8 + test/receiverevidencefix/ts/src/http.ts | 30 ++ test/receiverevidencefix/ts/src/response.ts | 4 + 25 files changed, 651 insertions(+), 146 deletions(-) create mode 100644 test/receiverevidencefix/go/src/cmd/run.go create mode 100644 test/receiverevidencefix/go/src/gotool.go create mode 100644 test/receiverevidencefix/go/src/internal/log/log.go create mode 100644 test/receiverevidencefix/go/src/service.go create mode 100644 test/receiverevidencefix/js/lib/delegate/request.js create mode 100644 test/receiverevidencefix/js/lib/delegate/response.js create mode 100644 test/receiverevidencefix/js/lib/listing.js create mode 100644 test/receiverevidencefix/js/lib/meta.js create mode 100644 test/receiverevidencefix/js/lib/mirror.js create mode 100644 test/receiverevidencefix/js/lib/reflect.js create mode 100644 test/receiverevidencefix/py/src/tui/listing.py create mode 100644 test/receiverevidencefix/py/src/web/jsonutil.py create mode 100644 test/receiverevidencefix/py/src/web/render.py create mode 100644 test/receiverevidencefix/ts/src/atomics.ts create mode 100644 test/receiverevidencefix/ts/src/http.ts create mode 100644 test/receiverevidencefix/ts/src/response.ts diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index 29e0e4f2e..a70075bf3 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -14,16 +14,21 @@ # a stored ASGI callable `self._send()` drawn to another class's nested `_send`; a constructor-assigned # field `this.bucket.listSchemas()` drawn to the caller's own class; a loop variable over inner routers # drawn to SmartRouter.add; `ctx.req.raw.headers.get()` drawn to Context.get because the file imports -# Context; a member call `context.handler()` drawn to a same-named FREE function. +# Context; a member call `context.handler()` drawn to a same-named FREE function; an object-literal +# module's `this.ctx.get()` drawn to that module's own `get`; a Go struct field or local named like a +# package (`s.log.Errorf`) drawn to the in-repo `log.Errorf`. # (Bd) a builtin/library object behind a variable, in a file that names the class: WeakMap `.set/.get` → the # same file's accessors, Promise `.then` → Reply.prototype.then, a stream's `.on` → a TEST double, # `dict.get/keys/items` → StylesBase in a file that imports Styles, `list.append`/`str.split` → # Content.append/split, URLSearchParams `.append`, Set `.delete`, an outside router's `.all`, an asyncio -# handle's `.cancel`, an outside parser's `.write`, an outside tcell screen's `.Size`. +# handle's `.cancel`, an outside parser's `.write`, an outside tcell screen's `.Size`, the globals +# `Reflect.get` / `Atomics.add` beside in-repo `get` / `add`, a Python outside from-import +# (`from json import dumps as stringify`) beside in-repo `stringify` / `dumps`. # (Bp) a parameter or local callable bound to a def elsewhere: `done()` (a parameter) → another file's # closure `done`; `read = os.read; read( fd )` → Stylesheet.read and `feed = parser.feed` → the base # class's feed when the parser is a constructed XTermParser (the rows FE-A's local-Keep rule restored); -# `add_widget = widgets.append; add_widget( n )` → another class's nested add_widget. +# `add_widget = widgets.append; add_widget( n )` → another class's nested add_widget; a JS alias +# `const onerror = ctx.onerror` and a destructured `const { onerror } = ctx`, then `onerror( e )`. # (Bi) an implicit receiver (Java, Kotlin, C#, C++, Swift, Ruby): a bare `render()` / `flush()` inside a class # bound to an UNRELATED class's method, though the language reaches only the class's own members, its # superclasses and mixins, a free/top-level function, or an import. @@ -36,7 +41,10 @@ # — never a legend-only hedge, never a silent drop. Every surface shows the same row with the same hedge: # --callees, --callers, --impact, --path, --connect, --expand , --for , and MCP find_symbol / # find_referencing_symbols. A wrong candidate may also disappear (resolved elsewhere, or counted external/ -# unresolved), but a TRUE target must stay visible, hedged or not. +# unresolved), but a TRUE target must stay visible, hedged or not. A SINGLE by-name candidate is hedged too: +# one candidate is not evidence (ruling D1). An --impact row at d>=2 that no all-proven path reaches inherits +# the hedge; a row with some all-proven path stays plain (ruling D3). Every surface's --legend=full (and the MCP +# tool description or answer) defines via="name" and says what it does NOT mean. # # PREDICATES (each over one answer; an answer about no symbol is NOROOT and never a pass; rows compare # (kind, name, file[:line]) — the line only where a spec carries one): @@ -47,45 +55,65 @@ # marked — the named row is present WITH the hedge (a name-only true edge is kept and labelled) # # ARMS (one fixture root per language under test/receiverevidencefix/; each root is indexed on its own). -# (J) JS: Bm ctx.onerror (plain, optional-chained), constructor-assigned field, member call → free function; -# Bd WeakMap/Promise/stream/router/Set/URLSearchParams; Bp a `done` parameter. Near misses: this.onerror, -# same-file bare call, relative module receiver, a closure called in its own function, construction in -# scope (`new Application()` / `new Schemas()` decide between same-named methods), a chained name-only -# call kept visible; the name-only `reply.send()` on a parameter is kept and marked. -# (T) TS: Bm a loop variable over Router[] → SmartRouter.add; WHATWG Headers.get → Context.get/Cache.get. -# Near misses: an annotated parameter, construction through an import alias, a constructed local. +# (J) JS: Bm ctx.onerror (plain, optional-chained, aliased, destructured), an object-literal module's +# this.ctx.get, member call → free function; Bd WeakMap/Promise/stream/router/Set/URLSearchParams, the +# globals Reflect/Atomics; Bp a `done` parameter. Evidence that must RESOLVE: a constructor-assigned field +# (this.bucket → Schemas.listSchemas). Near misses: this.onerror, same-file bare call, relative module +# receiver (also one bound to the name `Reflect`), a closure called in its own function, construction +# in scope (`new Application()` / `new Schemas()` decide between same-named methods), super.m(), a +# class-name (static) receiver; the name-only `reply.send()` / `reply.code()` on a parameter are kept +# and marked. +# (T) TS: Bm a loop variable over Router[] → SmartRouter.add; WHATWG Headers.get → Context.get/Cache.get; +# the global Atomics.add. Near misses: an annotated parameter, construction through an import alias, a +# constructed local. # (P) Python: Bd dict/set/list/str in a file that imports the class; an argument's .animate; asyncio handle; -# an outside package's parser; Bm self._send → a nested _send, a non-self receiver in the class's own -# file (mode_screen.refresh, self.screen.refresh); Bp read = os.read, feed = parser.feed (base class), -# add_widget = widgets.append. Near misses: self.update, a constructor-assigned field (+ cone), -# a typed parameter (+ cone), construction, an import alias, an imported function beside same-named -# methods, cls.method(), the XTermParser.feed the alias really reaches; a name-only parameter call -# kept and marked. -# (G) Go: Bm typed struct field → Merger.Get/Length (the true util.Chars rows visible), a typed parameter, a -# typed local through an aliased import, an embedded field's promoted method; Bd an outside screen's -# Size, an outside value's Runes. Near misses: a pointer-literal local, the method receiver itself. +# an outside package's parser; an outside from-import (plain and aliased); Bm self._send → a nested +# _send, a non-self receiver in the class's own file (mode_screen.refresh, the property self.screen); +# Bp read = os.read, feed = parser.feed (base class and an unrelated class), add_widget = +# widgets.append. Near misses: self.update, a constructor-assigned field (+ cone), a typed parameter +# (+ cone), construction, an import alias, an imported function beside same-named methods, +# cls.method(), a class-name receiver (Styles.copy), the XTermParser.feed the alias really reaches, a +# proven and a name-only site to the same method (the merged row stays plain); a name-only parameter +# call kept and marked. +# (G) Go: evidence that must RESOLVE (ruling D4): a typed struct field, a typed parameter, a typed local +# through an aliased import, an embedded field's promoted method — each exactly the util.Chars row, never +# Merger.Get/Length; Bd an outside screen's Size, an outside value's Runes, a field or local named `log` +# of an outside type beside the in-repo log.Errorf. Near misses: a pointer-literal local, the method +# receiver itself, a real import of the in-repo log package. # (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE # the tree (or that imports the name from outside) never proves an unrelated class's method. Near -# misses: own members (private too), an in-repo superclass's member (the cone), a free / top-level -# function, a Ruby included module and a Ruby top-level def. +# misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), +# a free / top-level function, a Ruby included module and a Ruby top-level def. # (H) propagation and parity: the name-only witness `respondWith → reply.send()` is marked in --callees, # --callers, --impact (d=1), --path, --connect, --expand , --for , MCP find_symbol and # find_referencing_symbols; the false witness `respond → ctx.onerror()` is absent-or-marked on every -# one of them; and the hedge bit is VALUE-EQUAL between CLI --callees and MCP calls, CLI --callers and -# MCP calledBy, and --callees and --expand , over a set of selectors. +# one of them; the --for HOP blocks () never prove encode → Response.append, +# respond → Application.onerror, pipePayload → the test double's on, back → its own module's get; +# --impact at d>=2 inherits the hedge (viaModule through respondWith → send; serve through +# respond → onerror) unless some all-proven path reaches the row (both: direct → new Reply().send); +# the legend of every surface defines via="name" with a NOT sentence; and the hedge bit is +# VALUE-EQUAL between CLI --callees and MCP calls, CLI --callers and MCP calledBy, and --callees and +# --expand , over a set of selectors. +# (O) graded false callees seen on an earlier binary, as minimal repros: Go exec Cmd.Output → an in-repo +# Output method (fmt.Errorf / strings.Split pinned clean), TS Date#getTime → an in-repo getTime (the +# global Response pinned clean), Python list.append on a list literal → Content.append; each on +# --callees and its --for hop block, with constructed-local near misses. # (C) conservation: each root's census dispositions still sum to calls= with unaccounted=0 (no silent drop). # (K) the predicates can fail: a planted unhedged false row fails notproven, a planted hedged row passes it and # fails proven, marked rejects an unhedged row, a rootless or not-found answer is NOROOT, the MCP reader -# reads the hedge, and a non-zero or missing exit status fails the arm. +# reads the hedge, a non-zero or missing exit status fails the arm, a parser crash (a malformed defs=) +# fails the arm, and a hop reader never reads another hop's . +# GUARDS: arms marked `# guard` were already green on the pre-change binary; they pin a shape the change must +# not break and are not evidence of the fix. # # FLOORS — named, not gated here: # * Objective-C (a method is only ever sent `[recv msg]`; a bare call is a C function — FE-A's ladder), PHP, Lua, # Zig, Dart, Elixir: no arm. # * a computed member `ctx['onerror']()` records no call at all (pinned notproven, trivially). -# * return-type inference (`mode_screen = self.get_screen(); mode_screen.refresh()` → Screen.refresh) is not -# required: only the false single row is gated. -# * --impact rows at d>=2 reached only through a hedged edge: presence is gated, their hedge is not. -# * --uses / --dead-code / --safe-delete and F2's --for hop expansion beyond the block: not gated here. +# * return-type inference (`mode_screen = self.get_screen(); mode_screen.refresh()` → Screen.refresh, or a +# property's return) is not required: only the false single row is gated. +# * --uses / --dead-code / --safe-delete, and --for hop blocks of symbols other than the four named in (H): not +# gated here. # * a Python `cls()` construction inside a classmethod is polymorphic (a subclass's override is a legitimate # candidate): no arm. # @@ -113,11 +141,17 @@ rw(){ local r="$1"; shift; ( cd "$CORPUS/$r" && "$BIN" . --no-cache "$@" 2>/dev/ # ── the one reader every predicate shares ───────────────────────────────────────────────────────────────────── # parse.py MODE FILE [ARGS] — prints one line per row; NOROOT when the document has no answer about a symbol. +# It EXITS NON-ZERO on malformed input (a non-numeric defs=/to_defs=/reachable=) or on any crash, and every call +# site goes through P, which FAILs the arm on a non-zero exit (a crash never reads as "no rows"). # rows rows of a callees/callers/impact/path answer: "t n path line H d" (H = name | -, d = depth or -) -# ecalls rows inside (all of them, or only those inside the / body named ARG for --for): -# "n line H" +# ecalls rows inside (all of them, or only those inside the body named ARG): "n line H" +# (a joined row `l="4,8"` prints one line per line number) +# hcalls rows inside the --for HOP block : "n line H" (NOROOT when that hop is not shown) # edges rows of a connect answer: "f t H" # mcp the entries of one array (ARG) of an MCP tools/call result: "name file line H" +# legend DEFINED when the answer's legend comments define via="name" — the mechanism ("name alone") and a +# NOT sentence ("does NOT mean") within the definition — else UNDEFINED +# mcplegend the same over an MCP stream: the tools/list description of tool ARG, or the tools/call answer cat >"$TMP/parse.py" <<'PY' import json, re, sys mode, path = sys.argv[ 1 ], sys.argv[ 2 ] @@ -126,15 +160,33 @@ doc = open( path, encoding="utf-8", errors="replace" ).read() def attr( tag, name ): m = re.search( r'\s' + name + r'="([^"]*)"', tag ) return m.group( 1 ) if m else None +def num( v, what ): + if v is None: + return None + if not re.fullmatch( r"[0-9]+", v ): + sys.stderr.write( "non-numeric %s=%r\n" % ( what, v ) ) + sys.exit( 3 ) + return int( v ) def hedge( tag ): return "name" if attr( tag, "via" ) == "name" else "-" +def crows( scope ): + blocks = re.findall( r"].*?", scope, re.S ) + if not blocks: + print( "NOROOT" ); sys.exit( 0 ) + for b in blocks: + for c in re.findall( r"]*>", b ): + for l in ( attr( c, "l" ) or "-" ).split( "," ): + print( attr( c, "n" ), l, hedge( c ) ) +DEF = re.compile( r'via="name".{0,700}?name alone.{0,700}?does NOT mean', re.S ) if mode == "rows": root = re.search( r"<(callees|callers|impact|path) [^>]*>", doc ) if not root: print( "NOROOT" ); sys.exit( 0 ) head = root.group( 0 ) - found = attr( head, "defs" ) or attr( head, "to_defs" ) - if root.group( 1 ) != "path" and ( not found or int( found ) < 1 ): + found = num( attr( head, "defs" ), "defs" ) + if found is None: + found = num( attr( head, "to_defs" ), "to_defs" ) + if root.group( 1 ) != "path" and ( found is None or found < 1 ): print( "NOROOT" ); sys.exit( 0 ) depth = "-" for s in re.findall( r"]*/>", doc[ root.start(): ] ): @@ -142,7 +194,7 @@ if mode == "rows": if not ( t and n and p ): continue if attr( s, "d" ): - depth = attr( s, "d" ) + depth = str( num( attr( s, "d" ), "d" ) ) f, _, l = p.rpartition( ":" ) print( t, n, f, l, hedge( s ), depth ) elif mode == "ecalls": @@ -152,12 +204,12 @@ elif mode == "ecalls": if not m: print( "NOROOT" ); sys.exit( 0 ) scope = m.group( 1 ) - blocks = re.findall( r"].*?", scope, re.S ) - if not blocks: + crows( scope ) +elif mode == "hcalls": + m = re.search( r']*\sn="' + re.escape( arg ) + r'"[^>]*>(.*?)', doc, re.S ) + if not arg or not m: print( "NOROOT" ); sys.exit( 0 ) - for b in blocks: - for c in re.findall( r"]*>", b ): - print( attr( c, "n" ), attr( c, "l" ) or "-", hedge( c ) ) + crows( m.group( 1 ) ) elif mode == "edges": root = re.search( r"]*>", doc ) if not root: @@ -174,8 +226,36 @@ elif mode == "mcp": print( "NOROOT" ); sys.exit( 0 ) for e in d[ arg ]: print( e.get( "name" ), e.get( "file" ), e.get( "line" ), "name" if e.get( "via" ) == "name" else "-" ) +elif mode == "legend": + text = " ".join( re.findall( r"", doc, re.S ) ) + print( "DEFINED" if DEF.search( text ) else "UNDEFINED" ) +elif mode == "mcplegend": + texts = [] + for line in doc.splitlines(): + try: + r = json.loads( line ) + except Exception: + continue + res = r.get( "result" ) or {} + for t in res.get( "tools" ) or []: + if t.get( "name" ) == arg: + texts.append( t.get( "description" ) or "" ) + for c in res.get( "content" ) or []: + texts.append( c.get( "text" ) or "" ) + if not texts: + print( "NOROOT" ); sys.exit( 0 ) + norm = " ".join( texts ).replace( '"via":"name"', 'via="name"' ) + print( "DEFINED" if DEF.search( norm ) else "UNDEFINED" ) +else: + sys.stderr.write( "unknown mode %s\n" % mode ); sys.exit( 2 ) PY -parse(){ python3 "$TMP/parse.py" "$@"; } +# P MODE FILE [ARG] — the reader's output into $PARSED; a non-zero exit (crash, malformed input) FAILs the arm +P(){ + PARSED="$( python3 "$TMP/parse.py" "$@" 2>"$TMP/parse.err" )" + local rc=$? + if [ "$rc" -ne 0 ]; then no "the reader exited rc=$rc on [$*]: $( head -c 300 "$TMP/parse.err" | tr '\n' ' ' )"; return 1; fi + return 0 +} # answer ROOT VERB SEL [EXTRA…] — write the verb's answer to a per-call file, its exit status beside it (FILE.rc) answer(){ @@ -190,13 +270,19 @@ ran_ok(){ if [ "$rc" = "0" ]; then return 0; fi no "$1 exited rc=${rc:-unknown}"; return 1 } -# getrows ROOT VERB SEL — rows of the verb's answer into $GOT ("" on a failed run, NOROOT when about no symbol) +# rowsof LABEL FILE — the rows of FILE into $GOT; a reader failure or an answer about no symbol FAILs (returns 1) +rowsof(){ + GOT="" + P rows "$2" || return 1 + GOT="$PARSED" + if [ "$GOT" = "NOROOT" ]; then no "$1 produced no answer about a symbol"; return 1; fi + return 0 +} +# getrows ROOT VERB SEL — rows of the verb's answer into $GOT ("" on a failed run or reader, FAIL already printed) getrows(){ local f; f="$( answer "$1" "$2" "$3" )"; GOT="" ran_ok "($1) --$2=$3" "$f" || return 1 - GOT="$( parse rows "$f" )" - if [ "$GOT" = "NOROOT" ]; then no "($1) --$2=$3 produced no <$2> answer about a symbol"; return 1; fi - return 0 + rowsof "($1) --$2=$3" "$f" } # match SPEC — the awk condition for "n path[:line]" over a rows line "t n path line H d" specmatch(){ # prints the matching rows of $GOT for SPEC @@ -253,14 +339,28 @@ marked(){ else no "($r) --$v=$s dropped the name-only true edge [$spec]: $( show )"; fi done } +# hopnotproven ROOT QUERY HOP NAME LINE — the --for answer shows the hop block (else FAIL: the arm needs +# it), and that hop's row NAME at l=LINE is absent or via="name" +hopnotproven(){ + local r="$1" q="$2" h="$3" n="$4" l="$5" f + f="$( answer "$r" for "$q" )" + ran_ok "($r) --for=\"$q\"" "$f" || return + P hcalls "$f" "$h" || return + if [ "$PARSED" = "NOROOT" ]; then no "($r) --for=\"$q\" shows no hop with (the arm needs that hop)"; return; fi + if [ -n "$( printf '%s\n' "$PARSED" | awk -v n="$n" -v l="$l" '$1 == n && $2 == l && $3 != "name"' )" ]; then + no "($r) --for=\"$q\": hop $h proves (no via=\"name\"): $( printf '%s' "$PARSED" | tr '\n' ';' )" + else ok "($r) --for=\"$q\": hop $h never proves (absent or hedged)"; fi +} echo "=== (J) JS: receivers with no evidence never prove a same-named method ===" notproven js callees lib/application.js:respond "onerror lib/application.js" visible js callees lib/application.js:respond "onerror lib/context.js" notproven js callees lib/application.js:respondSafely "onerror lib/application.js" -notproven js callees lib/application.js:respondComputed "onerror lib/application.js" -notproven js callees lib/application.js:schemas "listSchemas lib/application.js" -visible js callees lib/application.js:schemas "listSchemas lib/schemas.js" +notproven js callees lib/application.js:respondComputed "onerror lib/application.js" # guard: no call recorded today +notproven js callees lib/application.js:respondAlias "onerror lib/application.js" # guard +notproven js callees lib/application.js:respondDestructured "onerror lib/application.js" # guard +notproven js callees lib/delegate/response.js:back "get lib/delegate/response.js" +visible js callees lib/delegate/response.js:back "get lib/delegate/request.js" notproven js callees lib/handle.js:run "handler lib/handle.js" notproven js callees lib/request.js:compile "get lib/request.js" "set lib/request.js" notproven js callees lib/reply.js:awaitResult "then lib/reply.js" @@ -269,62 +369,78 @@ notproven js callees lib/routes.js:fallback "all lib/routes.js" notproven js callees lib/routes.js:forget "delete lib/routes.js" notproven js callees lib/search.js:encode "append lib/response.js" notproven js callees lib/reply.js:writePayload "done lib/parser.js" -echo "--- (J) evidence decides between same-named methods" +notproven js callees lib/meta.js:peek "get lib/meta.js" "get lib/reflect.js" # guard +notproven js callees lib/meta.js:bump "add lib/meta.js" # guard +echo "--- (J) evidence decides between same-named methods, and a constructor-assigned field RESOLVES" exactproven js callees lib/uses.js:build "cls Application lib/application.js;method onerror lib/application.js;cls Schemas lib/schemas.js;method listSchemas lib/schemas.js" +notproven js callees lib/application.js:schemas "listSchemas lib/application.js" +exactproven js callees lib/application.js:schemas "method listSchemas lib/schemas.js" echo "--- (J) near misses: true edges kept" exactproven js callees lib/application.js:handle "method onerror lib/application.js;fn respond lib/application.js" exactproven js callees lib/handle.js:wrapped "fn handler lib/handle.js" exactproven js callees lib/uses.js:viaModule "fn respondWith lib/reply.js" +exactproven js callees lib/uses.js:make "method create lib/application.js" # guard: a class-name (static) receiver +exactproven js callees lib/listing.js:list "method listSchemas lib/schemas.js" # guard: super.listSchemas() +exactproven js callees lib/mirror.js:look "method get lib/reflect.js" # guard: a relative module bound to `Reflect` exactproven js callees lib/trailer.js:sendTrailer "fn send lib/trailer.js" +exactproven js callees lib/reply.js:direct "fn Reply lib/reply.js;method send lib/reply.js" notproven js callees lib/reply.js:notFound "send lib/trailer.js" -visible js callees lib/reply.js:notFound "send lib/reply.js" "code lib/reply.js" +marked js callees lib/reply.js:notFound "code lib/reply.js" "send lib/reply.js" marked js callees lib/reply.js:respondWith "send lib/reply.js" echo "=== (T) TS ===" notproven ts callees src/router/smart.ts:match "add src/router/smart.ts" -notproven ts callees src/middleware/auth.ts:bearer "get src/context.ts" "get src/cache.ts" +notproven ts callees src/middleware/auth.ts:bearer "get src/context.ts" "get src/cache.ts" # cache.ts: guard +notproven ts callees src/atomics.ts:bump "add src/atomics.ts" "add src/router/smart.ts" "add src/router/trie.ts" # guard echo "--- (T) near misses: annotation, alias construction, constructed local" exactproven ts callees src/middleware/auth.ts:readUser "method get src/context.ts" exactproven ts callees src/middleware/alias.ts:remember "cls Context src/context.ts;method set src/context.ts" exactproven ts callees src/middleware/alias.ts:single "cls TrieRouter src/router/trie.ts;method add src/router/trie.ts;method match src/router/trie.ts" echo "=== (P) Python ===" -notproven py callees src/tui/css/stylesheet.py:reparse "get src/tui/css/styles.py" "keys src/tui/css/styles.py" "items src/tui/css/styles.py" "update src/tui/css/stylesheet.py" +notproven py callees src/tui/css/stylesheet.py:reparse "get src/tui/css/styles.py" "keys src/tui/css/styles.py" "items src/tui/css/styles.py" "update src/tui/css/stylesheet.py" # update: guard notproven py callees src/tui/css/stylesheet.py:refresh_rules "animate src/tui/css/styles.py" notproven py callees src/tui/reactive.py:watch "append src/tui/content.py" -notproven py callees src/tui/reactive.py:toggle "split src/tui/content.py" "split src/tui/geometry.py" +notproven py callees src/tui/reactive.py:toggle "split src/tui/content.py" "split src/tui/geometry.py" # geometry.py: guard notproven py callees src/tui/callback.py:invoke "call_later src/tui/message_pump.py" "cancel src/tui/worker.py" notproven py callees src/web/formparsers.py:parse_form "write src/web/datastructures.py" notproven py callees src/web/requests.py:send_json "_send src/web/errors.py" +notproven py callees src/web/render.py:render "stringify src/web/jsonutil.py" "dumps src/web/jsonutil.py" # guard: outside from-import notproven py callees src/tui/app.py:switch_mode "refresh src/tui/app.py" notproven py callees src/tui/app.py:repaint_screen "refresh src/tui/app.py" visible py callees src/tui/app.py:repaint_screen "refresh src/tui/screen.py" -notproven py callees src/tui/drivers/linux.py:process_events "read src/tui/css/stylesheet.py" "feed src/tui/xterm_parser.py:2" +notproven py callees src/tui/drivers/linux.py:process_events "read src/tui/css/stylesheet.py" "feed src/tui/xterm_parser.py:2" "feed src/tui/byte_stream.py" notproven py callees src/tui/screen.py:collect "add_widget src/tui/compositor.py" echo "--- (P) near misses: self, constructor-assigned field, typed parameter, construction, alias, import, cls" proven py callees src/tui/drivers/linux.py:process_events "fn feed src/tui/xterm_parser.py:7" exactproven py callees src/tui/css/stylesheet.py:own "fn update src/tui/css/stylesheet.py;fn get src/tui/css/styles.py" +# repaint: self.refresh() (proven) and self.screen.refresh() (a property: name-only) reach the same App.refresh; +# the merged row stays plain, and Screen.refresh may only ride along hedged exactproven py callees src/tui/app.py:repaint "fn refresh src/tui/app.py" exactproven py callees src/tui/uses.py:sync "fn update src/tui/css/stylesheet.py" exactproven py callees src/tui/uses.py:apply "fn animate src/tui/css/styles.py" exactproven py callees src/tui/uses.py:build "cls Stylesheet src/tui/css/stylesheet.py;fn update src/tui/css/stylesheet.py;fn read src/tui/css/stylesheet.py" +exactproven py callees src/tui/uses.py:clone "fn copy src/tui/css/styles.py" # guard: a class-name receiver exactproven py callees src/tui/css/stylesheet.py:parse_rules "fn parse src/tui/css/parse.py" exactproven py callees src/tui/css/stylesheet.py:blank "fn default_rules src/tui/css/stylesheet.py" marked py callees src/tui/callback.py:schedule "call_later src/tui/message_pump.py" -echo "=== (G) Go ===" +echo "=== (G) Go: declared types RESOLVE (D4) ===" notproven go callees src/result.go:buildResult "Get src/merger.go" "Length src/merger.go" -visible go callees src/result.go:buildResult "Get src/util/chars.go" "Length src/util/chars.go" +exactproven go callees src/result.go:buildResult "method Get src/util/chars.go;method Length src/util/chars.go" exactproven go callees src/result.go:fromValue "method Get src/util/chars.go" exactproven go callees src/embed.go:aliased "method Length src/util/chars.go" notproven go callees src/embed.go:firstRune "Get src/merger.go" -visible go callees src/embed.go:firstRune "Get src/util/chars.go" +exactproven go callees src/embed.go:firstRune "method Get src/util/chars.go" notproven go callees src/tui/tcell.go:MaxY "Size src/tui/tcell.go" notproven go callees src/tui/tcell.go:Graphemes "Runes src/tui/tcell.go" -echo "--- (G) near misses: a pointer-literal local, the method receiver itself" +notproven go callees src/service.go:Fail "Errorf src/internal/log/log.go" +notproven go callees src/service.go:Report "Errorf src/internal/log/log.go" +echo "--- (G) near misses: a pointer-literal local, the method receiver itself, a real package import" exactproven go callees src/result.go:merged "method Length src/merger.go" exactproven go callees src/merger.go:First "method Get src/merger.go" exactproven go callees src/tui/tcell.go:Lines "method Size src/tui/tcell.go" +exactproven go callees src/cmd/run.go:Run "fn Errorf src/internal/log/log.go" # guard echo "=== (I) implicit receiver: a bare call never proves an unrelated class's method ===" notproven java callees src/main/java/app/Logger.java:line "render src/main/java/app/Exporter.java" @@ -332,13 +448,14 @@ notproven java callees src/main/java/app/Logger.java:close "flush src/main/java/ notproven java callees src/main/java/app/FileBuffer.java:sync "flush src/main/java/app/Exporter.java" notproven kt callees src/app/Plain.kt:finish "flush src/app/Exporter.kt" "flush src/app/Logger.kt" notproven cs callees App/Logger.cs:Line "Render App/Exporter.cs" -notproven cs callees App/Logger.cs:Finish "Flush App/Logger.cs" "Flush App/Exporter.cs" +notproven cs callees App/Logger.cs:Finish "Flush App/Logger.cs" "Flush App/Exporter.cs" # Exporter.cs: guard notproven cpp callees src/plain.cpp:finish "flush src/exporter.cpp" "flush src/logger.cpp" notproven swift callees Sources/App/Plain.swift:finish "flush Sources/App/Exporter.swift" "flush Sources/App/Logger.swift" notproven rb callees lib/app/plain.rb:finish "render lib/app/exporter.rb" "flush lib/app/logger.rb" -echo "--- (I) near misses: own members, the in-repo superclass (cone), free/top-level functions, mixins" +echo "--- (I) near misses: own members, the in-repo superclass (cone, super.m()), free/top-level functions, mixins" proven java callees src/main/java/app/Logger.java:close "method reset src/main/java/app/Logger.java" proven java callees src/main/java/app/FileBuffer.java:sync "method flush src/main/java/app/Buffer.java" +exactproven java callees src/main/java/app/FileBuffer.java:drain "method flush src/main/java/app/Buffer.java" exactproven java callees src/main/java/app/Buffer.java:write "method flush src/main/java/app/Buffer.java" exactproven kt callees src/app/Logger.kt:line "fn render src/app/Logger.kt" exactproven kt callees src/app/Logger.kt:close "fn flush src/app/Logger.kt;fn reset src/app/Logger.kt" @@ -350,82 +467,160 @@ exactproven swift callees Sources/App/Logger.swift:close "fn flush Sources/App/L exactproven rb callees lib/app/logger.rb:line "method stamp lib/app/helpers.rb" exactproven rb callees lib/app/logger.rb:close "method flush lib/app/logger.rb;method reset lib/app/logger.rb;method top_level_note lib/app/notes.rb" +echo "=== (O) graded false callees from an earlier binary (Go, TS, Python repros): red where still present, pinned clean otherwise ===" +notproven go callees src/gotool.go:runGo "Output src/gotool.go" +notproven go callees src/gotool.go:runGo "Errorf src/internal/log/log.go" "Split src/gotool.go" # guard: fmt.Errorf, strings.Split +exactproven go callees src/gotool.go:runTool "method Output src/gotool.go;method Split src/gotool.go" # guard +notproven ts callees src/http.ts:reply "getTime src/http.ts" +notproven ts callees src/http.ts:reply "Response src/response.ts" # guard: the global Response +notproven ts callees src/http.ts:stamped "getTime src/http.ts" +exactproven ts callees src/http.ts:local "cls Clock src/http.ts;method getTime src/http.ts" +exactproven ts callees src/http.ts:wrap "cls Response src/response.ts" # guard +notproven py callees src/tui/listing.py:gather "append src/tui/content.py" +exactproven py callees src/tui/listing.py:grow "cls Content src/tui/content.py;fn append src/tui/content.py" +hopnotproven go "how does runGo run the go command" runGo Output 14 +hopnotproven ts "how does reply build a Response" reply getTime 5 +hopnotproven ts "how does stamped get the time" stamped getTime 5 +hopnotproven py "how does gather append nodes" gather append 5 + echo "=== (H) propagation: one hedge, every surface ===" # the name-only TRUE witness respondWith → reply.send(): kept and marked everywhere marked js callers lib/reply.js:send "respondWith lib/reply.js" -f="$( answer js impact lib/reply.js:send )" -if ran_ok "(js) --impact=lib/reply.js:send" "$f"; then - GOT="$( parse rows "$f" )" - if [ "$GOT" = "NOROOT" ]; then no "(H) --impact=lib/reply.js:send produced no answer about a symbol" - else - d1="$( printf '%s\n' "$GOT" | awk '$2 == "respondWith" && $6 == "1" { print $5 }' )" - if [ "$d1" = "name" ]; then ok "(H) --impact=lib/reply.js:send: the d=1 row respondWith is marked via=\"name\"" - else no "(H) --impact=lib/reply.js:send: the d=1 row respondWith is [${d1:-absent}], want via=\"name\": $( show )"; fi - if [ -n "$( printf '%s\n' "$GOT" | awk '$2 == "viaModule"' )" ]; then ok "(H) --impact=lib/reply.js:send keeps viaModule (reached through the hedged edge)" - else no "(H) --impact=lib/reply.js:send dropped viaModule: $( show )"; fi - fi -fi -f="$( answer js path lib/uses.js:viaModule,lib/reply.js:send )" -if ran_ok "(js) --path=viaModule,send" "$f"; then - GOT="$( parse rows "$f" )" +# impact_d3 — the d=1 row respondWith is marked; viaModule (d=2, reached ONLY through respondWith → send) inherits +# the hedge; both (d=2, also reached through direct → new Reply().send, all proven) and direct (d=1) stay plain +impact_send(){ + local f; f="$( answer js impact lib/reply.js:send )" + ran_ok "(js) --impact=lib/reply.js:send" "$f" || return + rowsof "(H) --impact=lib/reply.js:send" "$f" || return + local d1 vm bo di + d1="$( printf '%s\n' "$GOT" | awk '$2 == "respondWith" && $6 == "1" { print $5 }' )" + if [ "$d1" = "name" ]; then ok "(H) --impact=lib/reply.js:send: the d=1 row respondWith is marked via=\"name\"" + else no "(H) --impact=lib/reply.js:send: the d=1 row respondWith is [${d1:-absent}], want via=\"name\": $( show )"; fi + vm="$( printf '%s\n' "$GOT" | awk '$2 == "viaModule" && $6 == "2" { print $5 }' )" + if [ "$vm" = "name" ]; then ok "(H) --impact=lib/reply.js:send: viaModule (d=2, only through the hedged edge) inherits via=\"name\"" + else no "(H) --impact=lib/reply.js:send: viaModule at d=2 is [${vm:-absent}], want via=\"name\" (D3): $( show )"; fi + bo="$( printf '%s\n' "$GOT" | awk '$2 == "both" && $6 == "2" { print $5 }' )" + if [ "$bo" = "-" ]; then ok "(H) --impact=lib/reply.js:send: both (d=2, an all-proven path through direct) stays plain" + else no "(H) --impact=lib/reply.js:send: both at d=2 is [${bo:-absent}], want plain (an all-proven path reaches it): $( show )"; fi + di="$( printf '%s\n' "$GOT" | awk '$2 == "direct" && $6 == "1" { print $5 }' )" + if [ "$di" = "-" ]; then ok "(H) --impact=lib/reply.js:send: direct (d=1, new Reply().send) stays plain" + else no "(H) --impact=lib/reply.js:send: direct at d=1 is [${di:-absent}], want plain: $( show )"; fi +} +impact_send +path_send(){ + local f last; f="$( answer js path lib/uses.js:viaModule,lib/reply.js:send )" + ran_ok "(js) --path=viaModule,send" "$f" || return + P rows "$f" || return + GOT="$PARSED" last="$( printf '%s\n' "$GOT" | awk 'NF >= 5' | tail -1 )" case "$last" in "method send lib/reply.js 7 name"*) ok "(H) --path=viaModule,send: the hop into Reply.prototype.send is marked via=\"name\"";; *) no "(H) --path=viaModule,send: last hop is [${last:-none}], want Reply.prototype.send marked via=\"name\": $( show )";; esac -fi -f="$( answer js connect lib/reply.js:respondWith,lib/reply.js:send )" -if ran_ok "(js) --connect=respondWith,send" "$f"; then - e="$( parse edges "$f" | awk '$1 == "respondWith" && $2 == "send" { print $3 }' )" +} +path_send +connect_send(){ + local f e; f="$( answer js connect lib/reply.js:respondWith,lib/reply.js:send )" + ran_ok "(js) --connect=respondWith,send" "$f" || return + P edges "$f" || return + e="$( printf '%s\n' "$PARSED" | awk '$1 == "respondWith" && $2 == "send" { print $3 }' )" if [ "$e" = "name" ]; then ok "(H) --connect: the edge respondWith → send is marked via=\"name\"" - else no "(H) --connect: the edge respondWith → send is [${e:-absent}], want via=\"name\": $( parse edges "$f" | tr '\n' ';' )"; fi -fi -f="$( answer js expand lib/reply.js:respondWith --top-k=0 --legend=full )" -if ran_ok "(js) --expand=respondWith" "$f"; then - c="$( parse ecalls "$f" | awk '$1 == "send" { print $3 }' )" + else no "(H) --connect: the edge respondWith → send is [${e:-absent}], want via=\"name\": $( printf '%s' "$PARSED" | tr '\n' ';' )"; fi +} +connect_send +expand_send(){ + local f c; f="$( answer js expand lib/reply.js:respondWith --top-k=0 --legend=full )" + ran_ok "(js) --expand=respondWith" "$f" || return + P ecalls "$f" || return + c="$( printf '%s\n' "$PARSED" | awk '$1 == "send" { print $3 }' | sort -u )" if [ "$c" = "name" ]; then ok "(H) --expand=respondWith: row send is marked via=\"name\"" - else no "(H) --expand=respondWith: row send is [${c:-absent}], want via=\"name\": $( parse ecalls "$f" | tr '\n' ';' )"; fi -fi -f="$( answer js for "how does respondWith send the reply" --legend=full )" -if ran_ok "(js) --for=respondWith" "$f"; then - c="$( parse ecalls "$f" respondWith | awk '$1 == "send" { print $3 }' )" + else no "(H) --expand=respondWith: row send is [${c:-absent}], want via=\"name\": $( printf '%s' "$PARSED" | tr '\n' ';' )"; fi +} +expand_send +for_send(){ + local f c; f="$( answer js for "how does respondWith send the reply" --legend=full )" + ran_ok "(js) --for=respondWith" "$f" || return + P ecalls "$f" respondWith || return + if [ "$PARSED" = "NOROOT" ]; then no "(H) --for: no respondWith body with (the arm needs it)"; return; fi + c="$( printf '%s\n' "$PARSED" | awk '$1 == "send" { print $3 }' | sort -u )" if [ "$c" = "name" ]; then ok "(H) --for: respondWith's body row send is marked via=\"name\"" - else no "(H) --for: respondWith's body row send is [${c:-absent}], want via=\"name\": $( parse ecalls "$f" respondWith | tr '\n' ';' )"; fi -fi + else no "(H) --for: respondWith's body row send is [${c:-absent}], want via=\"name\": $( printf '%s' "$PARSED" | tr '\n' ';' )"; fi +} +for_send # the FALSE witness respond → ctx.onerror(): absent or marked on every surface notproven js callers lib/application.js:onerror "respond lib/application.js" "respondSafely lib/application.js" -f="$( answer js impact lib/application.js:onerror )" -if ran_ok "(js) --impact=lib/application.js:onerror" "$f"; then - GOT="$( parse rows "$f" )" - if [ "$GOT" = "NOROOT" ]; then no "(H) --impact=lib/application.js:onerror produced no answer about a symbol" - elif [ -n "$( printf '%s\n' "$GOT" | awk '$2 == "respond" && $6 == "1" && $5 != "name"' )" ]; then +impact_onerror(){ + local f se rs; f="$( answer js impact lib/application.js:onerror )" + ran_ok "(js) --impact=lib/application.js:onerror" "$f" || return + rowsof "(H) --impact=lib/application.js:onerror" "$f" || return + if [ -n "$( printf '%s\n' "$GOT" | awk '$2 == "respond" && $6 == "1" && $5 != "name"' )" ]; then no "(H) --impact=lib/application.js:onerror proves respond at d=1: $( show )" else ok "(H) --impact=lib/application.js:onerror never proves respond at d=1"; fi -fi -f="$( answer js path lib/application.js:respond,lib/application.js:onerror )" -if ran_ok "(js) --path=respond,onerror" "$f"; then - GOT="$( parse rows "$f" )" + # serve reaches onerror only through respond: when respond is shown (hedged), serve is shown at d=2 marked + rs="$( printf '%s\n' "$GOT" | awk '$2 == "respond" { print $5 }' )" + se="$( printf '%s\n' "$GOT" | awk '$2 == "serve" { print $5 "@" $6 }' )" + if [ -n "$rs" ]; then + if [ "$se" = "name@2" ]; then ok "(H) --impact=lib/application.js:onerror: serve (d=2, only through respond's name-only call) inherits via=\"name\"" + else no "(H) --impact=lib/application.js:onerror: serve is [${se:-absent}], want via=\"name\" at d=2 (D3): $( show )"; fi + else + case "$se" in ""|name@*) ok "(H) --impact=lib/application.js:onerror: respond is gone, serve is absent or marked";; + *) no "(H) --impact=lib/application.js:onerror: serve is PROVEN [$se] with respond gone: $( show )";; esac + fi +} +impact_onerror +path_onerror(){ + local f last reach top; f="$( answer js path lib/application.js:respond,lib/application.js:onerror )" + ran_ok "(js) --path=respond,onerror" "$f" || return + P rows "$f" || return + GOT="$PARSED" last="$( printf '%s\n' "$GOT" | awk 'NF >= 5' | tail -1 )" - reach="$( grep -oE ']*>' "$f" | grep -oE ' reachable="[0-9]+"' | grep -oE '[0-9]+' )" - if [ -z "$reach" ]; then no "(H) --path=respond,onerror carries no reachable= (no answer)" - elif [ "$reach" = "0" ]; then ok "(H) --path=respond,onerror: unreachable (the false edge is gone)" + top="$( grep -oE ']*>' "$f" | grep -oE ' to_p="[^"]*"' )" + if [ "$top" != ' to_p="lib/application.js:12"' ]; then no "(H) --path=respond,onerror bound the target to [${top:-nothing}], want to_p=\"lib/application.js:12\""; return; fi + reach="$( grep -oE ']*>' "$f" | grep -oE ' reachable="[^"]*"' | sed 's/.*="//; s/"$//' )" + case "$reach" in ''|*[!0-9]*) no "(H) --path=respond,onerror carries no numeric reachable= ([$reach])"; return;; esac + if [ "$reach" = "0" ]; then ok "(H) --path=respond,onerror: unreachable (the false edge is gone)" else case "$last" in *" name "*|*" name") ok "(H) --path=respond,onerror: the hop into Application.onerror is marked via=\"name\"";; *) no "(H) --path=respond,onerror proves the hop into Application.onerror: [$last]";; esac; fi -fi -f="$( answer js expand lib/application.js:respond --top-k=0 --legend=full )" -if ran_ok "(js) --expand=respond" "$f"; then - rows="$( parse ecalls "$f" )" - if [ "$rows" = "NOROOT" ]; then no "(H) --expand=respond emitted no block" - elif [ -n "$( printf '%s\n' "$rows" | awk '$1 == "onerror" && $2 == "12" && $3 != "name"' )" ]; then - no "(H) --expand=respond: proves onerror at application.js:12: $( printf '%s' "$rows" | tr '\n' ';' )" +} +path_onerror +expand_onerror(){ + local f; f="$( answer js expand lib/application.js:respond --top-k=0 --legend=full )" + ran_ok "(js) --expand=respond" "$f" || return + P ecalls "$f" || return + if [ "$PARSED" = "NOROOT" ]; then no "(H) --expand=respond emitted no block" + elif [ -n "$( printf '%s\n' "$PARSED" | awk '$1 == "onerror" && $2 == "12" && $3 != "name"' )" ]; then + no "(H) --expand=respond: proves onerror at application.js:12: $( printf '%s' "$PARSED" | tr '\n' ';' )" else ok "(H) --expand=respond: never proves Application.onerror"; fi -fi +} +expand_onerror + +echo "--- (H) --for HOP blocks: the graded false rows were hop , not only body " +hopnotproven js "how does handle report an error" encode append 3 +hopnotproven js "how does respond report an error on ctx" respond onerror 12 +hopnotproven js "how does pipePayload handle payload end" pipePayload on 4 +hopnotproven js "how does the response go back to the referrer" back get 4 + +echo "--- (H) the legend defines via=\"name\" and says what it does NOT mean, on every surface" +legend_arm(){ + local label="$1" f="$2" + ran_ok "(H) legend $label" "$f" || return + P legend "$f" || return + if [ "$PARSED" = "DEFINED" ]; then ok "(H) legend $label defines via=\"name\" (by name alone) with a NOT sentence" + else no "(H) legend $label: no definition of via=\"name\" naming the mechanism (\"name alone\") and a \"does NOT mean\" sentence"; fi +} +legend_arm "--callees" "$( answer js callees lib/reply.js:respondWith --legend=full )" +legend_arm "--callers" "$( answer js callers lib/reply.js:send --legend=full )" +legend_arm "--impact" "$( answer js impact lib/reply.js:send --legend=full )" +legend_arm "--path" "$( answer js path lib/uses.js:viaModule,lib/reply.js:send --legend=full )" +legend_arm "--connect" "$( answer js connect lib/reply.js:respondWith,lib/reply.js:send --legend=full )" +legend_arm "--expand" "$( answer js expand lib/reply.js:respondWith --top-k=0 --legend=full )" +legend_arm "--for" "$( answer js for "how does respondWith send the reply" --legend=full )" echo "--- (H) MCP twins (fresh TMPDIR: the MCP cache lives there)" mkdir -p "$TMP/mcp" -# mcp ROOT TOOL SELECTOR OUT — one tools/call; the raw stream into OUT, the server's exit status into OUT.rc +# mcp ROOT TOOL SELECTOR OUT — one tools/call (then tools/list); the raw stream into OUT, the exit status into OUT.rc mcp(){ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ "$( printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s","symbol":"%s"}}}' "$2" "$CORPUS/$1" "$3" )" \ @@ -438,8 +633,9 @@ mcp_hedge(){ out="$TMP/mcp.$r.$tool.$( printf '%s' "$sel" | tr '/:.' '___' )" mcp "$r" "$tool" "$sel" "$out" ran_ok "(H) MCP $tool $sel" "$out" || return - if [ "$( parse mcp "$out" "$field" )" = "NOROOT" ]; then no "(H) MCP $tool $sel: no JSON answer with $field"; return; fi - h="$( parse mcp "$out" "$field" | awk -v n="$n" -v f="$file" '$1 == n && $2 == f { print $4 }' | sort -u | tr '\n' ' ' | sed 's/ $//' )" + P mcp "$out" "$field" || return + if [ "$PARSED" = "NOROOT" ]; then no "(H) MCP $tool $sel: no JSON answer with $field"; return; fi + h="$( printf '%s\n' "$PARSED" | awk -v n="$n" -v f="$file" '$1 == n && $2 == f { print $4 }' | sort -u | tr '\n' ' ' | sed 's/ $//' )" if [ "$want" = "name" ]; then if [ "$h" = "name" ]; then ok "(H) MCP $tool $sel: $field entry $n@$file is marked via=name" else no "(H) MCP $tool $sel: $field entry $n@$file is [${h:-absent}], want via=name"; fi @@ -453,36 +649,57 @@ mcp_hedge js find_referencing_symbols lib/reply.js:send calledBy respondWith lib mcp_hedge js find_symbol lib/application.js:respond calls onerror lib/application.js notproven mcp_hedge js find_referencing_symbols lib/application.js:onerror calledBy respond lib/application.js notproven mcp_hedge py find_symbol src/tui/callback.py:schedule calls call_later src/tui/message_pump.py name +# mcp_legend TOOL SEL — tools/call then tools/list: the tool's description or its answer defines "via":"name" +mcp_legend(){ + local tool="$1" sel="$2" out="$TMP/mcplegend.$1" + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + "$( printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":{"path":"%s","symbol":"%s"}}}' "$tool" "$CORPUS/js" "$sel" )" \ + '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' \ + | TMPDIR="$TMP/mcp" "$BIN" --mcp >"$out" 2>/dev/null + printf '%s' "$?" >"$out.rc" + ran_ok "(H) MCP legend $tool" "$out" || return + P mcplegend "$out" "$tool" || return + if [ "$PARSED" = "DEFINED" ]; then ok "(H) MCP $tool: the tool description or answer defines \"via\":\"name\" with a NOT sentence" + elif [ "$PARSED" = "NOROOT" ]; then no "(H) MCP $tool: no tools/list or tools/call answer to read" + else no "(H) MCP $tool: neither the tool description nor the answer defines \"via\":\"name\" (\"name alone\" + \"does NOT mean\")"; fi +} +mcp_legend find_symbol lib/reply.js:respondWith +mcp_legend find_referencing_symbols lib/reply.js:send echo "--- (H) parity: the hedge bit is value-equal across CLI and MCP, and across --callees and --expand" # cli_set ROOT VERB SEL — "name@file#H" lines of the CLI answer; mcp_set ROOT TOOL SEL FIELD — the same from MCP -cli_set(){ local f rc; f="$( answer "$1" "$2" "$3" )"; rc="$( cat "$f.rc" 2>/dev/null )" +# (a run or reader failure prints RUNFAIL, which parity FAILs) +cli_set(){ local f rc rows; f="$( answer "$1" "$2" "$3" )"; rc="$( cat "$f.rc" 2>/dev/null )" if [ "$rc" != "0" ]; then echo "RUNFAIL --$2=$3 rc=${rc:-unknown}"; return; fi - parse rows "$f" | awk 'NF >= 5 { print $2 "@" $3 "#" $5 } $0 == "NOROOT" { print }' | sort -u; } -mcp_set(){ local out rc; out="$TMP/mcpset.$1.$2.$( printf '%s' "$3" | tr '/:.' '___' )"; mcp "$1" "$2" "$3" "$out" + rows="$( python3 "$TMP/parse.py" rows "$f" 2>/dev/null )" || { echo "RUNFAIL reader on --$2=$3"; return; } + printf '%s\n' "$rows" | awk 'NF >= 5 { print $2 "@" $3 "#" $5 } $0 == "NOROOT" { print }' | sort -u; } +mcp_set(){ local out rc rows; out="$TMP/mcpset.$1.$2.$( printf '%s' "$3" | tr '/:.' '___' )"; mcp "$1" "$2" "$3" "$out" rc="$( cat "$out.rc" 2>/dev/null )" if [ "$rc" != "0" ]; then echo "RUNFAIL MCP $2 $3 rc=${rc:-unknown}"; return; fi - parse mcp "$out" "$4" | awk 'NF >= 4 { print $1 "@" $2 "#" $4 } $0 == "NOROOT" { print }' | sort -u; } + rows="$( python3 "$TMP/parse.py" mcp "$out" "$4" 2>/dev/null )" || { echo "RUNFAIL reader on MCP $2 $3"; return; } + printf '%s\n' "$rows" | awk 'NF >= 4 { print $1 "@" $2 "#" $4 } $0 == "NOROOT" { print }' | sort -u; } parity(){ local label="$1" a="$2" b="$3" - case "$a$b" in *RUNFAIL*) no "(H) parity $label: a run exited non-zero ([$a] [$b])"; return;; esac + case "$a$b" in *RUNFAIL*) no "(H) parity $label: a run or reader failed ([$a] [$b])"; return;; esac if [ -z "$a" ] || [ "$a" = "NOROOT" ] || [ "$b" = "NOROOT" ]; then no "(H) parity $label: an empty or rootless side ([$a] vs [$b])"; return; fi if [ "$a" = "$b" ]; then ok "(H) parity $label: $( printf '%s' "$a" | tr '\n' ' ' )" else no "(H) parity $label: CLI [$( printf '%s' "$a" | tr '\n' ' ' )] vs [$( printf '%s' "$b" | tr '\n' ' ' )]"; fi } -for sel in lib/application.js:respond lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile lib/uses.js:build lib/application.js:handle; do +for sel in lib/application.js:respond lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile lib/uses.js:build lib/application.js:handle lib/delegate/response.js:back; do parity "callees/find_symbol.calls $sel" "$( cli_set js callees "$sel" )" "$( mcp_set js find_symbol "$sel" calls )" done for sel in lib/reply.js:send lib/application.js:onerror lib/schemas.js:listSchemas; do parity "callers/find_referencing_symbols.calledBy $sel" "$( cli_set js callers "$sel" )" "$( mcp_set js find_referencing_symbols "$sel" calledBy )" done # --callees vs --expand : compared by name + hedge (a row names no file) -for sel in lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile; do +for sel in lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile lib/delegate/response.js:back; do fc="$( answer js callees "$sel" )"; fe="$( answer js expand "$sel" --top-k=0 --legend=full )" if ran_ok "(H) --callees=$sel" "$fc" && ran_ok "(H) --expand=$sel" "$fe"; then - a="$( parse rows "$fc" | awk 'NF >= 5 { print $2 "#" $5 }' | sort -u )" - b="$( parse ecalls "$fe" | awk 'NF >= 3 { print $1 "#" $3 }' | sort -u )" - parity "callees/expand-calls $sel" "$a" "$b" + if ra="$( python3 "$TMP/parse.py" rows "$fc" 2>/dev/null )" && rb="$( python3 "$TMP/parse.py" ecalls "$fe" 2>/dev/null )"; then + a="$( printf '%s\n' "$ra" | awk 'NF >= 5 { print $2 "#" $5 }' | sort -u )" + b="$( printf '%s\n' "$rb" | awk 'NF >= 3 { print $1 "#" $3 }' | sort -u )" + parity "callees/expand-calls $sel" "$a" "$b" + else no "(H) parity callees/expand-calls $sel: the reader failed"; fi fi done @@ -505,32 +722,33 @@ PY done echo "=== (K) the predicates can fail ===" -GOT="$( printf '' >"$TMP/k1.xml"; parse rows "$TMP/k1.xml" )" +kread(){ P rows "$1" && GOT="$PARSED"; } +printf '' >"$TMP/k1.xml"; kread "$TMP/k1.xml" if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then ok "(K) an unhedged planted row reads as PROVEN (notproven would fail it)" else no "(K) the reader missed an unhedged planted row: [$GOT]"; fi -GOT="$( printf '' >"$TMP/k2.xml"; parse rows "$TMP/k2.xml" )" +printf '' >"$TMP/k2.xml"; kread "$TMP/k2.xml" if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 == "name"' )" ] && [ -z "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then ok "(K) a hedged planted row reads as marked (notproven passes it, proven fails it)" else no "(K) the reader did not read via=\"name\": [$GOT]"; fi -GOT="$( printf '' >"$TMP/k3.xml"; parse rows "$TMP/k3.xml" )" +printf '' >"$TMP/k3.xml"; kread "$TMP/k3.xml" if [ -n "$( specmatch "onerror lib/application.js" | awk '$5 != "name"' )" ]; then ok "(K) via=\"import\" is not the hedge" else no "(K) the reader took via=\"import\" for the hedge: [$GOT]"; fi printf 'nothing here' >"$TMP/k4.xml" -if [ "$( parse rows "$TMP/k4.xml" )" = "NOROOT" ]; then ok "(K) an answer with no root element is NOROOT" +if P rows "$TMP/k4.xml" && [ "$PARSED" = "NOROOT" ]; then ok "(K) an answer with no root element is NOROOT" else no "(K) a rootless document read as an answer"; fi printf '' >"$TMP/k5.xml" -if [ "$( parse rows "$TMP/k5.xml" )" = "NOROOT" ]; then ok "(K) an answer about no symbol (no defs=) is NOROOT" +if P rows "$TMP/k5.xml" && [ "$PARSED" = "NOROOT" ]; then ok "(K) an answer about no symbol (no defs=) is NOROOT" else no "(K) a not-found answer read as an empty answer"; fi printf 'sig' >"$TMP/k6.xml" -if [ "$( parse ecalls "$TMP/k6.xml" respondWith )" = "NOROOT" ]; then ok "(K) a --for answer without the named body is NOROOT, never an empty pass" +if P ecalls "$TMP/k6.xml" respondWith && [ "$PARSED" = "NOROOT" ]; then ok "(K) a --for answer without the named body is NOROOT, never an empty pass" else no "(K) ecalls read another body's as respondWith's"; fi printf 'sig' >"$TMP/k7.xml" -if [ "$( parse ecalls "$TMP/k7.xml" respondWith )" = "send 7 name" ]; then ok "(K) ecalls reads a marked row" -else no "(K) ecalls missed a marked row: [$( parse ecalls "$TMP/k7.xml" respondWith )]"; fi +if P ecalls "$TMP/k7.xml" respondWith && [ "$PARSED" = "send 7 name" ]; then ok "(K) ecalls reads a marked row" +else no "(K) ecalls missed a marked row: [$PARSED]"; fi printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"calls\":[{\"name\":\"send\",\"file\":\"lib/reply.js\",\"line\":7,\"via\":\"name\"},{\"name\":\"code\",\"file\":\"lib/reply.js\",\"line\":12}]}"}]}}' >"$TMP/k8.json" -if [ "$( parse mcp "$TMP/k8.json" calls | tr '\n' ';' )" = "send lib/reply.js 7 name;code lib/reply.js 12 -;" ]; then ok "(K) the MCP reader reads the hedge bit per entry" -else no "(K) the MCP reader misread: [$( parse mcp "$TMP/k8.json" calls | tr '\n' ';' )]"; fi -if [ "$( parse mcp "$TMP/k8.json" calledBy )" = "NOROOT" ]; then ok "(K) an MCP answer without the field is NOROOT" +if P mcp "$TMP/k8.json" calls && [ "$( printf '%s' "$PARSED" | tr '\n' ';' )" = "send lib/reply.js 7 name;code lib/reply.js 12 -" ]; then ok "(K) the MCP reader reads the hedge bit per entry" +else no "(K) the MCP reader misread: [$( printf '%s' "$PARSED" | tr '\n' ';' )]"; fi +if P mcp "$TMP/k8.json" calledBy && [ "$PARSED" = "NOROOT" ]; then ok "(K) an MCP answer without the field is NOROOT" else no "(K) the MCP reader invented a missing field"; fi printf '1' >"$TMP/k9.rc" if ( ran_ok k "$TMP/k9" ) >/dev/null; then no "(K) ran_ok accepted a run that exited 1" @@ -544,5 +762,27 @@ if ( parity k "" "" ) | grep -q FAIL; then ok "(K) parity fails on two empty sid else no "(K) parity accepted two empty sides"; fi if ( parity k "RUNFAIL --callees=x rc=1" "RUNFAIL --callees=x rc=1" ) | grep -q FAIL; then ok "(K) parity fails when a run exited non-zero" else no "(K) parity accepted a failed run"; fi +# change 1: a reader crash or malformed defs= FAILs the arm; it never reads as "no rows" (the old fall-through) +printf '' >"$TMP/k11.xml" +if python3 "$TMP/parse.py" rows "$TMP/k11.xml" >/dev/null 2>&1; then no "(K) the reader accepted a non-numeric defs=" +else ok "(K) the reader exits non-zero on a non-numeric defs="; fi +if ( rowsof k "$TMP/k11.xml" ) | grep -q FAIL; then ok "(K) a reader failure FAILs the arm (never an empty pass)" +else no "(K) a reader failure fell through to PASS"; fi +if ( fail=0; rowsof k "$TMP/k11.xml" >/dev/null; [ "$fail" -eq 1 ] ); then ok "(K) a reader failure sets the gate's failure flag" +else no "(K) a reader failure left the failure flag clear"; fi +printf '' >"$TMP/k12.xml" +if P hcalls "$TMP/k12.xml" encode && [ "$PARSED" = "append 3 name" ] && P hcalls "$TMP/k12.xml" respond && [ "$PARSED" = "onerror 12 -" ]; then + ok "(K) a hop reader reads only its own hop's " +else no "(K) the hop reader crossed hops: [$PARSED]"; fi +if P hcalls "$TMP/k12.xml" pipePayload && [ "$PARSED" = "NOROOT" ]; then ok "(K) a hop that is not shown is NOROOT, never an empty pass" +else no "(K) the hop reader invented a hop: [$PARSED]"; fi +printf '' >"$TMP/k13.xml" +if P ecalls "$TMP/k13.xml" && [ "$( printf '%s' "$PARSED" | tr '\n' ';' )" = "get 4 -;get 8 -" ]; then ok "(K) a joined row reads as one row per line" +else no "(K) a joined row misread: [$PARSED]"; fi +printf '' >"$TMP/k14.xml" +printf '' >"$TMP/k15.xml" +if P legend "$TMP/k14.xml" && [ "$PARSED" = "DEFINED" ] && P legend "$TMP/k15.xml" && [ "$PARSED" = "UNDEFINED" ]; then + ok "(K) the legend reader needs the mechanism and the NOT sentence AFTER via=\"name\"" +else no "(K) the legend reader misread a planted legend"; fi if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAILURES ABOVE"; exit 1; fi diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md index 203cb560f..b171750f4 100644 --- a/test/receiverevidencefix/README.md +++ b/test/receiverevidencefix/README.md @@ -9,26 +9,35 @@ definition beside them, plus near-miss calls whose receiver IS proven and whose beside same-file accessors; Promise `.then` beside `Reply.prototype.then`; a stream's `.on` beside a test double; an outside router's `.all` and a Set's `.delete` beside instance shorthands; URLSearchParams `.append` beside `Response.append`; a `done` parameter beside another file's closure; `context.handler()` beside a free - `handler`. Kept: `this.onerror()`, a same-file bare call, a relative module receiver, a closure called in its - own function, `new Application()` / `new Schemas()` deciding between same-named methods; `reply.send()` on a - parameter is the name-only true edge the gate wants kept AND marked. + `handler`; an object-literal module's `this.ctx.get()` beside its own `get` (the true target is the request + delegate's `get`); an alias `const onerror = ctx.onerror` and a destructured `const { onerror } = ctx`; the + globals `Reflect.get` / `Atomics.add` beside in-repo `get` / `add`. Kept: `this.onerror()`, a same-file bare call, a relative module receiver, a closure called in its + own function, `new Application()` / `new Schemas()` deciding between same-named methods, `super.listSchemas()`, + a class-name receiver `Application.create()`, a relative module bound to the name `Reflect`, `new Reply().send()`; + `reply.send()` / `reply.code()` on a parameter are the name-only true edges the gate wants kept AND marked. + `serve` (via respond) and `both` (via direct and respondWith) give `--impact` its d=2 rows. - `ts/` — a loop variable over `Router[]` beside `SmartRouter.add`; WHATWG `headers.get()` beside - `Context.get` / `Cache.get`. Kept: an annotated parameter, construction through an import alias, a + `Context.get` / `Cache.get`; the global `Atomics.add`; `Date#getTime` beside an in-repo `Clock.getTime`, and the + global `Response` beside an in-repo `Response` class. Kept: an annotated parameter, construction through an import alias, a constructed local. - `py/` (src layout) — dict/set/list/str receivers in files that import `Styles` / `Content`; an argument's `.animate`; an asyncio handle's `.cancel`; an outside package's parser `.write`; `self._send` holding a passed - callable beside a nested `_send`; non-self receivers in `App`'s own file; `read = os.read` and + callable beside a nested `_send`; non-self receivers in `App`'s own file (`screen` is a property); + `from json import dumps as stringify` beside in-repo `stringify` / `dumps`; `append` on a list literal; `read = os.read` and `feed = parser.feed` called from a nested def; `add_widget = widgets.append` beside a nested `add_widget`. Kept: `self.update()`, a constructor-assigned field through the class cone, a typed parameter, construction, - an import alias, an imported function beside same-named methods, `cls.default_rules()`, the + an import alias, an imported function beside same-named methods, `cls.default_rules()`, `Styles.copy(x)`, a + constructed `Content`'s `append`, the `XTermParser.feed` the alias really reaches; `pump.call_later()` on a parameter is kept and marked. - `go/` — an item's typed `text util.Chars` field, a typed parameter, a typed local through an aliased import and an embedded field, all beside `Merger.Get/Length` in the caller's package; an outside `tcell.Screen`'s - `Size` and an outside value's `Runes` beside a renderer's methods. Kept: `m := &Merger{}`, the method - receiver itself. + `Size` and an outside value's `Runes` beside a renderer's methods; a field / local named `log` of an outside type + beside the in-repo `log.Errorf`; exec's `Cmd.Output` beside an in-repo `Output` (with `fmt.Errorf` and + `strings.Split`). Kept: `m := &Merger{}`, the method receiver itself, a real import of the in-repo log package, + a constructed in-repo `Tool`. - `java/ kt/ cs/ cpp/ swift/ rb/` — a bare call inside a class whose base is outside the tree (or whose name comes from an outside import) beside an unrelated class's method of that name. Kept: own members (private - too), an in-repo superclass's member, a free / top-level function, a Ruby included module, a Ruby top-level + too), an in-repo superclass's member (and Java `super.flush()`), a free / top-level function, a Ruby included module, a Ruby top-level def. The names are paraphrases of graded false rows; the code is minimal and is never built. diff --git a/test/receiverevidencefix/go/go.mod b/test/receiverevidencefix/go/go.mod index d88fd0224..8854a7dbf 100644 --- a/test/receiverevidencefix/go/go.mod +++ b/test/receiverevidencefix/go/go.mod @@ -4,3 +4,4 @@ go 1.22 require github.com/gdamore/tcell/v2 v2.7.0 require github.com/rivo/uniseg v0.4.7 +require go.uber.org/zap v1.27.0 diff --git a/test/receiverevidencefix/go/src/cmd/run.go b/test/receiverevidencefix/go/src/cmd/run.go new file mode 100644 index 000000000..0c9b2664d --- /dev/null +++ b/test/receiverevidencefix/go/src/cmd/run.go @@ -0,0 +1,8 @@ +package cmd + +import "example.com/fzfish/src/internal/log" + +// Run: a real import of the in-repo log package, so log.Errorf is that package's function. +func Run() error { + return log.Errorf("run") +} diff --git a/test/receiverevidencefix/go/src/gotool.go b/test/receiverevidencefix/go/src/gotool.go new file mode 100644 index 000000000..2cc5d91e9 --- /dev/null +++ b/test/receiverevidencefix/go/src/gotool.go @@ -0,0 +1,37 @@ +package fzf + +import ( + "fmt" + "os/exec" + "strings" +) + +// Tool has methods named like stdlib functions and methods it never reaches. +type Tool struct { + out []byte +} + +func (t *Tool) Output() ([]byte, error) { + return t.out, nil +} + +func (t *Tool) Split(s string) []string { + return []string{s} +} + +// runGo: fmt.Errorf, exec's Cmd.Output and strings.Split are stdlib, never Tool's methods or the log package. +func runGo(args string) ([]string, error) { + cmd := exec.Command("go", args) + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("go: %w", err) + } + return strings.Split(string(out), "\n"), nil +} + +// runTool: a local of the in-repo type keeps its edges. +func runTool(s string) []string { + t := &Tool{} + t.Output() + return t.Split(s) +} diff --git a/test/receiverevidencefix/go/src/internal/log/log.go b/test/receiverevidencefix/go/src/internal/log/log.go new file mode 100644 index 000000000..729567025 --- /dev/null +++ b/test/receiverevidencefix/go/src/internal/log/log.go @@ -0,0 +1,8 @@ +package log + +import "fmt" + +// Errorf is the in-repo log package's function. +func Errorf(format string, a ...any) error { + return fmt.Errorf(format, a...) +} diff --git a/test/receiverevidencefix/go/src/service.go b/test/receiverevidencefix/go/src/service.go new file mode 100644 index 000000000..120c425c2 --- /dev/null +++ b/test/receiverevidencefix/go/src/service.go @@ -0,0 +1,19 @@ +package fzf + +import "go.uber.org/zap" + +// Service logs through an outside logger held in a field named like the in-repo log package. +type Service struct { + log *zap.SugaredLogger +} + +// Fail: s.log is a field of an outside type, never the in-repo log package. +func (s *Service) Fail(msg string) { + s.log.Errorf("failed: %s", msg) +} + +// Report: a local named log shadows nothing in-repo here; this file never imports the log package. +func Report(l *zap.SugaredLogger) { + log := l + log.Errorf("report") +} diff --git a/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java b/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java index 68c9953cc..2ffddbf9c 100644 --- a/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java +++ b/test/receiverevidencefix/java/src/main/java/app/FileBuffer.java @@ -5,4 +5,8 @@ public class FileBuffer extends Buffer { public void sync() { flush(); } + + public void drain() { + super.flush(); + } } diff --git a/test/receiverevidencefix/js/lib/application.js b/test/receiverevidencefix/js/lib/application.js index dbba98fd1..797bd3983 100644 --- a/test/receiverevidencefix/js/lib/application.js +++ b/test/receiverevidencefix/js/lib/application.js @@ -25,6 +25,10 @@ class Application { listSchemas () { return [] } + + static create () { + return new Application() + } } function respond (ctx) { @@ -40,4 +44,21 @@ function respondComputed (ctx) { ctx['onerror'](new Error('computed')) } +// An alias and a destructured binding of the same context member: still no evidence about `ctx`. +function respondAlias (ctx) { + const onerror = ctx.onerror + onerror(new Error('alias')) +} + +function respondDestructured (ctx) { + const { onerror } = ctx + onerror(new Error('destructured')) +} + +// A caller of respond: --impact=onerror reaches it at d=2 only through respond's name-only call. +function serve (ctx) { + return respond(ctx) +} + module.exports = Application +module.exports.serve = serve diff --git a/test/receiverevidencefix/js/lib/delegate/request.js b/test/receiverevidencefix/js/lib/delegate/request.js new file mode 100644 index 000000000..cc371ffdf --- /dev/null +++ b/test/receiverevidencefix/js/lib/delegate/request.js @@ -0,0 +1,8 @@ +'use strict' +// A request delegate: ctx.get(field) reaches this get through delegation, never by a static edge. +module.exports = { + get (field) { + const header = this.header + return header[field] || '' + } +} diff --git a/test/receiverevidencefix/js/lib/delegate/response.js b/test/receiverevidencefix/js/lib/delegate/response.js new file mode 100644 index 000000000..b91aa0f63 --- /dev/null +++ b/test/receiverevidencefix/js/lib/delegate/response.js @@ -0,0 +1,11 @@ +'use strict' +// An object-literal module: `this.ctx` is a request context, so this.ctx.get() is never this module's own get. +module.exports = { + get (field) { + return this.header[field] + }, + + back () { + return this.ctx.get('Referrer') + } +} diff --git a/test/receiverevidencefix/js/lib/listing.js b/test/receiverevidencefix/js/lib/listing.js new file mode 100644 index 000000000..43eb92ed6 --- /dev/null +++ b/test/receiverevidencefix/js/lib/listing.js @@ -0,0 +1,10 @@ +'use strict' +// Near miss: super.listSchemas() reaches the superclass's method. +const Schemas = require('./schemas') + +class Listing extends Schemas { + list () { + return super.listSchemas() + } +} +module.exports = Listing diff --git a/test/receiverevidencefix/js/lib/meta.js b/test/receiverevidencefix/js/lib/meta.js new file mode 100644 index 000000000..493845506 --- /dev/null +++ b/test/receiverevidencefix/js/lib/meta.js @@ -0,0 +1,19 @@ +'use strict' +// Reflect and Atomics are globals: their get/add are never this file's functions of those names. +function get (obj, key) { + return obj[key] +} + +function add (a, b) { + return a + b +} + +function peek (obj) { + return Reflect.get(obj, 'x') +} + +function bump (arr) { + return Atomics.add(arr, 0, 1) +} + +module.exports = { get, add, peek, bump } diff --git a/test/receiverevidencefix/js/lib/mirror.js b/test/receiverevidencefix/js/lib/mirror.js new file mode 100644 index 000000000..38c805f51 --- /dev/null +++ b/test/receiverevidencefix/js/lib/mirror.js @@ -0,0 +1,8 @@ +'use strict' +// Near miss: a file-local binding named Reflect IS the relative module, so its get is a real edge. +const Reflect = require('./reflect') + +function look (obj) { + return Reflect.get(obj, 'k') +} +module.exports = { look } diff --git a/test/receiverevidencefix/js/lib/reflect.js b/test/receiverevidencefix/js/lib/reflect.js new file mode 100644 index 000000000..c6bddb6c1 --- /dev/null +++ b/test/receiverevidencefix/js/lib/reflect.js @@ -0,0 +1,6 @@ +'use strict' +module.exports = { + get (obj, key) { + return obj[key] + } +} diff --git a/test/receiverevidencefix/js/lib/reply.js b/test/receiverevidencefix/js/lib/reply.js index b51cf28c7..09e4f7990 100644 --- a/test/receiverevidencefix/js/lib/reply.js +++ b/test/receiverevidencefix/js/lib/reply.js @@ -40,4 +40,15 @@ function respondWith (reply, body) { reply.send(body) } -module.exports = { Reply, awaitResult, pipePayload, writePayload, notFound, respondWith } +// Near miss for --impact inheritance: `direct` reaches send through a construction (proven), so `both` +// is reached at d=2 by a proven path as well as through respondWith's name-only edge. +function direct (r) { + return new Reply(r).send('x') +} + +function both (r) { + direct(r) + respondWith(r, 'y') +} + +module.exports = { Reply, awaitResult, pipePayload, writePayload, notFound, respondWith, direct, both } diff --git a/test/receiverevidencefix/js/lib/uses.js b/test/receiverevidencefix/js/lib/uses.js index 2b8cbd73a..098d29bac 100644 --- a/test/receiverevidencefix/js/lib/uses.js +++ b/test/receiverevidencefix/js/lib/uses.js @@ -16,4 +16,8 @@ function viaModule (reply) { return replies.respondWith(reply, 'ok') } -module.exports = { build, viaModule, Store } +function make () { + return Application.create() +} + +module.exports = { build, viaModule, make, Store } diff --git a/test/receiverevidencefix/py/src/tui/app.py b/test/receiverevidencefix/py/src/tui/app.py index 14f2823ef..a3b56f868 100644 --- a/test/receiverevidencefix/py/src/tui/app.py +++ b/test/receiverevidencefix/py/src/tui/app.py @@ -5,7 +5,12 @@ class App: """A non-self receiver in the class's own file: the file defining App is no evidence.""" def __init__(self): - self.screen = Screen() + self._screen_stack = [Screen()] + + @property + def screen(self): + """The top of the screen stack: a property, so `self.screen` carries no receiver evidence.""" + return self._screen_stack[-1] def refresh(self): return False diff --git a/test/receiverevidencefix/py/src/tui/listing.py b/test/receiverevidencefix/py/src/tui/listing.py new file mode 100644 index 000000000..f39b0dd92 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/listing.py @@ -0,0 +1,15 @@ +from .content import Content + + +def gather(nodes): + """list.append on a list literal local, in a file that imports Content (which defines append).""" + out = [] + for node in nodes: + out.append(node) + return out + + +def grow(text): + """Near miss: a constructed Content's append is that class's method.""" + c = Content(text) + return c.append("!") diff --git a/test/receiverevidencefix/py/src/tui/uses.py b/test/receiverevidencefix/py/src/tui/uses.py index 6488425b2..b6b8b93d7 100644 --- a/test/receiverevidencefix/py/src/tui/uses.py +++ b/test/receiverevidencefix/py/src/tui/uses.py @@ -22,3 +22,8 @@ def build(path): sheet.update({}) alias = Sheet() return alias.read(path) + + +def clone(styles): + """Near miss: a class-name receiver is evidence.""" + return Styles.copy(styles) diff --git a/test/receiverevidencefix/py/src/web/jsonutil.py b/test/receiverevidencefix/py/src/web/jsonutil.py new file mode 100644 index 000000000..0e65e198a --- /dev/null +++ b/test/receiverevidencefix/py/src/web/jsonutil.py @@ -0,0 +1,7 @@ +def stringify(obj): + """In-repo names that an outside from-import shadows in another file.""" + return str(obj) + + +def dumps(obj): + return repr(obj) diff --git a/test/receiverevidencefix/py/src/web/render.py b/test/receiverevidencefix/py/src/web/render.py new file mode 100644 index 000000000..44165fdf6 --- /dev/null +++ b/test/receiverevidencefix/py/src/web/render.py @@ -0,0 +1,7 @@ +from json import dumps as stringify +from json import dumps + + +def render(obj): + """`stringify` and `dumps` here are json.dumps (an outside from-import), never jsonutil's functions.""" + return stringify(obj) + dumps(obj) diff --git a/test/receiverevidencefix/ts/src/atomics.ts b/test/receiverevidencefix/ts/src/atomics.ts new file mode 100644 index 000000000..38cf9d632 --- /dev/null +++ b/test/receiverevidencefix/ts/src/atomics.ts @@ -0,0 +1,8 @@ +// Atomics is a global: its add is never this file's add, nor a router's. +export function add(a: number, b: number): number { + return a + b +} + +export function bump(arr: Int32Array): number { + return Atomics.add(arr, 0, 1) +} diff --git a/test/receiverevidencefix/ts/src/http.ts b/test/receiverevidencefix/ts/src/http.ts new file mode 100644 index 000000000..4ca2004be --- /dev/null +++ b/test/receiverevidencefix/ts/src/http.ts @@ -0,0 +1,30 @@ +import { Response as AppResponse } from './response' + +// An in-repo method named like Date#getTime. +export class Clock { + getTime(): number { + return 0 + } +} + +// reply: the global Response (this file never imports one under that name) and Date#getTime. +export function reply(body: string): Response { + const stamp = new Date().getTime() + return new Response(body + stamp) +} + +// stamped: a local Date's getTime. +export function stamped(): number { + const d = new Date() + return d.getTime() +} + +// Near misses: a constructed local of the in-repo class, and the in-repo Response through its import alias. +export function local(): number { + const c = new Clock() + return c.getTime() +} + +export function wrap(body: string): AppResponse { + return new AppResponse(body) +} diff --git a/test/receiverevidencefix/ts/src/response.ts b/test/receiverevidencefix/ts/src/response.ts new file mode 100644 index 000000000..360133dfe --- /dev/null +++ b/test/receiverevidencefix/ts/src/response.ts @@ -0,0 +1,4 @@ +// An in-repo class named like the WHATWG global Response. +export class Response { + constructor(public body: string) {} +} From c699a1c9a4af7d7d5d6413e49d4c49dd8f52dcd5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 09:54:40 -0400 Subject: [PATCH 003/229] resolve: a call bound by name alone keeps its rows, marked via="name"; typed receivers resolve A member call (or an implicit-receiver bare call) whose receiver nothing proves used to show one confident row per same-named definition the name ladder reached. Two halves (src/receiverevidence.h): Receivers the source types now RESOLVE: this/self/cls inside a class and super/base through its bases, a typed parameter or local (TS/Python/Go annotations, a Go method receiver, a JS `new Foo()` / Python `Foo()` / Go `Foo{}` initializer), a constructed receiver (`new Foo().m()`), a class-name receiver, a chain of stated field classes (`this.x = new Foo()`, Python `self.x = Foo()`, Go struct and embedded fields), Python `f = obj.m`, an import alias of the class, a Ruby mixin. Ingest records the receiver chain (memberPath, memberCtor; Java/Kotlin/C#/Swift member calls are now member calls) and four new binding kinds read by this rule alone. A call the rules leave unanswered keeps the candidates the language's own lookup proves (an implicit receiver's class and bases, a free function in scope, the module a receiver alias names); with none proven it is NAME-ONLY: every same-file and same-directory candidate (and a member call's included file's) is listed, a lone global one too, and each edge carries a per-edge bit (Graph::outNameOnly) that a proven site at the same edge clears. Rule 3's include-file narrow no longer decides a member call. --callees, --callers, --impact (a row reached by no all-proven path inherits it), --path, --connect, the rows of --expand and --for, and MCP find_symbol / find_referencing_symbols / impact / path render via="name" ("via":"name"), each with a gated legend sentence saying what it does not mean. kParserVer 141 -> 142, kCacheVersion 28 -> 29 (the train renumbers). Co-Authored-By: Claude Opus 5.5 --- src/columnar.h | 18 +- src/graph.h | 327 +++++++++++++++++++- src/graphlegend.h | 54 ++++ src/infra/fieldid.h | 4 +- src/ingest_binds.h | 482 +++++++++++++++++++++++++++++- src/ingest_cache.h | 24 +- src/ingest_model.h | 2 + src/ingest_sidecap.h | 10 +- src/main.cpp | 6 +- src/mcpverbs.h | 62 ++-- src/model.h | 32 +- src/nonlocalstate.h | 2 +- src/packtask.h | 6 +- src/quality.h | 5 +- src/receiverevidence.h | 660 +++++++++++++++++++++++++++++++++++++++++ src/serialize.h | 51 +++- src/tracelocus.h | 2 +- src/verbs_for.h | 12 +- src/verbs_navigate.h | 82 +++-- 19 files changed, 1755 insertions(+), 86 deletions(-) create mode 100644 src/receiverevidence.h diff --git a/src/columnar.h b/src/columnar.h index e75b5a805..6ebd5dd60 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -193,9 +193,12 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, const char* wrapperTag, const std::string& wrapperAttrs, const std::vector& rows, std::string_view rootPrefix = {}, const std::vector* testReach = nullptr, - const std::vector* depth = nullptr ) + const std::vector* depth = nullptr, + const std::vector* viaName = nullptr ) { std::vector esc; + // FE-B: a dense `` column (1 = the row's edge is name-only, via="name"), present only when some row carries it + const bool hasVia = viaName != nullptr && std::any_of( viaName->begin(), viaName->end(), []( char c ) { return c != 0; } ); std::vector rowFiles; rowFiles.reserve( rows.size() ); for( NodeId id : rows ) @@ -208,7 +211,8 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( kColumnarLegend, out ); // §B1.5: once per output, before the element it describes rw::emitTo( out, "<{} {} format=\"columnar\">", wrapperTag, wrapperAttrs.c_str() ); emitPathTable( out, ing, uniqueFiles, esc, rootPrefix ); - rw::emitTo( out, "", rows.size(), columnarOptionalFields( testReach != nullptr, depth != nullptr ) ); + rw::emitTo( out, "", rows.size(), columnarOptionalFields( testReach != nullptr, depth != nullptr ), + hasVia ? ",via" : "" ); // path index array std::fputs( "", out ); @@ -258,6 +262,16 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( "", out ); // A6 / 0.6.5: each present only when the caller passed it — see the wrapper banner and emitColumnarOptionalColumns. emitColumnarOptionalColumns( out, ing, rows, testReach, depth ); + if( hasVia ) + { + std::fputs( "", out ); + for( std::size_t i = 0; i < rows.size(); ++i ) + { + std::fputs( i ? "," : "", out ); + std::fputc( i < viaName->size() && ( *viaName )[ i ] != 0 ? '1' : '0', out ); + } + std::fputs( "", out ); + } rw::emitTo( out, "", wrapperTag ); } diff --git a/src/graph.h b/src/graph.h index 1310254d5..30ab382c9 100644 --- a/src/graph.h +++ b/src/graph.h @@ -19,6 +19,7 @@ #include "prconverge.h" // W2-F: RankDisclosure — the power iteration's own account, carried with its result #include "smallvec.h" // rw::SmallVec — THE ONE ALIAS (src/smallvec.h picks the implementation) #include "resolve.h" // P2-D one-hop type narrowing (Rule 1: class membership) — applied before the name-based fallback +#include "receiverevidence.h" // FE-B: typed receivers RESOLVE; a call nothing proves keeps its rows marked via="name" #include "scipoverlay.h" // SCIP precision overlay (data struct only; parser lives in scip.h) #include "pincensus.h" // eval-only per-call-site decision census (--pin-census); inert unless armed #include "externalnames.h" // Phase 5: the committed builtin/stdlib tables behind the external-name veto @@ -62,6 +63,9 @@ struct Graph // the export (JS/TS); read, not chosen by name // Empty ⇒ no overlay, no FFI/import edge and nothing split: every edge // uniquely resolved, so the whole attribute is absent (omit-at-confident). + std::vector outNameOnly; // FE-B: per-out-edge HEDGE bit, parallel to outTargets — 1 = every call site that bound this + // edge did so by NAME ALONE (no receiver evidence: receiverevidence.h), rendered via="name" + // on every surface; 0 = at least one site proved it. Empty ⇒ no edge is name-only. std::size_t scipDocsSeen = 0; // # SCIP documents consumed (0 unless --scip); honesty summary std::size_t scipEdgesPinned = 0; // # (from,to) edges the SCIP index pinned; honesty summary std::vector> implementors; // base-class id → derived class ids (inheritance/Lego view) @@ -173,6 +177,65 @@ struct Graph // emits nothing — the map is byte-identical either way. }; +// FE-B: is the call edge from → to NAME-ONLY (Graph::outNameOnly) — bound by name alone at every site that bound it, so every +// surface renders it via="name". false for an edge that does not exist and on a graph with no name-only edge at all. +inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept +{ + if( g.outNameOnly.empty() || std::size_t( from ) + 1 >= g.outOff.size() ) + { + return false; + } + const auto b = g.outTargets.begin() + g.outOff[ from ]; + const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; + const auto it = std::lower_bound( b, e, to ); // ascending within a source (buildGraph sorts by (from, to)) + return it != e && *it == to && g.outNameOnly[ std::size_t( it - g.outTargets.begin() ) ] != 0; +} + +// FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) +inline bool edgeExists( const Graph& g, NodeId from, NodeId to ) noexcept +{ + if( std::size_t( from ) + 1 >= g.outOff.size() ) + { + return false; + } + const auto b = g.outTargets.begin() + g.outOff[ from ]; + const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; + return std::binary_search( b, e, to ); +} + +// FE-B: a neighbour row of a selector that matched several definitions is via="name" iff EVERY edge joining it to one +// of them is name-only — one proven edge makes the row proven. `towardMatches`: the row is a CALLER (row → match); +// else a callee (match → row). false when no edge joins them, and on a graph with no name-only edge. +inline bool rowNameOnly( const Graph& g, std::span matches, NodeId row, bool towardMatches ) noexcept +{ + if( g.outNameOnly.empty() ) + { + return false; + } + bool any = false; + for( const NodeId m : matches ) + { + const NodeId from = towardMatches ? row : m; + const NodeId to = towardMatches ? m : row; + if( !edgeExists( g, from, to ) ) + { + continue; + } + if( !edgeNameOnly( g, from, to ) ) + { + return false; + } + any = true; + } + return any; +} + +// The via="name" spelling every XML row of a name-only edge carries (" via=\"name\"", 11 bytes), "" otherwise. +inline const char* viaNameAttr( bool nameOnly ) noexcept +{ + return nameOnly ? " via=\"name\"" : ""; +} + // The prov= VOCABULARY, spelled once. serialize's XML and serializeJson's JSON must emit identical // words for identical edges — test/mcpclidiffcheck.sh is the gate that says so — and two ternary // chains over the same integers is precisely how two dialects drift apart. Returns "" for 0 (a @@ -1142,9 +1205,9 @@ inline FnPtrBindTables buildFnPtrBindTables( const IngestResult& ing ) }; for( const Binding& b : ing.bindings ) { - if( b.kind == LocalBindKind::Type || b.kind == LocalBindKind::ParamType || b.var.empty() || b.typeName.empty() ) + if( b.kind == LocalBindKind::Type || b.kind == LocalBindKind::ParamType || isReceiverEvidenceKind( b.kind ) || b.var.empty() || b.typeName.empty() ) { - continue; // a var→TYPE fact (declared or parameter) is never a fn-pointer binding + continue; // a var→TYPE fact (declared or parameter, or FE-B's receiver evidence) is never a fn-pointer binding } const std::string& tgt = ( b.typeName == kFnBindClobberTarget ) ? emptyTarget : b.typeName; if( b.fromSymbol != kNoNode ) @@ -1343,9 +1406,9 @@ inline FieldNarrowTables buildFieldNarrowTables( const IngestResult& ing, const // Every record, an assignment's included: localNameEvidence keeps an assignment out of Rule 2b's declared bit (the // member it assigns stays typed, which is what assignmentNamesNoClass guarded here), while Rule 2c still reads it as // proof the token is a variable (test/fieldnarrowcheck.sh arm v3). - if( b.fromSymbol == kNoNode || b.var.empty() ) + if( b.fromSymbol == kNoNode || b.var.empty() || isReceiverEvidenceKind( b.kind ) ) { - continue; + continue; // FE-B's receiver evidence names fields and aliases the other bindings already record (or never declare) } key.clear(); Narrower::appendUint( key, b.fromSymbol ); @@ -2822,7 +2885,10 @@ inline void collectFileClassEvidence( const IngestResult& ing, const std::vector } for( const Binding& b : ing.bindings ) { - noteBindingClasses( gate, fileGated, b ); + if( !isReceiverEvidenceKind( b.kind ) ) // FE-B's receiver evidence is read by ReceiverEvidence alone + { + noteBindingClasses( gate, fileGated, b ); + } } for( const auto& [ importKey, bound ] : jsImports.targets ) // key "#" (jsImportKey) { @@ -3999,6 +4065,99 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const ClassIdentity classIds = buildClassIdentity( ing, chaUp ); // Rule 2's class identity: nesting, owners, real inheritance (resolve.h) std::vector filtScratch; // reused per-call survivor buffer for CHA-lite / arity filtering rw::SmallVec reachScratch; // reused per-call buffer: the candidates a call can reach by name (reachableByName) + // FE-B (receiverevidence.h): the receiver-evidence tables, the per-edge hedge sets and the reused per-call buffers. + ReceiverEvidence recvEv( ing, chaUp, classNames ); + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/2i: receiver-evidence tables" ); + recvEv.build(); + } + HashMap nameOnlyEdges; // (from<<32|to) of an edge some call bound by NAME ALONE (no receiver evidence) + HashMap provenEdges; // … and of an edge some call bound WITH evidence: a proven site wins the merged row + std::vector provedScratch; // reused: the name-ladder candidates the language's own lookup proves + std::vector memberRule3; // reused: Rule 3's included-file candidates for a MEMBER call — a hedge tier, never a proof + // FE-B: which of a call's name-ladder candidates the language's own lookup PROVES (receiverevidence.h, point 2). A + // member call's receiver must name the candidate's class (this/self/cls and the class's bases, super and the bases, + // a class-name receiver) or its module (a JS/TS/Go/Python module alias); a bare call is an implicit receiver's own + // class or a free function in scope where the receiver is implicit, and elsewhere a definition the caller's scope + // sees — its own file, a file it imports, Go's package directory — reached by a name that is not a local or a + // parameter of the caller (a call THROUGH one holds whatever was passed in). + const auto ownerIsClass = [ & ]( NodeId c ) -> std::string_view + { + const std::string& owner = recvEv.ownerClass[ c ]; + return ( !owner.empty() && classNames.contains( owner ) ) ? std::string_view( owner ) : std::string_view{}; + }; + const auto ladderProves = [ & ]( const Reference& ref, NodeId c ) -> bool + { + const Symbol& cs = ing.symbols[ c ]; + if( isMemberCallRef( ref ) ) + { + const ReceiverEvidence::Chain ch = ReceiverEvidence::chainOf( ref ); + if( !ch.valid || !ch.path.empty() || !ch.ctor.empty() ) + { + return false; // a chained or constructed receiver the resolve rule could not type proves nothing here + } + const std::string_view owner = ownerIsClass( c ); + if( isThisRoot( ch.root ) || ( ref.lang == Lang::Python && ch.root == "cls" ) ) + { + const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); + if( !callerCls.empty() ) + { + return !owner.empty() && recvEv.inCone( callerCls, owner ); + } + return owner.empty() && cs.fileId == ref.fileId && cs.kind == SymKind::Method; // an object literal's own member + } + if( isSuperRoot( ch.root ) ) + { + const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); + return !callerCls.empty() && !owner.empty() && owner != callerCls && recvEv.inCone( callerCls, owner ); + } + if( recvEv.moduleNames( ref, ch.root, c ) ) + { + return true; + } + if( ref.lang == Lang::Python && ref.recv == RecvKind::NamedVar ) + { + bindKey.clear(); Narrower::appendUint( bindKey, ref.fileId ); bindKey.push_back( '#' ); bindKey.append( ref.recvVar ); + if( const auto ait = extVeto.importBindFile.find( bindKey ); ait != extVeto.importBindFile.end() && ait->second == cs.fileId ) + { + return true; + } + } + return !owner.empty() && classNames.contains( std::string( ch.root ) ) && recvEv.typedLocal( ref.fromSymbol, ch.root ) == nullptr + && recvEv.inCone( ch.root, owner ); // a class-name receiver: the class or a base (`Cls.create()`, `Base.m( self )`) + } + if( implicitReceiverLang( ref.lang ) ) + { + const std::string_view owner = ownerIsClass( c ); + if( owner.empty() ) + { + return true; // a free / top-level function + } + const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); + return !callerCls.empty() && recvEv.inCone( callerCls, owner ); + } + if( ref.lang == Lang::Rust || ref.lang == Lang::C ) + { + return true; // FE-A's rules already keep a receiverless call to what its scope reaches + } + if( falseEdges.isLocalOfCaller( ref ) ) + { + return false; + } + if( cs.fileId == ref.fileId ) + { + return !localDefOutOfReach( ing, c, ref ); + } + if( ref.fileId < fileIncludes.size() && std::binary_search( fileIncludes[ ref.fileId ].begin(), fileIncludes[ ref.fileId ].end(), symFileId[ c ] ) ) + { + return true; + } + if( ref.lang == Lang::Go ) + { + return fileDir[ cs.fileId ] == fileDir[ ref.fileId ]; // one package is one directory + } + return ( ref.lang == Lang::TypeScript || ref.lang == Lang::JavaScript ) && rootRelPath( ing, cs.fileId ).ends_with( ".d.ts" ); + }; // ---- census arming + the ORACLE side (eval-only; src/pincensus.h) ------------------------------ // The oracle rows are a straight transcription of the overlay's own (from, calleeName) → target table @@ -4417,6 +4576,20 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null fieldTypeNarrowed = narrowed; } const bool receiverTypeNarrowed = narrowed && !narrowedBeforeReceiverRules; // Rule 2, 2c or 2b chose the candidates (S6-C reads it) + // FE-B receiver evidence (receiverevidence.h, point 1): a receiver whose class the source states — `this`/`super` + // in a class, a typed parameter or local, a constructed receiver, a class-name receiver, a chain of stated field + // classes, Python's `feed = parser.feed` — resolves to that class's member or the shallowest base defining it. + if( !scipPinned && !canonical && !narrowed && recvEv.narrow( r ) ) + { + for( NodeId c : recvEv.found ) + { + if( langCompatible( ing.symbols[ c ].lang, r.lang ) && sameRoot( c, r.fileId ) ) + { + cand.push_back( c ); + } + } + narrowed = !cand.empty(); + } // Python Rule 2d (module-ALIAS receiver narrow, issue #287): `alias.m(...)` where `alias` is bound in // the caller's file by `import X as alias` / `import X` / `from pkg import X as alias` to a module X // that resolves to EXACTLY ONE indexed file (ExternalVetoTables::importBindFile — Step-A, plus the @@ -4498,16 +4671,23 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null && reachableByName( ing, *baseIds, r, reachScratch ) && !( r.role == RefRole::Call && r.qualifier.empty() && bindingTier.empty() && externalVeto.isExternalBound( r ) ); const rw::SmallVec* nameIds = localsYield ? &reachScratch : baseIds; + // FE-B: for a MEMBER call the include graph says which file the caller imports, never what its receiver is — the + // included file's definitions join the name-only tier below instead of deciding the call. + const bool hedgeEligible = !scipPinned && !canonical && receiverHedgeLang( r.lang ) && r.role == RefRole::Call; + memberRule3.clear(); if( !scipPinned && !canonical && !narrowed && it != byName.end() ) { if( narrower.rule3IncludeFile( *nameIds, r.fileId, rule3Out, localsYield ? 1u : 2u ) || ( localsYield && narrower.rule3IncludeFile( *baseIds, r.fileId, rule3Out ) ) ) { + // an implicit receiver's bare call is the same question: the included file's definition must be one the + // language's lookup reaches (a free function, the class's own cone) — decided with the ladder's proof below + std::vector& into = ( hedgeEligible && ( isMemberCallRef( r ) || implicitReceiverLang( r.lang ) ) ) ? memberRule3 : cand; for( NodeId c : rule3Out ) { if( langCompatible( ing.symbols[c].lang, r.lang ) ) { - cand.push_back( c ); + into.push_back( c ); } } narrowed = !cand.empty(); @@ -4625,6 +4805,51 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null continue; } + // ---- FE-B: PROVEN or NAME-ONLY (receiverevidence.h, point 2). A call no rule above answered binds by name; the + // candidates the language's own lookup proves are its answer (the rest never were candidates of that lookup), and + // with none proven the call is NAME-ONLY: the tier below keeps every same-file and same-directory candidate (and a + // member call's included-file ones) instead of one locality pick, each edge marked via="name" — D1 holds a single + // candidate to the same rule, because one candidate is not evidence. + bool nameOnly = false; + if( hedgeEligible && !narrowed && !cand.empty() && r.lang != Lang::Elixir ) + { + provedScratch.clear(); + if( !memberRule3.empty() && !isMemberCallRef( r ) ) + { + // an implicit receiver's Rule 3 answer, kept where the lookup proves it — Rule 3's own decision, as before + for( NodeId c : memberRule3 ) + { + if( ladderProves( r, c ) ) + { + provedScratch.push_back( c ); + } + } + if( !provedScratch.empty() ) + { + cand.swap( provedScratch ); + narrowed = true; + } + } + if( !narrowed ) + { + provedScratch.clear(); + for( NodeId c : cand ) + { + if( ladderProves( r, c ) ) + { + provedScratch.push_back( c ); + } + } + if( provedScratch.empty() ) + { + nameOnly = true; + } + else if( provedScratch.size() < cand.size() ) + { + cand.swap( provedScratch ); + } + } + } // ---- tier ladder (the name-based fallback) — SKIPPED when SCIP pinned this site, and for Rule 2's class-identity CLAIM (a type fact, not a locality guess) const bool identityClaim = narrowed && narrower.identityClaimFor( r ); if( !scipPinned && ( r.lang == Lang::Elixir || identityClaim ) ) { tier = cand; } @@ -4672,6 +4897,38 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null continue; } } + else if( nameOnly ) + { + // FE-B: every local candidate, hedged — the same file, the same directory, a member call's included file + bool anyFile = false, anyDir = false; + const std::uint32_t rdir = fileDir[ r.fileId ]; + for( NodeId c : cand ) + { + const bool file = ing.symbols[ c ].fileId == r.fileId; + const bool dir = fileDir[ ing.symbols[ c ].fileId ] == rdir; + if( file || dir || std::find( memberRule3.begin(), memberRule3.end(), c ) != memberRule3.end() ) + { + tier.push_back( c ); + anyFile = anyFile || file; + anyDir = anyDir || dir; + } + } + tierConf = anyFile ? 1.0f : anyDir ? 0.5f : 0.2f; + if( tier.empty() ) + { + if( cand.size() == 1 && !localsYield ) + { + tier = cand; // a unique global, hedged + } + else + { + ++g.declinedOut[ r.fromSymbol ]; + internDeclinedList( g, declinedListsByHash, cand ); + disposition = CallDisposition::Declined; + continue; + } + } + } else { for( NodeId c : cand ) @@ -4826,7 +5083,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // declaring the member is a genuine split (the shape Rule 2c also keeps whole), not a locality race. if( !scipPinned && !bindingPinned && r.lang != Lang::Elixir && tier.size() > 1 && !ing.symbols[ r.fromSymbol ].scope.empty() && r.recv != RecvKind::FieldOfThis && r.recv != RecvKind::FieldOfVar && r.recv != RecvKind::SuperObj && !identityClaim - && !isJsTsLitRecv( r.recv ) && r.recv != RecvKind::JavaTypeCandidate ) + && !isJsTsLitRecv( r.recv ) && r.recv != RecvKind::JavaTypeCandidate && !nameOnly ) // FE-B: a name-only call keeps every candidate { const std::string& callerCanon = g.localityKey[ r.fromSymbol ]; // == canonId here (the caller is scoped) const std::size_t localityCap = receiverLocalityCap( r, receiverTypeNarrowed, ing.files[ ing.symbols[ r.fromSymbol ].fileId ] ); @@ -5073,6 +5330,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { finalSegmentEdges[ekey] = 1; // remember (from,to) for prov="final-segment" } + ( nameOnly ? nameOnlyEdges : provenEdges )[ ekey ] = 1; // FE-B: a proven site wins the merged row (Graph::outNameOnly) } disposition = CallDisposition::Bound; } @@ -5132,6 +5390,12 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.outProv.assign( edges.size(), 0u ); } const EdgeProvenanceSets provSets{ bindingEdges, importEdges, splitEdges, finalSegmentEdges }; + // FE-B: the hedge bit, allocated only when some edge is name-only (empty ⇒ every edge proven, nothing marked anywhere) + const auto hedged = [ & ]( std::uint64_t k ) { return nameOnlyEdges.contains( k ) && !provenEdges.contains( k ); }; + if( std::any_of( edges.begin(), edges.end(), [ & ]( const E& e ) { return hedged( ( std::uint64_t( e.from ) << 32 ) | e.to ); } ) ) + { + g.outNameOnly.assign( edges.size(), 0u ); + } { std::vector cur( g.outOff.begin(), g.outOff.begin() + N ); for( const E& e : edges ) @@ -5148,6 +5412,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { g.outProv[ pos ] = edgeProvenance( scip && scip->isPrecise( e.from, e.to ), provSets, ( std::uint64_t( e.from ) << 32 ) | e.to ); } + if( !g.outNameOnly.empty() ) + { + g.outNameOnly[ pos ] = hedged( ( std::uint64_t( e.from ) << 32 ) | e.to ) ? 1u : 0u; + } } } @@ -8268,6 +8536,51 @@ inline std::vector transitiveCallersDepth( const Graph& g, std::span transitiveCallers( const Graph& g, std::span seeds ) { return transitiveCallersDepth( g, seeds, nullptr ); } +// FE-B (ruling D3): the callers that reach a seed through an ALL-PROVEN path — every edge on it bound with receiver +// evidence (Graph::outNameOnly 0). An --impact row outside this set is reached only through some name-only edge and +// inherits via="name"; a row with ANY all-proven path stays plain, at whatever depth. Indexed by node id; EMPTY (no row +// hedged) on a graph with no name-only edge, so such a corpus pays one emptiness test. +inline std::vector provenCallerReach( const Graph& g, std::span seeds ) +{ + std::vector proven; + if( g.outNameOnly.empty() ) + { + return proven; + } + const std::size_t N = g.wOutDeg.size(); + std::vector q; + proven.assign( N, 0 ); + for( NodeId s : seeds ) + { + if( s < N && !proven[s] ) + { + proven[s] = 1; + q.push_back( s ); + } + } + const auto* ro = g.inEdges.rowOffsets(); + const auto* ci = g.inEdges.colIndices(); + for( std::size_t head = 0; head < q.size(); ++head ) + { + const NodeId u = q[ head ]; + for( std::uint32_t k = ro[u]; k < ro[u + 1]; ++k ) + { + const NodeId c = ci[k]; + if( c < N && !proven[c] && !edgeNameOnly( g, c, u ) ) + { + proven[c] = 1; + q.push_back( c ); + } + } + } + return proven; +} +// the --impact row's hedge: reached, but by no all-proven path +inline bool impactRowNameOnly( const std::vector& proven, NodeId n ) noexcept +{ + return !proven.empty() && n < proven.size() && proven[n] == 0; +} + // ── Depth-labelled --impact (0.6.5) — the hop depth the walk above already records, put on the listing ────── // A flat blast radius cannot tell a direct caller from a four-hop dependent, and a page window cut over a // PageRank-only order drops rows of every depth at once, so a capped answer had no clean boundary. Both diff --git a/src/graphlegend.h b/src/graphlegend.h index b35e988e2..4529fdef7 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -854,6 +854,60 @@ inline constexpr const char* kModScopeLegend = "t=\"modscope\" is a row for a file's MODULE SCOPE, named : the statements outside every named definition, which is where a top-level call and an anonymous callback body's calls live. It is a CALLER, not a function — nothing in the source can name it, so it never appears as a callee, and it has no body, so expanding it returns none. A file that has no such call has no such row. "; inline const char* modScopeLegend( bool on ) noexcept { return on ? kModScopeLegend : ""; } +// FE-B (test/receiverevidencecheck.sh): the reading of via="name" — ONE wording for every surface that marks a row with it +// (--callees/--callers, --impact, --path, --connect's , the rows of --expand and --for, and MCP's "via":"name"). +// Gated like modScopeLegend: an answer carrying no such row pays 0 bytes. What it does NOT mean is said in the sentence, +// because the attribute reads as a verdict and is not one. +inline constexpr const char* kViaNameLegend = + "via=\"name\" on a row: the target was matched by name alone, because nothing (self/this, a construction, a type annotation, " + "an import, the class's bases) proves the receiver is that class; every by-name candidate in reach is listed (same file and " + "directory, else a lone definition), and any one of them, or none (a builtin or an outside object), may be the real target. " + "It does NOT mean the edge is false, and a row without it is evidence-bound, not type-checked. "; +inline constexpr const char* kViaNameImpactClause = + "An --impact row reached only through such an edge carries it too; one that some all-proven path reaches stays plain. "; +inline constexpr const char* kViaNameColumnarClause = "format=columnar carries it as the column (1 = via=\"name\"). "; +inline std::string viaNameLegend( bool present, bool impact = false, bool columnar = false ) +{ + if( !present ) + { + return {}; + } + std::string out = kViaNameLegend; + if( impact ) + { + out += kViaNameImpactClause; + } + if( columnar ) + { + out += kViaNameColumnarClause; + } + return out; +} +// …as its own comment node, for the surfaces whose legend is written beside the rows (a / block's ) +inline std::string viaNameLegendComment() +{ + return std::string( ""; +} +// …and MCP's spelling: the same sentence as the `via_note` key of an answer one of whose entries carries "via":"name". +inline std::string viaNameNoteJson() +{ + std::string text = kViaNameLegend; + for( std::size_t at = text.find( "via=\"name\"" ); at != std::string::npos; at = text.find( "via=\"name\"", at ) ) + { + text.replace( at, 10, "\\\"via\\\":\\\"name\\\"" ); // via="name" -> \"via\":\"name\" inside the JSON string + at += 16; + } + if( const std::size_t at = text.find( "on a row" ); at != std::string::npos ) + { + text.replace( at, 8, "on an entry" ); + } + if( !text.empty() && text.back() == ' ' ) + { + text.pop_back(); + } + return ",\"via_note\":\"" + text + "\""; +} + // The same fact in --for's two dialects, which are prose strips joined by ';' and ':' rather than XML comment // clauses — one constant each so the wording cannot drift from kModScopeLegend above. Present-only, on that // verb's own `modScopePresent` bit (verbs_for.h), like every other clause in those two strips. diff --git a/src/infra/fieldid.h b/src/infra/fieldid.h index 561ade969..8f71256f9 100644 --- a/src/infra/fieldid.h +++ b/src/infra/fieldid.h @@ -70,7 +70,7 @@ enum class NodeField : std::uint8_t Alias, Alternative, Argument, Arguments, Attribute, Body, Captures, Condition, Consequence, Constructor, Declaration, Declarator, DefaultValue, Definition, Directive, - Field, Function, Initializer, Key, Left, + Expression, Field, Function, Initializer, Key, Left, Method, ModuleName, Name, Object, Operand, Operator, Parameter, Parameters, Path, Pattern, Property, Receiver, Right, Scope, Source, Subject, @@ -99,7 +99,7 @@ inline constexpr std::array kNodeFieldNames = { { NodeField::Captures, "captures", 8 }, { NodeField::Condition, "condition", 9 }, { NodeField::Consequence, "consequence", 11 }, { NodeField::Constructor, "constructor", 11 }, { NodeField::Declaration, "declaration", 11 }, { NodeField::Declarator, "declarator", 10 }, { NodeField::DefaultValue, "default_value", 13 }, { NodeField::Definition, "definition", 10 }, { NodeField::Directive, "directive", 9 }, - { NodeField::Field, "field", 5 }, { NodeField::Function, "function", 8 }, { NodeField::Initializer, "initializer", 11 }, + { NodeField::Expression, "expression", 10 }, { NodeField::Field, "field", 5 }, { NodeField::Function, "function", 8 }, { NodeField::Initializer, "initializer", 11 }, { NodeField::Key, "key", 3 }, { NodeField::Left, "left", 4 }, { NodeField::Method, "method", 6 }, { NodeField::ModuleName, "module_name", 11 }, { NodeField::Name, "name", 4 }, { NodeField::Object, "object", 6 }, { NodeField::Operand, "operand", 7 }, { NodeField::Operator, "operator", 8 }, { NodeField::Parameter, "parameter", 9 }, diff --git a/src/ingest_binds.h b/src/ingest_binds.h index fd42bc3fc..d4dbb3766 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -68,8 +68,11 @@ struct RecvShape std::string var; std::string field; bool viaArrow = false; - bool member = false; // FE-A: Go/JS/TS/Rust — the callee is a member access's field (model.h Reference::memberCall) + bool member = false; // FE-A: Go/JS/TS/Rust — the callee is a member access's field (model.h Reference::memberCall); + // FE-B adds Java, Kotlin, C# and Swift, whose member calls recorded no shape at all std::string root; // FE-A: that access's receiver-chain root identifier, "" when not an identifier + std::string path; // FE-B: the member names between root and the callee, '.'-joined (model.h Reference::memberPath) + std::string ctor; // FE-B: the class a constructed receiver names (model.h Reference::memberCtor) }; // FE-A (model.h Reference::memberRoot): the ROOT identifier of a Go/JS/TS/Rust receiver chain — `crypto` for @@ -107,6 +110,158 @@ inline std::string memberChainRoot( TSNode recv, std::string_view src ) return {}; } +// FE-B (model.h Reference::memberPath / memberCtor): one receiver chain, walked from the call's immediate receiver down to +// its root, for the languages whose member calls carry no RecvKind (JS/TS, Go, Java, Kotlin, C#, Swift). `root` keeps +// memberChainRoot's answer for JS/TS and Go (FE-A reads it) and adds `super` (JS/TS/Java/Kotlin/Swift; C#'s `base`), +// `this`/`self` spellings of the other languages, which FE-A never sees. `path` collects the member names passed on +// the way down, in source order; `ctor` is the class a constructed receiver names (`new X( … )`, Go `X{ … }` or +// `&X{ … }`), final segment, with root "". Any other node (a call, a subscript, a literal) ends the walk with root "" +// and an empty path. Bounded by the chain's depth like memberChainRoot. +struct MemberChain +{ + std::string root; + std::string path; + std::string ctor; +}; + +inline std::string_view finalTypeSegment( TSNode typeNode, std::string_view src ) +{ + for( int guard = 0; guard < 8 && !ts_node_is_null( typeNode ); ++guard ) + { + const char* t = ts_node_type( typeNode ); + if( kindIs( t, "type_identifier" ) || kindIs( t, "identifier" ) || kindIs( t, "simple_identifier" ) ) + { + return pattern::nodeText( typeNode, src ); + } + if( kindIs( t, "qualified_type" ) ) // Go `pkg.T` + { + typeNode = fieldChild( typeNode, NodeField::Name ); + } + else if( kindIs( t, "pointer_type" ) || kindIs( t, "generic_type" ) || kindIs( t, "parenthesized_type" ) ) + { + typeNode = ts_node_named_child_count( typeNode ) > 0 ? ts_node_named_child( typeNode, ts_node_named_child_count( typeNode ) - 1 ) : TSNode{}; + if( !ts_node_is_null( typeNode ) && kindIs( t, "generic_type" ) ) + { + typeNode = ts_node_named_child( ts_node_parent( typeNode ), 0 ); // `Foo`: the name, never the argument list + } + } + else if( kindIs( t, "member_expression" ) || kindIs( t, "scoped_type_identifier" ) || kindIs( t, "nested_identifier" ) ) + { + const std::uint32_t n = ts_node_named_child_count( typeNode ); + typeNode = n > 0 ? ts_node_named_child( typeNode, n - 1 ) : TSNode{}; + if( !ts_node_is_null( typeNode ) && kindIs( ts_node_type( typeNode ), "property_identifier" ) ) + { + return pattern::nodeText( typeNode, src ); + } + } + else + { + return {}; + } + } + return {}; +} + +// the class a constructed receiver names, final segment; "" when `node` constructs nothing this rule reads +inline std::string_view constructedClass( TSNode node, Lang lang, std::string_view src ) +{ + const char* t = ts_node_type( node ); + if( ( lang == Lang::JavaScript || lang == Lang::TypeScript ) && kindIs( t, "new_expression" ) ) + { + return finalTypeSegment( fieldChild( node, NodeField::Constructor ), src ); + } + if( ( lang == Lang::Java || lang == Lang::CSharp ) && kindIs( t, "object_creation_expression" ) ) + { + return finalTypeSegment( fieldChild( node, NodeField::Type ), src ); + } + if( lang == Lang::Go && kindIs( t, "composite_literal" ) ) + { + return finalTypeSegment( fieldChild( node, NodeField::Type ), src ); + } + if( lang == Lang::Go && kindIs( t, "unary_expression" ) && ts_node_named_child_count( node ) == 1 ) + { + const TSNode operand = ts_node_named_child( node, 0 ); + return kindIs( ts_node_type( operand ), "composite_literal" ) ? finalTypeSegment( fieldChild( operand, NodeField::Type ), src ) : std::string_view{}; + } + return {}; +} + +inline MemberChain memberChainOf( TSNode recv, Lang lang, std::string_view src ) +{ + MemberChain out; + std::vector names; // collected receiver-first (innermost member last), reversed below + TSNode node = recv; + for( int depth = 0; depth < 64 && !ts_node_is_null( node ); ++depth ) + { + const char* t = ts_node_type( node ); + if( kindIs( t, "parenthesized_expression" ) && ts_node_named_child_count( node ) == 1 ) + { + node = ts_node_named_child( node, 0 ); + continue; + } + if( kindIs( t, "identifier" ) || kindIs( t, "this" ) || kindIs( t, "self" ) || kindIs( t, "package_identifier" ) || kindIs( t, "super" ) + || kindIs( t, "simple_identifier" ) || kindIs( t, "this_expression" ) || kindIs( t, "super_expression" ) || kindIs( t, "self_expression" ) + || kindIs( t, "base_expression" ) ) + { + out.root = std::string( pattern::nodeText( node, src ) ); + break; + } + TSNode next{}; + TSNode name{}; + if( kindIs( t, "member_expression" ) ) // JS/TS + { + next = fieldChild( node, NodeField::Object ); name = fieldChild( node, NodeField::Property ); + } + else if( kindIs( t, "selector_expression" ) ) // Go + { + next = fieldChild( node, NodeField::Operand ); name = fieldChild( node, NodeField::Field ); + } + else if( kindIs( t, "field_access" ) ) // Java + { + next = fieldChild( node, NodeField::Object ); name = fieldChild( node, NodeField::Field ); + } + else if( kindIs( t, "member_access_expression" ) ) // C# + { + next = fieldChild( node, NodeField::Expression ); name = fieldChild( node, NodeField::Name ); + } + else if( kindIs( t, "navigation_expression" ) && ts_node_named_child_count( node ) == 2 ) // Kotlin / Swift + { + next = ts_node_named_child( node, 0 ); + const TSNode suffix = ts_node_named_child( node, 1 ); + name = ts_node_named_child_count( suffix ) > 0 ? ts_node_named_child( suffix, ts_node_named_child_count( suffix ) - 1 ) : TSNode{}; + } + else + { + const std::string_view ctor = constructedClass( node, lang, src ); + if( ctor.empty() ) + { + return MemberChain{}; // a call, subscript or literal: no root identifier and no path + } + out.ctor = std::string( ctor ); + break; + } + if( ts_node_is_null( next ) || ts_node_is_null( name ) ) + { + return MemberChain{}; + } + names.push_back( pattern::nodeText( name, src ) ); + node = next; + } + if( out.root.empty() && out.ctor.empty() ) + { + return MemberChain{}; + } + for( auto it = names.rbegin(); it != names.rend(); ++it ) + { + if( !out.path.empty() ) + { + out.path.push_back( '.' ); + } + out.path.append( *it ); + } + return out; +} + // FE-A: the member-access node kind and its receiver field for the languages whose receiverOf records no shape (Go, JS/TS, // Rust; and C, whose only member call is a call through a function-pointer field). inline TSNode memberOnlyReceiver( TSNode parent, Lang lang ) noexcept @@ -134,6 +289,28 @@ inline TSNode memberOnlyReceiver( TSNode parent, Lang lang ) noexcept { return fieldChild( parent, NodeField::Argument ); // `ops->open( x )`: a call through a function-pointer FIELD } + // FE-B: the implicit-receiver languages whose member calls recorded no shape, so a bare `flush()` and `w.flush()` + // were one reference. A receiver here makes the call a member call; its absence keeps it bare (an implicit this). + if( lang == Lang::Java && kindIs( t, "method_invocation" ) ) + { + return fieldChild( parent, NodeField::Object ); // null for a bare `flush()` + } + if( lang == Lang::CSharp && kindIs( t, "member_access_expression" ) ) + { + return fieldChild( parent, NodeField::Expression ); + } + if( lang == Lang::CSharp && kindIs( t, "member_binding_expression" ) ) + { + const TSNode cond = ts_node_parent( parent ); // `w?.Bump()`: the conditional access holds the receiver + return ( !ts_node_is_null( cond ) && kindIs( ts_node_type( cond ), "conditional_access_expression" ) && ts_node_named_child_count( cond ) > 0 ) + ? ts_node_named_child( cond, 0 ) : TSNode{}; + } + if( ( lang == Lang::Kotlin || lang == Lang::Swift ) && kindIs( t, "navigation_suffix" ) ) + { + const TSNode nav = ts_node_parent( parent ); + return ( !ts_node_is_null( nav ) && kindIs( ts_node_type( nav ), "navigation_expression" ) && ts_node_named_child_count( nav ) > 0 ) + ? ts_node_named_child( nav, 0 ) : TSNode{}; + } return TSNode{}; } @@ -708,7 +885,15 @@ inline RecvShape receiverOf( TSNode nameNode, Lang lang, std::string_view src ) } RecvShape member; // recv stays None (non-literal TS/JS, every Go/Rust member call); FE-A marks the shape member.member = true; - member.root = memberChainRoot( obj, src ); + if( lang == Lang::Rust || lang == Lang::C ) + { + member.root = memberChainRoot( obj, src ); // FE-A's root only: no receiver-evidence rule reads these two + return member; + } + MemberChain chain = memberChainOf( obj, lang, src ); // FE-B: root + path + constructed class + member.root = std::move( chain.root ); + member.path = std::move( chain.path ); + member.ctor = std::move( chain.ctor ); return member; } if( !isMemberAccessNode( ts_node_type( parent ), lang ) ) @@ -2384,6 +2569,298 @@ struct BindCtx bool cFamilyFn = false; }; +// ── FE-B receiver-evidence capture (model.h LocalBindKind::RecvType / MemberType / MethodAlias) ────────────────────────── +// The class a written TYPE names, final segment — "" for anything that names no single class (an array, a union, a +// string annotation, a generic's argument). Unwraps the annotation wrappers each grammar puts around the type. +inline std::string_view annotatedClass( TSNode node, std::string_view src ) +{ + for( int guard = 0; guard < 8 && !ts_node_is_null( node ); ++guard ) + { + const char* t = ts_node_type( node ); + if( kindIs( t, "identifier" ) || kindIs( t, "type_identifier" ) || kindIs( t, "simple_identifier" ) ) + { + return nodeTextOf( node, src ); + } + if( kindIs( t, "type" ) || kindIs( t, "type_annotation" ) || kindIs( t, "pointer_type" ) || kindIs( t, "parenthesized_type" ) ) + { + node = ts_node_named_child_count( node ) == 1 ? ts_node_named_child( node, 0 ) : TSNode{}; + } + else if( kindIs( t, "qualified_type" ) ) // Go `pkg.T` + { + node = fieldChild( node, NodeField::Name ); + } + else if( kindIs( t, "attribute" ) ) // Python `mod.T` + { + node = fieldChild( node, NodeField::Attribute ); + } + else if( kindIs( t, "nested_type_identifier" ) ) // TS `ns.T` + { + node = fieldChild( node, NodeField::Name ); + } + else if( kindIs( t, "generic_type" ) ) // TS `T` / Go `T[U]`: the class, not the argument + { + node = ts_node_named_child_count( node ) > 0 ? ts_node_named_child( node, 0 ) : TSNode{}; + } + else + { + return {}; + } + } + return {}; +} + +inline void pushEvidenceBind( BindCtx& cx, LocalBindKind kind, std::string_view var, std::string_view type, std::string_view imported, + std::uint32_t at, bool flag ) +{ + EXPECTS( isReceiverEvidenceKind( kind ), "only the receiver-evidence kinds ride this emitter" ); + if( type.empty() || ( var.empty() && !( kind == LocalBindKind::RecvType && flag ) ) ) + { + return; // a nameless record is kept only for a method's own class (Go `func (T) m()`, JS `T.prototype.m = …`) + } + RawBind b; + b.fileId = cx.fileId; + b.startByte = at; + b.lang = cx.lang; + b.kind = kind; + b.isFromAssignment = flag; + b.var.assign( var ); + b.typeName.assign( type ); + b.importedName.assign( imported ); + cx.binds->push_back( std::move( b ) ); +} + +// the class a constructor-shaped initializer names — JS/TS `new Foo( … )`, Python `Foo( … )`, Go `Foo{ … }` / `&Foo{ … }` +inline std::string_view constructedBy( TSNode value, Lang lang, std::string_view src ) +{ + if( ts_node_is_null( value ) ) + { + return {}; + } + if( lang == Lang::Python ) + { + if( !kindIs( ts_node_type( value ), "call" ) ) + { + return {}; + } + const TSNode fn = fieldChild( value, NodeField::Function ); + return ( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "identifier" ) ) ? nodeTextOf( fn, src ) : std::string_view{}; + } + return constructedClass( value, lang, src ); +} + +// FE-B: one node's receiver evidence. Python, JS/TS and Go only — the languages whose member calls carry no type rule today. +inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) +{ + const Lang lang = cx.lang; + const std::string_view src = cx.src; + const std::uint32_t at = ts_node_start_byte( n ); + if( lang == Lang::Python ) + { + if( kindIs( t, "typed_parameter" ) || kindIs( t, "typed_default_parameter" ) ) + { + TSNode name = fieldChild( n, NodeField::Name ); + if( ts_node_is_null( name ) && ts_node_named_child_count( n ) > 0 ) + { + name = ts_node_named_child( n, 0 ); // typed_parameter spells its identifier without a field + } + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); + } + } + else if( kindIs( t, "import_from_statement" ) ) + { + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "aliased_import" ) ) + { + const TSNode name = fieldChild( c, NodeField::Name ); + const TSNode alias = fieldChild( c, NodeField::Alias ); + if( !ts_node_is_null( name ) && !ts_node_is_null( alias ) ) + { + pushEvidenceBind( cx, LocalBindKind::NameAlias, nodeTextOf( alias, src ), finalSegment( nodeTextOf( name, src ) ), {}, ts_node_start_byte( c ), false ); + } + } + return true; + } ); + } + else if( kindIs( t, "assignment" ) ) + { + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); + if( ts_node_is_null( lhs ) ) + { + return; + } + const char* lt = ts_node_type( lhs ); + if( kindIs( lt, "identifier" ) && !ts_node_is_null( fieldChild( n, NodeField::Type ) ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( lhs, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); + } + else if( kindIs( lt, "identifier" ) && !ts_node_is_null( rhs ) && kindIs( ts_node_type( rhs ), "attribute" ) ) + { + const TSNode obj = fieldChild( rhs, NodeField::Object ); + const TSNode attr = fieldChild( rhs, NodeField::Attribute ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::MethodAlias, nodeTextOf( lhs, src ), nodeTextOf( obj, src ), nodeTextOf( attr, src ), at, false ); + } + } + else if( kindIs( lt, "attribute" ) ) + { + const TSNode obj = fieldChild( lhs, NodeField::Object ); + const TSNode attr = fieldChild( lhs, NodeField::Attribute ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) && nodeTextOf( obj, src ) == "self" ) + { + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( attr, src ), constructedBy( rhs, lang, src ), {}, at, false ); + } + } + } + return; + } + if( lang == Lang::JavaScript || lang == Lang::TypeScript ) + { + if( lang == Lang::JavaScript && kindIs( t, "variable_declarator" ) ) // TS records these as Type already + { + const TSNode name = fieldChild( n, NodeField::Name ); + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), constructedBy( fieldChild( n, NodeField::Value ), lang, src ), {}, at, false ); + } + } + else if( lang == Lang::TypeScript && ( kindIs( t, "required_parameter" ) || kindIs( t, "optional_parameter" ) ) ) + { + const TSNode name = fieldChild( n, NodeField::Pattern ); + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); + } + } + else if( lang == Lang::TypeScript && kindIs( t, "public_field_definition" ) ) + { + const TSNode name = fieldChild( n, NodeField::Name ); + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "property_identifier" ) ) + { + std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + if( type.empty() ) + { + type = constructedBy( fieldChild( n, NodeField::Value ), lang, src ); + } + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( name, src ), type, {}, at, false ); + } + } + else if( kindIs( t, "assignment_expression" ) ) + { + const TSNode lhs = fieldChild( n, NodeField::Left ); + if( ts_node_is_null( lhs ) || !kindIs( ts_node_type( lhs ), "member_expression" ) ) + { + return; + } + const TSNode obj = fieldChild( lhs, NodeField::Object ); + const TSNode prop = fieldChild( lhs, NodeField::Property ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( prop ) && kindIs( ts_node_type( obj ), "this" ) && kindIs( ts_node_type( prop ), "property_identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( prop, src ), constructedBy( fieldChild( n, NodeField::Right ), lang, src ), {}, at, false ); + } + // `Foo.prototype.m = function …`: the member's class, recorded INSIDE the function so the record attributes to it + const TSNode rhs = fieldChild( n, NodeField::Right ); + if( !ts_node_is_null( obj ) && kindIs( ts_node_type( obj ), "member_expression" ) && !ts_node_is_null( rhs ) + && ( kindIs( ts_node_type( rhs ), "function_expression" ) || kindIs( ts_node_type( rhs ), "function" ) || kindIs( ts_node_type( rhs ), "arrow_function" ) ) + && nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "prototype" ) + { + const TSNode ctor = fieldChild( obj, NodeField::Object ); + if( !ts_node_is_null( ctor ) && kindIs( ts_node_type( ctor ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, {}, nodeTextOf( ctor, src ), {}, ts_node_start_byte( rhs ), true ); + } + } + } + return; + } + if( lang != Lang::Go ) + { + return; + } + if( kindIs( t, "parameter_declaration" ) ) + { + const TSNode list = ts_node_parent( n ); + const TSNode method = ts_node_is_null( list ) ? TSNode{} : ts_node_parent( list ); + const bool receiver = !ts_node_is_null( method ) && kindIs( ts_node_type( method ), "method_declaration" ) + && ts_node_eq( fieldChild( method, NodeField::Receiver ), list ); + const std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + bool named = false; + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); + if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "identifier" ) ) + { + named = true; + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( c, src ), type, {}, at, receiver ); + } + return true; + } ); + if( !named && receiver ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, {}, type, {}, at, true ); // `func (T) m()`: the class alone + } + } + else if( kindIs( t, "var_spec" ) ) + { + std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + const TSNode values = fieldChild( n, NodeField::Value ); + if( type.empty() && !ts_node_is_null( values ) && ts_node_named_child_count( values ) == 1 ) + { + type = constructedBy( ts_node_named_child( values, 0 ), lang, src ); + } + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); + if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( c, src ), type, {}, at, false ); + } + return true; + } ); + } + else if( kindIs( t, "short_var_declaration" ) ) + { + const TSNode left = fieldChild( n, NodeField::Left ); + const TSNode right = fieldChild( n, NodeField::Right ); + if( ts_node_is_null( left ) || ts_node_is_null( right ) || ts_node_named_child_count( left ) != 1 || ts_node_named_child_count( right ) != 1 ) + { + return; // `a, b := …`: which value names which class is not this rule's question + } + const TSNode name = ts_node_named_child( left, 0 ); + if( kindIs( ts_node_type( name ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), constructedBy( ts_node_named_child( right, 0 ), lang, src ), {}, at, false ); + } + } + else if( kindIs( t, "field_declaration" ) ) + { + const std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + bool named = false; + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); + if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "field_identifier" ) ) + { + named = true; + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( c, src ), type, {}, at, false ); + } + return true; + } ); + if( !named ) + { + pushEvidenceBind( cx, LocalBindKind::MemberType, type, type, {}, at, true ); // an embedded field: its methods are promoted + } + } +} + void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) { // The body below is the pass's own node step, unchanged; these aliases keep it reading against the @@ -2526,6 +3003,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // DEFINITION's named parameters and a range-for's loop variable. Unconditional (the helper gates // language and node type itself); disjoint from every branch of the Rule-2 chain above. captureShadowScopeDecls( n, t, fileId, lang, src, binds ); + captureReceiverEvidence( cx, n, t ); // FE-B: typed parameters/locals, field types, method aliases (Python, JS/TS, Go) // ── L3 fn-pointer/callback capture (C/C++/ObjC) — a SEPARATE if (not part of the Rule-2 chain above): // the same `declaration` node can carry BOTH a Rule-2 var→type fact and a var→function fact diff --git a/src/ingest_cache.h b/src/ingest_cache.h index e9b4e0cdd..450879a8e 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -85,6 +85,8 @@ struct RawRef // field of a depth-2 chained receiver (recv FieldOfThis/FieldOfVar); "" otherwise std::string composeRel; // "creates" (value/inline) or "uses" (reference/pointer) when isCompose; "" otherwise std::string memberRoot; // FE-A: the receiver chain's ROOT identifier when memberCall (`JSON` in `JSON.parse()`); see model.h + std::string memberPath; // FE-B: the member names between memberRoot and the callee, '.'-joined ("" = `root.m()`); see model.h + std::string memberCtor; // FE-B: the class a constructed receiver names (`new X().m()`, Go `X{}.m()`); see model.h }; // P2-D Rule 2 raw local-variable type binding (pre-attribution). startByte sits inside the enclosing @@ -134,7 +136,11 @@ constexpr std::uint32_t kCacheMagic = 0x4b505443; // "CTPK" // all match) rather than silently re-absolutizing a key that was never root-relative to begin // with — a v2 cache simply misses on every lookup that survives the guard, which is exactly the // self-healing full-reparse path already used for any other corrupt/stale cache. -constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedgecheck.sh) — RawRef gains `memberCall` (u8) and +constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiverevidencecheck.sh) — RawRef gains `memberPath` (str) and + // `memberCtor` (str) after `memberRoot` (ref record 46 -> 54 bytes lean), + // and RawBind gains four APPENDED kinds (RecvType, MemberType, + // MethodAlias, NameAlias). A FORMAT change: the version guard rejects a v28 blob. + // 28: FE-A (test/falseedgecheck.sh) — RawRef gains `memberCall` (u8) and // `memberRoot` (str) after `qualifierRootsStd` (ref record 41 -> 46 // bytes lean). A FORMAT change: a v27 blob lacks the bytes, so the // version guard rejects it (self-healing full reparse). @@ -302,7 +308,12 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 141; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 142; // bump on any grammar/.scm/extraction change + // 142 = lane FE-B (receiver evidence): member calls record memberPath/memberCtor and + // Java/Kotlin/C#/Swift member calls are memberCall; typed parameters/locals, field types + // method aliases and aliased class imports are RecvType/MemberType/MethodAlias/NameAlias + // bindings. Above train 25's 141; + // the train renumbers. // 141 = 2026-10-04 (train 25): cache-key hygiene above every branch build's number. // Two merged lanes changed extraction under their own numbers: FE-A 134/135 // (member-call shape, ModuleAlias bindings, Rust path calls; kCacheVersion 28) @@ -2124,7 +2135,7 @@ inline void writeDef( ByteW& w, const RawDef& d, bool withLex, std::size_t fileD } } } -inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); } +inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); w.str( r.memberPath ); w.str( r.memberCtor ); } // loadCache's countFits() bounds a corrupt on-disk record COUNT against remaining bytes / // minRecordBytes BEFORE reserve() — the guard that keeps a hostile blob's 0xFFFFFFFF count from reaching @@ -2143,13 +2154,14 @@ inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8 // dlWeighted u32 + tokenCount u32 + tfWidth u8 = 9 bytes. A ref record is 3 u32 + 8 u8 + 5 empty // str(len u32) fields = 3*4 + 8*1 + 5*4 = 40 bytes (39 until the `viaArrow` u8, kCacheVersion 23; #150's // `qualifierRootsStd` u8 then added a 9th u8 — 40 -> 41, kCacheVersion 25; FE-A's `memberCall` u8 and `memberRoot` -// str then added a 10th u8 and a 6th str — 41 -> 46, kCacheVersion 28). +// str then added a 10th u8 and a 6th str — 41 -> 46, kCacheVersion 28; FE-B's `memberPath` and `memberCtor` strs then +// added a 7th and 8th str — 46 -> 54, kCacheVersion 29). // verifyCacheRecordMinimaTripwire() below derives these // same numbers from the REAL writer functions at runtime so the next field added to writeDef/writeRef // can't silently stale them. inline constexpr std::size_t kMinDefRecordBytesLean = 88; // 16×u32 + 16×u8 + 2×str(len u32, empty) inline constexpr std::size_t kMinDefRecordBytesRichExtra = 9; // v10 rich withLex extra: dlWeighted u32 + tokenCount u32 + tfWidth u8 -inline constexpr std::size_t kMinRefRecordBytes = 46; // 3×u32 + 10×u8 + 6×str(len u32, empty) +inline constexpr std::size_t kMinRefRecordBytes = 54; // 3×u32 + 10×u8 + 8×str(len u32, empty) inline std::size_t minDefRecordBytes( bool captureValueUses ) noexcept { @@ -2257,7 +2269,7 @@ inline RawDef readDef( ByteR& r, bool withLex, const std::vector& } return d; } -inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); return x; } +inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); x.memberPath = r.str(); x.memberCtor = r.str(); return x; } inline void writeBind( ByteW& w, const RawBind& b ) { w.u32( b.startByte ); w.u8( std::uint8_t( b.lang ) ); w.u8( std::uint8_t( b.kind ) ); w.u8( b.isFromAssignment ? 1 : 0 ); w.u32( b.spanStart ); w.u32( b.spanEnd ); w.str( b.var ); w.str( b.typeName ); w.str( b.importedName ); } inline RawBind readBind( ByteR& r ) { RawBind b; b.startByte = r.u32(); b.lang = r.enumU8( kLangCount ); b.kind = r.enumU8( kLocalBindKindCount ); b.isFromAssignment = r.u8() != 0; b.spanStart = r.u32(); b.spanEnd = r.u32(); b.var = r.str(); b.typeName = r.str(); b.importedName = r.str(); return b; } inline void writeFfi( ByteW& w, const BindingAlias& a ) { w.u8( std::uint8_t( a.kind ) ); w.u8( a.lowConf ? 1 : 0 ); w.str( a.aliasName ); w.str( a.targetName ); w.str( a.targetScope ); } diff --git a/src/ingest_model.h b/src/ingest_model.h index 13ad31101..cf68040b6 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -918,6 +918,8 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, ref.qualifierRootsStd = r.qualifierRootsStd; // #150: the FULL written qualifier chain is rooted at std ref.memberCall = r.memberCall; // FE-A: a Go/JS/TS/Rust member call (recv stays None) ref.memberRoot = std::move( r.memberRoot ); // FE-A: its receiver chain's root identifier + ref.memberPath = std::move( r.memberPath ); // FE-B: the members between that root and the callee + ref.memberCtor = std::move( r.memberCtor ); // FE-B: the class a constructed receiver names ref.fieldName = std::move( r.fieldName ); // S5-E: the member variable name (e.g. "m_pool") ref.composeRel = std::move( r.composeRel ); // S5-E: "creates" or "uses" ref.startByte = r.startByte; // shadow fix round: for the block-span containment test diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 0b3ebb9a7..29d199bcb 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1602,8 +1602,10 @@ void captureSideFacts( const LangEntry& le, std::uint32_t fileId, std::string_vi { arms.rust = &rustCtx; } + // FE-B: JavaScript and Go join for the receiver-evidence records alone (captureReceiverEvidence); every other + // branch of the pass gates its own language, so their record streams gain exactly those kinds. if( le.lang == Lang::Cpp || le.lang == Lang::ObjC || le.lang == Lang::Python || le.lang == Lang::TypeScript - || le.lang == Lang::Java + || le.lang == Lang::Java || le.lang == Lang::JavaScript || le.lang == Lang::Go || le.lang == Lang::C ) { arms.bind = &bindCtx; @@ -2345,13 +2347,17 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t if( !isImportRef && le.lang != Lang::Elixir ) // an import site has no receiver and no argument list — { // the defaults (RecvKind::None, argCountKnown=false) are the truth - RecvShape rs = le.lang == Lang::Java + // FE-B: a Java method INVOCATION reads its receiver like every other language (a member call or a bare + // implicit-this call); only a method REFERENCE (`T::m`) keeps issue #74's type-candidate shape. + const bool javaMethodRef = le.lang == Lang::Java && !ts_node_is_null( roleNode ) && kindIs( ts_node_type( roleNode ), "method_reference" ); + RecvShape rs = javaMethodRef ? javaMethodReferenceReceiver( roleNode, src ) : receiverOf( nameNode, le.lang, src ); // P2-D: `this`/`self`/`x`/`base.field` shape r.recv = rs.kind; r.recvVar = std::move( rs.var ); // → one-hop narrowing in resolve.h r.fieldName = std::move( rs.field ); // depth-2 intermediate field; "" otherwise r.viaArrow = rs.viaArrow; // `p->m()`: Rule 2b's smart-pointer pointee needs it r.memberCall = rs.member; r.memberRoot = std::move( rs.root ); // FE-A: a Go/JS/TS/Rust member call and its receiver root + r.memberPath = std::move( rs.path ); r.memberCtor = std::move( rs.ctor ); // FE-B: the rest of that receiver chain auto [ ac, ak ] = callArity( nameNode, le.lang, src ); // B2.2: call-site positional arg count r.argCount = ac; r.argCountKnown = ak; // → arity filter in graph.h } diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..59add33b1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2318,7 +2318,7 @@ int runDefaultMap( const MainDispatch& d ) if( !expandNodes.empty() ) { bodiesSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, cfg.compress, redactPtr, + { packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); }, // V1: octocode F2 sibs=/inc= rw::kBytesPerTokenBody ); @@ -2658,7 +2658,7 @@ int runDefaultMap( const MainDispatch& d ) // not as the literal source token, for that same reason (the count is a naive grep over this file). if( !expandNodes.empty() && noteAppliesToBundle ) // !serveWholeFile is this whole branch's precondition (see the if above) { - emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, cfg.compress, redactPtr, + emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); } ); bodiesEmittedEarly = true; @@ -2732,7 +2732,7 @@ int runDefaultMap( const MainDispatch& d ) } if( !expandNodes.empty() && !serveWholeFile && !bodiesEmittedEarly ) // #289: already served ahead of the map below { - emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, cfg.compress, redactPtr, + emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); } ); // L3: --expand bodies surface notes; V1: sibs=/inc= } diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..7aebd8b3e 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -716,8 +716,10 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // root it twins), and reused per row so `tested` is the same predicate the CLI prints. const HopTestedPartition chTested = computeHopTestedPartition( ing, g, referencingOnly ? calledBy : calls ); - const auto symObj = [ & ]( NodeId id ) -> std::string + bool anyVia = false; // FE-B: an entry carried "via":"name" — the answer then carries via_note, its reading + const auto symObj = [ & ]( NodeId id, bool via ) -> std::string { + anyVia = anyVia || via; const Symbol& s = ing.symbols[id]; // T4: attach the stable content-handle so the agent can `fetch_body{handle}` instead of us re-sending // the body. Names/signatures by default (this verb); bodies by handle on request. @@ -730,15 +732,16 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + "\",\"line\":" + std::to_string( s.line ) + ",\"handle\":\"" + mcpdetail::jsonEscape( handleFor( ix, id ) ) + "\"" + ( rw::isTestedByReach( ing, chTested.testReach, id ) ? ",\"tested\":true" : "" ) + + ( via ? ",\"via\":\"name\"" : "" ) // FE-B: the CLI row's via="name", the same edge bit (graph.h rowNameOnly) + "}"; }; - const auto rowArray = [ & ]( const std::vector& ids, const PageWindow& w ) -> std::string + const auto rowArray = [ & ]( const std::vector& ids, const PageWindow& w, bool callersSide ) -> std::string { std::string a = "["; for( std::size_t i = w.begin; i < w.end; ++i ) { if( i != w.begin ) { a += ","; } - a += symObj( ids[i] ); + a += symObj( ids[i], rowNameOnly( g, chRows.matches, ids[i], callersSide ) ); } a += "]"; return a; @@ -758,7 +761,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { out += "\"root\":\"" + mcpdetail::jsonEscape( root ) + "\","; } - out += "\"symbol\":" + symObj( chRows.matches.front() ); + out += "\"symbol\":" + symObj( chRows.matches.front(), false ); // §P10.6 / A6, in the CLI's own key names: defs= = definitions the name resolved to (the rows below are // the UNION of all of their neighbours), count= = the un-windowed row total, hop_tested/hop_untested = // the partition over that full set. @@ -798,10 +801,14 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { out += calledByCutJson( name, calledBy.size(), pwSecond, page.limit ); // cut-fix C: the second array's own cut } - out += ",\"calledBy\":" + rowArray( calledBy, referencingOnly ? pwPrimary : pwSecond ); + out += ",\"calledBy\":" + rowArray( calledBy, referencingOnly ? pwPrimary : pwSecond, true ); if( !referencingOnly ) { - out += ",\"calls\":" + rowArray( calls, pwPrimary ); + out += ",\"calls\":" + rowArray( calls, pwPrimary, false ); + } + if( anyVia ) + { + out += rw::viaNameNoteJson(); // FE-B: the reading of "via":"name", only in an answer that carries it } // Reference-as-value round: the CLI's window, the same rows (callhierarchy.h computed them once): valueRefs = // where the symbol is USED AS A VALUE (the --callers side), valueCallees = what it stores/passes and may call @@ -2658,7 +2665,7 @@ inline std::optional exemplarText( const std::string& root, const s fin( pick.winner ), wsym.ccx, exRootAttr.c_str(), ts( pick.winner ) ? " tested=\"1\"" : "", pick.lowConfidence ? " low_confidence=\"1\"" : "", pick.overCcxBar ? " over_ccx_bar=\"1\"" : "" ); - packBodies( mem, ing, { pick.winner }, 0 /* no byte budget in MCP (0 = unlimited) */, g.outOff, g.outTargets, false, redact, + packBodies( mem, ing, { pick.winner }, 0 /* no byte budget in MCP (0 = unlimited) */, g.outOff, g.outTargets, g.outNameOnly, false, redact, /*ranges=*/nullptr, /*noteIndex=*/nullptr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, exSingleRoot ? std::string_view( root ) : std::string_view() ); rw::emitRaw( mem, "" ); @@ -2694,6 +2701,7 @@ inline std::optional impactText( const std::string& root, const std // 0.6.5: the CLI --impact's walk, keeping each node's hop depth, and its order (graph.h orderByDepthThenRank). std::vector depth; const std::vector reach = transitiveCallersDepth( g, seeds, &depth ); + const std::vector proven = provenCallerReach( g, seeds ); // FE-B (D3): as on the CLI const auto [ rank, prIters, prConverged ] = rankGraph( g ); const RankDisclosure prD{ prIters, prConverged, true }; // W2-F: CLI --impact discloses this; so does its twin std::vector show = reach; @@ -2734,10 +2742,13 @@ inline std::optional impactText( const std::string& root, const std sizeImportTier( imports, page.limit, symbol ); // cut-fix C: limit sizes the tier, as on the CLI // Reference-as-value round: SYM's binding sites, the CLI --impact's value_refs=/ by the same call. const ValueRefRows imValueRefs = valueRefCallerRows( ing, valueRefIndexOf( ix ), seeds ); + const PageWindow ipw = pageWindow( show.size(), effectiveRowCap( page.limit, 40 ), page.offset ); + const bool imHasVia = std::any_of( show.begin() + ipw.begin, show.begin() + ipw.end, [ & ]( NodeId n ) { return impactRowNameOnly( proven, n ); } ); rw::emitTo( mem, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}-->", kImpactLegendOpen, kPageRaiseCapClause, reach.empty() ? "" : kImpactDepthLegend, // 0.6.5: exactly when d=/by_depth= ride, as on the CLI kImpactImportTierLegend, - impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ).c_str(), // #220: exactly when the root carries them, as on the CLI + ( impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them, as on the CLI + + viaNameLegend( imHasVia, true ) ).c_str(), // FE-B: exactly when a shown row is via="name", as on the CLI kTestedRowLegend, kImpactTestedPartitionLegend, // A6 kTestedLensBlindSpotLegend, // F-02: rides with the partition, byte-identical to the CLI twin unprovenDefsVerbLegend( UnprovenDefsVerb::Impact, unprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs=, as on the CLI @@ -2750,7 +2761,6 @@ inline std::optional impactText( const std::string& root, const std // same meaning as the CLI --impact — the two surfaces must not diverge on an honesty marker. // §B6 M4: the window and the disclosure now come from the shared pageview.h trio, exactly as they do on // the CLI arm, instead of a hand-rolled 40-row slice with a hand-rolled shown=/capped= pair. - const PageWindow ipw = pageWindow( show.size(), effectiveRowCap( page.limit, 40 ), page.offset ); const std::size_t shownRows = ipw.end - ipw.begin; char ipab[ kPageDisclosureCap ]; // R-E fix (2026-08-19): root-relative p= + root=, exactly as the CLI --impact now emits them. The first @@ -2771,8 +2781,9 @@ inline std::optional impactText( const std::string& root, const std { const Symbol& s = ing.symbols[ show[i] ]; const std::string_view rp = imSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], imRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= as on the CLI row. - rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, - depthRunAttrXml( show, depth, i, ipw.begin ), isTestedByReach( ing, impTestReach, show[i] ) ? " tested=\"1\"" : "" ); } + rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + depthRunAttrXml( show, depth, i, ipw.begin ), isTestedByReach( ing, impTestReach, show[i] ) ? " tested=\"1\"" : "", + viaNameAttr( impactRowNameOnly( proven, show[i] ) ) ); } // FE-B (D3) // the import tier's rows, after the symbol rows and under their own tag — a different unit, so a // different element (see the CLI arm and kImpactImportTierLegend for why they are never one number). emitImportRowsXml( mem, ing, std::span( imports.files ).first( imports.shown ), imRootPrefix, @@ -3197,8 +3208,13 @@ inline std::optional pathText( const std::string& root, const std:: // H5: the same brief floor legend + marker the CLI --path prints (verbs_navigate.h) — one wording, two transports. // Reference-as-value round: the CLI --path's to_value_refs=, by the same call. const std::size_t ptToValueRefs = toValueRefsCount( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); + bool ptHasVia = false; // FE-B: exactly when a hop of this path is via="name", as on the CLI + for( std::size_t i = 1; i < pth.size() && !ptHasVia; ++i ) + { + ptHasVia = edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ); + } rw::emitTo( mem, "{}", unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ).c_str(), + "graph holds none. {}{}{}-->{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) ).c_str(), toValueRefsLegend( ptToValueRefs > 0 ), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); rw::emitTo( mem, " pathText( const std::string& root, const std:: ex( from ).c_str(), ex( to ).c_str() ); } rw::emitRaw( mem, ">" ); - for( NodeId n : pth ) - { const Symbol& s = ing.symbols[n]; + for( std::size_t i = 0; i < pth.size(); ++i ) + { const Symbol& s = ing.symbols[ pth[i] ]; const std::string_view rp = ptSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], ptRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); - rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line ); } + rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + viaNameAttr( i > 0 && edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ) ) ); } // FE-B: as on the CLI rw::emitRaw( mem, "" ); return mcpAnswerText( stream ); // nullopt = the buffer lost bytes (dispatch answers -32603), "" stays not-found } @@ -3475,9 +3492,15 @@ inline void packConnect( std::FILE* out, const IngestResult& ing, const Graph& g const std::string connectUnprovenLegend = unprovenDefsVerbComment( UnprovenDefsVerb::Connect, unprovenDefs > 0, "" : std::string(); const std::size_t connectExtraBytes = connectRootAttr.size() + std::strlen( rootRelPathsLegend( !rootArg.empty() ) ) + connectUnindexedLegend.size() + connectUnprovenAttr.size() + connectUnprovenLegend.size() - + connectAmbiguousAttr.size(); + + connectAmbiguousAttr.size() + connectViaLegend.size(); // §2.4a: the derived hub threshold every Steiner row's connects= is read against, computed ONCE (it is a // property of the graph, not of a row) and named on the root so the label is never a bare assertion. @@ -3625,7 +3648,8 @@ inline void packConnect( std::FILE* out, const IngestResult& ing, const Graph& g for( const ConnectEdge& e : edges ) { payload.append( "" ); + .append( "\" t=\"" ).append( ex( ing.symbols[ e.to ].name ) ).append( "\"" ) + .append( viaNameAttr( edgeNameOnly( g, e.from, e.to ) ) ).append( "/>" ); // FE-B: a name-only edge says so } payload.append( "" ); } @@ -3688,8 +3712,8 @@ inline void packConnect( std::FILE* out, const IngestResult& ing, const Graph& g { estTokens = connectEstTokens( payload.size(), connectExtraBytes + std::strlen( connectOverAttr ) ); } - rw::emitTo( out, "{}{}{}{}", rw::cstr( kConnectHeader ), connectUnindexedLegend.c_str(), connectUnprovenLegend.c_str(), - rootRelPathsLegend( !rootArg.empty() ) ); + rw::emitTo( out, "{}{}{}{}{}", rw::cstr( kConnectHeader ), connectUnindexedLegend.c_str(), connectUnprovenLegend.c_str(), + connectViaLegend, rootRelPathsLegend( !rootArg.empty() ) ); rw::emitTo( out, "", res.terminals.size(), nodeTotal, edgeTotal, res.radius, connectedGroups, connectUnprovenAttr.c_str(), // H1: beside the counts it qualifies; absent at zero diff --git a/src/model.h b/src/model.h index cb58e3082..4dca37c64 100644 --- a/src/model.h +++ b/src/model.h @@ -752,6 +752,14 @@ struct Reference // `new X()`). Read only by graph.h's FalseEdgeRules. false/"" for every other language and every non-call ref. bool memberCall = false; std::string memberRoot; + // FE-B (test/receiverevidencecheck.sh): the rest of a member call's receiver chain, for the receiver-evidence rule + // (graph.h ReceiverEvidence). memberPath is the member names BETWEEN memberRoot and the callee, '.'-joined — + // "bucket" for `this.bucket.listSchemas()`, "req.raw.headers" for `ctx.req.raw.headers.get()`, "" for `root.m()`. + // memberCtor is the class a CONSTRUCTED receiver names, final segment — "Reply" for `new Reply( r ).send()`, Go + // "Merger" for `(&Merger{}).Len()` — with memberRoot "" (a construction has no root identifier). Both "" for every + // other shape and every language that records no member-call shape. + std::string memberPath; + std::string memberCtor; }; // A physical dependency: one #include / import directive (file → target). The target is the raw @@ -901,9 +909,31 @@ enum class LocalBindKind : std::uint8_t // (`const { stringify } = JSON`); importedName = the member it names, "*" for the whole module. Go: // var "." is a dot import. fromSymbol kNoNode, spans {0,0}. Read only by graph.h FalseEdgeRules; // every other binding consumer filters by kind or skips file-scope records. APPENDED (cache u8). + // FE-B (test/receiverevidencecheck.sh): the receiver-evidence facts graph.h ReceiverEvidence reads, and NOTHING else. + // Every other binding consumer filters by kind or skips these four by isReceiverEvidenceKind below — a field name + // recorded here is not a local of the method that assigns it, and a type written on a parameter is not a class the + // file "names" for the builtin-method gate. APPENDED (cache u8). + RecvType, // a parameter or local whose class the source states: var = the name, typeName = the class's final + // segment, importedName = the written type whole. A TS/Python/Go parameter's annotation, a JS + // `const x = new Foo()`, a Go `var x T` / `x := T{…}` / `x := &T{…}`, and a Go METHOD RECEIVER — the + // receiver's record sets isFromAssignment (the method's own class, which Go spells nowhere else), and + // so does a JS `Foo.prototype.m = function …` (var "", typeName Foo): that member's class. + MemberType, // a field whose class the source states: var = the field, typeName = the class's final segment. + // fromSymbol is the def that states it — a Python method's `self.x = Foo()`, a JS/TS method's + // `this.x = new Foo()`, a Go struct's `x T` — and the owning class is that def's class. A Go + // EMBEDDED field (`struct { u.Chars }`) sets isFromAssignment: its methods are promoted. + MethodAlias, // a local bound to an object's method: var = the local, typeName = the object's variable, + // importedName = the method (Python `feed = parser.feed`). + NameAlias, // a file's local spelling of an imported class: var = the local name, typeName = the name it imports + // (Python `from m import Stylesheet as Sheet`; JS/TS read their JsImport records instead). }; // The number of LocalBindKind enumerators — the bound readBind validates a cached kind byte against (see kSymKindCount). -inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::ModuleAlias ) + 1; +inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::NameAlias ) + 1; +// FE-B: the four kinds only graph.h ReceiverEvidence reads (see RecvType above). +inline bool isReceiverEvidenceKind( LocalBindKind k ) noexcept +{ + return k == LocalBindKind::RecvType || k == LocalBindKind::MemberType || k == LocalBindKind::MethodAlias || k == LocalBindKind::NameAlias; +} static_assert( enumCountIsExact(), "kLocalBindKindCount must name the LAST LocalBindKind enumerator — move it with the append" ); inline constexpr const char* kFnBindLambdaTarget = "(lambda)"; // parens are illegal in identifiers, so diff --git a/src/nonlocalstate.h b/src/nonlocalstate.h index 62fb3c145..216139dc4 100644 --- a/src/nonlocalstate.h +++ b/src/nonlocalstate.h @@ -572,7 +572,7 @@ inline std::vector collectAccesses( const IngestResult& ing, const s HashMap locallyBound; // "fromSymbol\x1fvar" -> 1 for( const Binding& b : ing.bindings ) { - if( b.fromSymbol != kNoNode ) + if( b.fromSymbol != kNoNode && !isReceiverEvidenceKind( b.kind ) ) // FE-B: a field's type fact binds no local { locallyBound[ std::to_string( b.fromSymbol ) + "\x1f" + b.var ] = 1; } diff --git a/src/packtask.h b/src/packtask.h index 62c7d49a4..49b518ce0 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1189,7 +1189,7 @@ inline std::size_t probeBodyCost( const IngestResult& ing, const Graph& g, NodeI // would price a listing the bundle will never emit, and every fit decision downstream inherits that. const PackTaskRendered one = packTaskRender( [ & ]( std::FILE* m ) { - packBodies( m, ing, { id }, SIZE_MAX, g.outOff, g.outTargets, in.compress, in.redact, nullptr, nullptr, &dummy, + packBodies( m, ing, { id }, SIZE_MAX, g.outOff, g.outTargets, g.outNameOnly, in.compress, in.redact, nullptr, nullptr, &dummy, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, in.rootArg, in.calleeRank ); }, in.renderFaults ); if( !one.ok ) @@ -1337,7 +1337,7 @@ inline std::vector selectMonotoneBodySubset( const IngestResult& ing, co EmittedBodies dummy; wrapperLen = packTaskRender( [ & ]( std::FILE* m ) { - packBodies( m, ing, {}, SIZE_MAX, g.outOff, g.outTargets, in.compress, in.redact, nullptr, nullptr, &dummy ); + packBodies( m, ing, {}, SIZE_MAX, g.outOff, g.outTargets, g.outNameOnly, in.compress, in.redact, nullptr, nullptr, &dummy ); }, in.renderFaults ).text.size(); for( std::size_t i = 0; i < n; ++i ) { @@ -1729,7 +1729,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, const std::vector renderIds = selectMonotoneBodySubset( ing, g, bodyIds, bodiesBudget, in ); const PackTaskRendered bodies = packTaskRender( [ & ]( std::FILE* m ) { - packBodies( m, ing, renderIds, bodiesBudget, g.outOff, g.outTargets, in.compress, in.redact, + packBodies( m, ing, renderIds, bodiesBudget, g.outOff, g.outTargets, g.outNameOnly, in.compress, in.redact, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &emittedBodies, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, in.rootArg, in.calleeRank ); }, in.renderFaults ); diff --git a/src/quality.h b/src/quality.h index 0b7c9de7f..077f3a3a9 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2179,8 +2179,9 @@ inline std::string cacheRootKeyHex( const std::string& root ) // `static_assert( quality::kIngestParserVerMirror == kParserVer && … )`, so a missed mirror now fails the build. It does // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. -constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 141; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestCacheVersionMirror = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) +constexpr std::uint32_t kIngestParserVerMirror = 142; // MUST equal ingest.cpp's kParserVer (gated) + // 142 = lane FE-B (receiver evidence, see kParserVer note; the train renumbers) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/receiverevidence.h b/src/receiverevidence.h new file mode 100644 index 000000000..e13cebd9a --- /dev/null +++ b/src/receiverevidence.h @@ -0,0 +1,660 @@ +#pragma once +// receiverevidence.h — FE-B (test/receiverevidencecheck.sh): what proves a call's RECEIVER, and what does not. +// +// A member call `x.m()` (or, where the receiver is implicit, a bare `m()`) used to bind to every in-repo definition +// spelled `m` that the name ladder reached, and the answer showed each such row as a plain edge — a confident claim +// with nothing behind it but the name. This header holds the two halves of the fix: +// +// 1. ReceiverEvidence::narrow — a RESOLVE rule for receivers the source types: `this`/`self`/`cls` inside a class (and +// `super`/`base`, through the bases only), a parameter or local whose class is written (a TS/Python/Go annotation, a +// Go method receiver, a JS `new Foo()` / Python `Foo()` / Go `Foo{}` initializer), a constructed receiver +// (`new Foo().m()`), a class-name receiver (`Foo.m()`), and a chain of fields whose classes are stated +// (`this.bucket = new Schemas()`, Python `self.x = Foo()`, a Go struct field, a Go EMBEDDED field whose methods +// are promoted). A hit is the call's whole answer, exactly like Rule 2. +// 2. ReceiverEvidence::ladderProves — for a call the rules above did not answer, which name-ladder candidates the +// language's own lookup can PROVE: an implicit receiver's own class and its bases, a free function in scope, a +// definition in the module a receiver alias names. A call left with no proven candidate is NAME-ONLY: its rows +// are kept, marked via="name" on every surface (graph.h Graph::outNameOnly), never dropped and never proven. +// +// What it does NOT do: infer a function's return type (`s = self.get_screen(); s.refresh()`), follow a property, or type +// a value that crossed a call boundary. Those receivers stay name-only — the honest answer, not a guess. + +#include "model.h" +#include "resolve.h" // includerDir; HashMap / SmallVec through it +#include "smallvec.h" + +#include +#include +#include +#include + +namespace rw +{ + +// The languages whose name-ladder rows the hedge covers. Every other language keeps its ladder unchanged (a disclosed +// floor in test/receiverevidencecheck.sh: Objective-C, PHP, Lua, Zig, Dart, Elixir, Scala, Bash and the rest). +inline bool receiverHedgeLang( Lang l ) noexcept +{ + switch( l ) + { + case Lang::JavaScript: case Lang::TypeScript: case Lang::Python: case Lang::Go: case Lang::Rust: case Lang::C: + case Lang::Cpp: case Lang::Java: case Lang::Kotlin: case Lang::CSharp: case Lang::Swift: case Lang::Ruby: + return true; + default: + return false; + } +} + +// The languages where a bare `m()` inside a method is an implicit `this.m()` (or reaches a free/top-level function). +inline bool implicitReceiverLang( Lang l ) noexcept +{ + return l == Lang::Java || l == Lang::Kotlin || l == Lang::CSharp || l == Lang::Swift || l == Lang::Cpp || l == Lang::Ruby; +} + +// a call written ON a receiver — every language's member shape: Python/C++/Ruby record a RecvKind, the rest memberCall +inline bool isMemberCallRef( const Reference& r ) noexcept +{ + return r.recv != RecvKind::None || r.memberCall; +} + +inline bool isThisRoot( std::string_view root ) noexcept +{ + return root == "this" || root == "self" || root == "Self"; +} + +inline bool isSuperRoot( std::string_view root ) noexcept +{ + return root == "super" || root == "base"; +} + +struct ReceiverEvidence +{ + static constexpr std::size_t kWalkCap = 64; // classes visited per base walk; deterministic (sorted chaUp) + + const IngestResult& ing; + const HashMap>& chaUp; // class name → its direct base names (sorted) + const HashMap& classNames; // every in-repo class-like name + std::vector ownerClass; // per symbol: the class that owns it, "" none + HashMap> methodsByType; // "Class::name" → its callables + HashMap memberType; // "Class#field" → field class ("" = tombstone) + HashMap> embeds; // "Class" → its other bases: Go embedded classes (promotion), + // Ruby include/extend/prepend modules + HashMap nameAlias; // "#local" → the class name it imports + HashMap localType; // "#var" → class ("" = tombstone) + HashMap> methodAlias; // "#var" → ( object var, method ) + HashMap moduleAlias; // "#name" → module as written ("" = tombstone) + std::vector> fnsByFile; // fileId → functions/methods sorted by start (enclosing walk) + std::vector> classesByFile; // fileId → class-like symbols sorted by start + mutable std::string key; + mutable std::vector frontier; + mutable rw::SmallVec found; + bool active = false; + + ReceiverEvidence( const IngestResult& i, const HashMap>& up, const HashMap& classes ) + : ing( i ), chaUp( up ), classNames( classes ) {} + + static bool classLike( SymKind k ) noexcept + { + return k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface; + } + static bool callableKind( SymKind k ) noexcept + { + return k == SymKind::Method || k == SymKind::Function; + } + const std::string& keyOf( std::uint32_t id, char sep, std::string_view name ) const + { + key.clear(); + key.append( std::to_string( id ) ); + key.push_back( sep ); + key.append( name ); + return key; + } + const std::string& typeKey( std::string_view type, std::string_view sep, std::string_view name ) const + { + key.assign( type ); + key.append( sep ); + key.append( name ); + return key; + } + + // ── build ───────────────────────────────────────────────────────────────────────────────────────────────────────── + static void tombstoneInsert( HashMap& m, const std::string& k, std::string_view v ) + { + const auto [ it, fresh ] = m.try_emplace( k, std::string( v ) ); + if( !fresh && it->second != v ) + { + it->second.clear(); // two different classes for one name: decide nothing + } + } + + void buildOwners() + { + const std::size_t N = ing.symbols.size(); + ownerClass.assign( N, {} ); + // the classes of each file by start; a symbol's owner is the innermost class whose span holds it + classesByFile.assign( ing.files.size(), {} ); + fnsByFile.assign( ing.files.size(), {} ); + for( const Symbol& s : ing.symbols ) + { + if( s.fileId >= ing.files.size() ) + { + continue; + } + if( classLike( s.kind ) ) + { + classesByFile[ s.fileId ].push_back( s.id ); + } + if( callableKind( s.kind ) ) + { + fnsByFile[ s.fileId ].push_back( s.id ); + } + } + const auto byStart = [ & ]( NodeId a, NodeId b ) { return ing.symbols[ a ].sigStartByte != ing.symbols[ b ].sigStartByte + ? ing.symbols[ a ].sigStartByte < ing.symbols[ b ].sigStartByte : a < b; }; + for( std::vector& v : classesByFile ) { std::sort( v.begin(), v.end(), byStart ); } + for( std::vector& v : fnsByFile ) { std::sort( v.begin(), v.end(), byStart ); } + for( const Symbol& s : ing.symbols ) + { + if( !s.scope.empty() ) + { + ownerClass[ s.id ] = s.scope; // the extractor's own scope (Python, C++, Kotlin, Ruby, JS prototype members) + continue; + } + if( s.fileId >= ing.files.size() || classLike( s.kind ) ) + { + continue; + } + if( const NodeId best = classAt( s.fileId, s.sigStartByte, s.endByte, s.id ); best != kNoNode ) + { + ownerClass[ s.id ] = ing.symbols[ best ].name; + } + } + // Go spells a method's class only on its receiver: the RecvType record that sets isFromAssignment + for( const Binding& b : ing.bindings ) + { + if( b.kind == LocalBindKind::RecvType && b.isFromAssignment && b.fromSymbol < N && !b.typeName.empty() ) + { + ownerClass[ b.fromSymbol ] = b.typeName; + } + } + } + + // the innermost class-like symbol of `fileId` whose span holds [start, end) (never `self`); kNoNode when none + NodeId classAt( std::uint32_t fileId, std::uint32_t start, std::uint32_t end, NodeId self = kNoNode ) const + { + NodeId best = kNoNode; + if( fileId >= classesByFile.size() ) + { + return best; + } + for( NodeId c : classesByFile[ fileId ] ) + { + const Symbol& cs = ing.symbols[ c ]; + if( cs.sigStartByte > start ) + { + break; + } + if( cs.id != self && cs.endByte >= end ) + { + best = c; // later starts are more inner + } + } + return best; + } + void addBase( const std::string& owner, std::string_view base ) + { + std::vector& e = embeds[ owner ]; + if( std::find( e.begin(), e.end(), base ) == e.end() ) + { + e.emplace_back( base ); + std::sort( e.begin(), e.end() ); + } + } + // Ruby: an include/extend/prepend directive inside a class body makes that module one more base of the class (the + // superclass directive at the open is already chaUp's). A symbolic, non-value directive is the only kind read. + void buildRubyMixins() + { + for( const Include& inc : ing.includes ) + { + if( !inc.isSymbolic || inc.isValueUse || inc.fileId >= classesByFile.size() || inc.target.empty() ) + { + continue; + } + const NodeId cls = classAt( inc.fileId, inc.byte, inc.byte + 1 ); + if( cls == kNoNode || ing.symbols[ cls ].lang != Lang::Ruby || ing.symbols[ cls ].sigStartByte == inc.byte ) + { + continue; // outside a class body, or the open's own superclass directive + } + std::string_view target = inc.target; + if( const std::size_t cut = target.rfind( "::" ); cut != std::string_view::npos ) + { + target = target.substr( cut + 2 ); + } + addBase( ing.symbols[ cls ].name, target ); + } + } + + void build() + { + buildOwners(); + buildRubyMixins(); + for( const Symbol& s : ing.symbols ) + { + if( callableKind( s.kind ) && !ownerClass[ s.id ].empty() && receiverHedgeLang( s.lang ) ) + { + methodsByType[ typeKey( ownerClass[ s.id ], "::", s.name ) ].push_back( s.id ); + } + } + const std::size_t N = ing.symbols.size(); + for( const Binding& b : ing.bindings ) + { + if( b.kind == LocalBindKind::ModuleAlias && b.fromSymbol == kNoNode && !b.var.empty() && !b.isFromAssignment ) + { + tombstoneInsert( moduleAlias, keyOf( b.fileId, '#', b.var ), b.typeName ); + continue; + } + if( b.kind == LocalBindKind::NameAlias && !b.var.empty() ) + { + tombstoneInsert( nameAlias, keyOf( b.fileId, '#', b.var ), b.typeName ); + continue; + } + if( b.kind == LocalBindKind::JsImport && !b.var.empty() && !b.importedName.empty() && b.importedName != b.var ) + { + tombstoneInsert( nameAlias, keyOf( b.fileId, '#', b.var ), b.importedName ); // `import { Context as Ctx }` + continue; + } + if( b.fromSymbol >= N ) + { + continue; + } + // a typed local the extractor also indexed as a symbol of its own (a Go `var c T` inside a function) belongs + // to the function that declares it + NodeId from = b.fromSymbol; + if( !callableKind( ing.symbols[ from ].kind ) && !classLike( ing.symbols[ from ].kind ) ) + { + if( const NodeId fn = enclosingFn( from ); fn != kNoNode ) + { + from = fn; + } + } + const bool typedLocal = b.kind == LocalBindKind::RecvType + || ( b.kind == LocalBindKind::Type && ( ing.symbols[ b.fromSymbol ].lang == Lang::TypeScript + || ing.symbols[ b.fromSymbol ].lang == Lang::Python ) ); + if( typedLocal && !b.var.empty() ) + { + tombstoneInsert( localType, keyOf( from, '#', b.var ), b.typeName ); + } + else if( b.kind == LocalBindKind::MemberType && !b.var.empty() ) + { + const Symbol& from = ing.symbols[ b.fromSymbol ]; + const std::string& owner = classLike( from.kind ) ? from.name : ownerClass[ b.fromSymbol ]; + if( owner.empty() ) + { + continue; + } + tombstoneInsert( memberType, typeKey( owner, "#", b.var ), b.typeName ); + if( b.isFromAssignment ) + { + addBase( owner, b.typeName ); + } + } + else if( b.kind == LocalBindKind::MethodAlias && !b.var.empty() ) + { + const auto [ it, fresh ] = methodAlias.try_emplace( keyOf( from, '#', b.var ), b.typeName, b.importedName ); + if( !fresh && ( it->second.first != b.typeName || it->second.second != b.importedName ) ) + { + it->second.first.clear(); // rebound to something else: decide nothing + } + } + } + active = true; + } + + // ── queries ─────────────────────────────────────────────────────────────────────────────────────────────────────── + // the innermost function/method of the same file whose span strictly holds `id`; kNoNode when none + NodeId enclosingFn( NodeId id ) const + { + const Symbol& inner = ing.symbols[ id ]; + if( inner.fileId >= fnsByFile.size() ) + { + return kNoNode; + } + NodeId best = kNoNode; + for( NodeId c : fnsByFile[ inner.fileId ] ) + { + const Symbol& s = ing.symbols[ c ]; + if( s.sigStartByte > inner.sigStartByte ) + { + break; + } + if( c != id && s.endByte >= inner.endByte && ( s.sigStartByte < inner.sigStartByte || s.endByte > inner.endByte ) ) + { + best = c; + } + } + return best; + } + // the class a typed local names in `from` or a function enclosing it; nullptr when untyped (or tombstoned) + const std::string* typedLocal( NodeId from, std::string_view var ) const + { + NodeId cur = from; + for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + { + if( const auto it = localType.find( keyOf( cur, '#', var ) ); it != localType.end() ) + { + return it->second.empty() ? nullptr : &it->second; + } + cur = enclosingFn( cur ); + } + return nullptr; + } + const std::pair* aliasedMethod( NodeId from, std::string_view var, NodeId& scope ) const + { + NodeId cur = from; + for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + { + if( const auto it = methodAlias.find( keyOf( cur, '#', var ) ); it != methodAlias.end() ) + { + scope = cur; + return it->second.first.empty() ? nullptr : &it->second; + } + cur = enclosingFn( cur ); + } + return nullptr; + } + // the class owning the caller — the caller's own owner, else the nearest enclosing function's (a closure in a method) + std::string_view callerClass( NodeId from ) const + { + NodeId cur = from; + for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + { + if( !ownerClass[ cur ].empty() ) + { + return ownerClass[ cur ]; + } + cur = enclosingFn( cur ); + } + return {}; + } + // `type`'s direct bases: the inheritance names, then (Go) its embedded classes + template + void forEachBase( std::string_view type, Fn&& fn ) const + { + if( const auto it = chaUp.find( std::string( type ) ); it != chaUp.end() ) + { + for( const std::string& b : it->second ) { fn( std::string_view( b ) ); } + } + if( const auto it = embeds.find( std::string( type ) ); it != embeds.end() ) + { + for( const std::string& b : it->second ) { fn( std::string_view( b ) ); } + } + } + // is `ancestor` the class `type` or one of its (transitive) bases + bool inCone( std::string_view type, std::string_view ancestor ) const + { + if( type.empty() || ancestor.empty() ) + { + return false; + } + frontier.clear(); + frontier.push_back( type ); + for( std::size_t i = 0; i < frontier.size() && i < kWalkCap; ++i ) + { + if( frontier[ i ] == ancestor ) + { + return true; + } + forEachBase( frontier[ i ], [ & ]( std::string_view b ) + { + if( std::find( frontier.begin(), frontier.end(), b ) == frontier.end() ) { frontier.push_back( b ); } + } ); + } + return false; + } + // `type`'s own `name`, else the shallowest base level defining it (several bases at one level: their union). The + // answer is in `found`; false when nothing in the cone defines it. superOnly skips `type` itself (`super.m()`). + bool methodOf( std::string_view type, std::string_view name, bool superOnly ) const + { + found.clear(); + if( type.empty() ) + { + return false; + } + std::vector level{ type }; + std::vector seen{ type }; + for( std::size_t depth = 0; depth < kWalkCap && !level.empty(); ++depth ) + { + if( !( superOnly && depth == 0 ) ) + { + for( std::string_view t : level ) + { + if( const auto it = methodsByType.find( typeKey( t, "::", name ) ); it != methodsByType.end() ) + { + for( NodeId c : it->second ) { found.push_back( c ); } + } + } + if( !found.empty() ) + { + return true; + } + } + std::vector next; + for( std::string_view t : level ) + { + forEachBase( t, [ & ]( std::string_view b ) + { + if( std::find( seen.begin(), seen.end(), b ) == seen.end() ) { seen.push_back( b ); next.push_back( b ); } + } ); + } + level.swap( next ); + } + return false; + } + // the class of field `field` on `type` (or the shallowest base declaring it); "" unknown + std::string_view fieldOf( std::string_view type, std::string_view field ) const + { + std::vector level{ type }; + std::vector seen{ type }; + for( std::size_t depth = 0; depth < kWalkCap && !level.empty(); ++depth ) + { + for( std::string_view t : level ) + { + if( const auto it = memberType.find( typeKey( t, "#", field ) ); it != memberType.end() ) + { + return it->second; // "" = tombstone: unknown + } + } + std::vector next; + for( std::string_view t : level ) + { + forEachBase( t, [ & ]( std::string_view b ) + { + if( std::find( seen.begin(), seen.end(), b ) == seen.end() ) { seen.push_back( b ); next.push_back( b ); } + } ); + } + level.swap( next ); + } + return {}; + } + + // The receiver chain of a member call as ( root, path, constructed class ), in every language's spelling. + struct Chain + { + std::string_view root; + std::string_view path; + std::string_view ctor; + bool valid = false; + }; + static Chain chainOf( const Reference& r ) noexcept + { + Chain c; + if( r.memberCall ) + { + c.root = r.memberRoot; + c.path = r.memberPath; + c.ctor = r.memberCtor; + c.valid = !c.root.empty() || !c.ctor.empty(); + return c; + } + switch( r.recv ) // Python / C++ / Ruby shapes (ingest_binds.h receiverOf) + { + case RecvKind::ThisObj: c.root = "self"; c.valid = true; break; + case RecvKind::NamedVar: c.root = r.recvVar; c.valid = !r.recvVar.empty(); break; + case RecvKind::FieldOfThis: c.root = "self"; c.path = r.fieldName; c.valid = !r.fieldName.empty(); break; + case RecvKind::FieldOfVar: c.root = r.recvVar; c.path = r.fieldName; c.valid = !r.recvVar.empty() && !r.fieldName.empty(); break; + case RecvKind::SuperObj: c.root = "super"; c.valid = true; break; + default: break; + } + return c; + } + + // The class the receiver ROOT names at this site; static/super set for a class-name or super receiver. "" unknown. + // a class name as the caller's FILE spells it, read through that file's import alias (`Ctx` → `Context`) + std::string_view unalias( std::uint32_t fileId, std::string_view name ) const + { + if( const auto it = nameAlias.find( keyOf( fileId, '#', name ) ); it != nameAlias.end() && !it->second.empty() ) + { + return it->second; + } + return name; + } + std::string_view rootClass( const Reference& r, const Chain& ch, bool& isStatic, bool& superOnly ) const + { + isStatic = false; + superOnly = false; + if( !ch.ctor.empty() ) + { + return unalias( r.fileId, ch.ctor ); + } + const std::string_view root = ch.root; + if( isThisRoot( root ) || ( r.lang == Lang::Python && root == "cls" ) ) + { + return callerClass( r.fromSymbol ); + } + if( isSuperRoot( root ) ) + { + superOnly = true; + return callerClass( r.fromSymbol ); + } + if( const std::string* t = typedLocal( r.fromSymbol, root ) ) + { + return unalias( r.fileId, *t ); + } + const std::string_view cls = unalias( r.fileId, root ); + if( classNames.contains( std::string( cls ) ) && !localType.contains( keyOf( r.fromSymbol, '#', root ) ) ) + { + isStatic = true; + return cls; + } + return {}; + } + + // RESOLVE: the definitions this member call's typed receiver reaches, in `found`; false when the receiver proves + // nothing (untyped, a type this tree does not define, or a type that defines no such member anywhere in its cone). + bool narrow( const Reference& r ) const + { + // C++, Ruby, Rust and C keep their own receiver rules (Rules 1, 2, 2b, 2c): this one adds the languages that had none + const bool typedLang = r.lang == Lang::JavaScript || r.lang == Lang::TypeScript || r.lang == Lang::Python || r.lang == Lang::Go + || r.lang == Lang::Java || r.lang == Lang::Kotlin || r.lang == Lang::CSharp || r.lang == Lang::Swift; + if( !active || r.fromSymbol >= ing.symbols.size() || !r.qualifier.empty() || !typedLang ) + { + return false; + } + if( !isMemberCallRef( r ) ) + { + return r.lang == Lang::Python && narrowAliasedMethod( r ); + } + const Chain ch = chainOf( r ); + if( !ch.valid ) + { + return false; + } + bool isStatic = false, superOnly = false; + std::string_view type = rootClass( r, ch, isStatic, superOnly ); + if( type.empty() ) + { + return false; + } + std::string_view rest = ch.path; + while( !rest.empty() ) // walk the stated field classes + { + const std::size_t dot = rest.find( '.' ); + const std::string_view field = rest.substr( 0, dot ); + rest = dot == std::string_view::npos ? std::string_view{} : rest.substr( dot + 1 ); + type = fieldOf( type, field ); + superOnly = false; + if( type.empty() ) + { + return false; + } + } + return methodOf( type, r.calleeName, superOnly ); + } + + // Python `feed = parser.feed; … feed( data )`: the alias names a typed object's method + bool narrowAliasedMethod( const Reference& r ) const + { + if( r.recv != RecvKind::None || r.calleeName.empty() ) + { + return false; + } + NodeId scope = kNoNode; + const std::pair* alias = aliasedMethod( r.fromSymbol, r.calleeName, scope ); + if( alias == nullptr ) + { + return false; + } + const std::string* type = typedLocal( scope, alias->first ); + return type != nullptr && methodOf( unalias( r.fileId, *type ), alias->second, false ); + } + + // does module alias `root` (bound in the caller's file) name the file defining `c` + bool moduleNames( const Reference& r, std::string_view root, NodeId c ) const + { + const auto it = moduleAlias.find( keyOf( r.fileId, '#', root ) ); + if( it == moduleAlias.end() || it->second.empty() ) + { + return false; + } + const std::string_view source = it->second; + const std::string_view target = rootRelPath( ing, ing.symbols[ c ].fileId ); + if( r.lang == Lang::Go ) + { + // an import path names a package DIRECTORY; the candidate's directory must end the path, segment-aligned + const std::string_view dir = includerDir( target ); + return !dir.empty() && source.size() >= dir.size() && source.ends_with( dir ) + && ( source.size() == dir.size() || source[ source.size() - dir.size() - 1 ] == '/' ); + } + if( !source.starts_with( "./" ) && !source.starts_with( "../" ) ) + { + return false; // a package specifier: outside the tree, or resolved by a bundler this rule does not model + } + // join the caller's directory with the relative specifier, then compare without the extension (or /index) + std::vector parts; + std::string_view base = includerDir( rootRelPath( ing, r.fileId ) ); + const auto push = [ & ]( std::string_view p ) + { + while( !p.empty() ) + { + const std::size_t slash = p.find( '/' ); + const std::string_view seg = p.substr( 0, slash ); + p = slash == std::string_view::npos ? std::string_view{} : p.substr( slash + 1 ); + if( seg.empty() || seg == "." ) { continue; } + if( seg == ".." ) { if( !parts.empty() ) { parts.pop_back(); } continue; } + parts.push_back( seg ); + } + }; + push( base ); + push( source ); + std::string joined; + for( std::string_view p : parts ) { if( !joined.empty() ) { joined.push_back( '/' ); } joined.append( p ); } + std::string_view stem = target; + if( const std::size_t dot = stem.rfind( '.' ); dot != std::string_view::npos && stem.find( '/', dot ) == std::string_view::npos ) + { + stem = stem.substr( 0, dot ); + } + return stem == joined || ( stem.ends_with( "/index" ) && stem.substr( 0, stem.size() - 6 ) == joined ); + } +}; + +} // namespace rw diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..c415d1b3f 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5563,6 +5563,9 @@ struct CalleeCallsSink // have a query. The ordering is a property of a CUT listing, not of a rendering, so the condition is // now the rank alone and packBodies threads one in (`calleeRank`); gate test/callsrankordercheck.sh. const std::vector* rank = nullptr; + // FE-B: Graph::outNameOnly (parallel to outTargets), so a callee row whose edge is name-only carries via="name" — the + // same bit --callees prints. nullptr / empty ⇒ no row is marked. + const std::vector* nameOnly = nullptr; }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` @@ -5695,17 +5698,18 @@ struct MergedCalleeNameRow { std::string_view name; // a view into ing.symbols — stable for the emitter's lifetime std::vector lines; // every definition line of that name; joined ascending at append time + bool via = false; // FE-B: these edges are name-only — merged only with rows of the same bit }; inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, - std::size_t& used, const CalleeCallsSink& sink ) + std::size_t& used, const CalleeCallsSink& sink, bool via = false ) { char lb[ 16 ]; rw::formatTo( lb, sizeof( lb ), "{}", cs.line ); bool merged = false; for( MergedCalleeNameRow& r : rows ) { - if( r.name == cs.name ) + if( r.name == cs.name && r.via == via ) { r.lines.push_back( cs.line ); merged = true; break; @@ -5713,10 +5717,10 @@ inline void collectCalleeNameRow( std::vector& rows, const } if( !merged ) { - rows.push_back( MergedCalleeNameRow { cs.name, { cs.line } } ); + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via } ); } - // the comma and the digits a merge appends, or the whole row it opens - used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16; + // the comma and the digits a merge appends, or the whole row it opens (FE-B: plus its 11-byte via="name") + used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( via ? 11u : 0u ); if( sink.recorded ) { sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, std::string() } ); // §H5: no sig to record @@ -5742,7 +5746,9 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector"; + callsBody += "\""; + callsBody += viaNameAttr( r.via ); // FE-B + callsBody += "/>"; } } @@ -5775,6 +5781,18 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector std::string callsBody; std::vector nameRows; // names-only rendering: collected, merged by name, written after the walk int shown = 0; + // FE-B: the edge id → cid's hedge bit (the walk reorders, so the bit is found by the target, ascending in the row) + const auto viaOf = [ & ]( NodeId cid ) -> bool + { + if( sink.nameOnly == nullptr || sink.nameOnly->empty() ) + { + return false; + } + const auto b = outTargets.begin() + outOff[ id ]; + const auto e = outTargets.begin() + outOff[ id + 1 ]; + const auto it = std::lower_bound( b, e, cid ); + return it != e && *it == cid && ( *sink.nameOnly )[ std::size_t( it - outTargets.begin() ) ] != 0; + }; for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < 16 && used < budgetBytes; ++k ) { const NodeId cid = walk[ k - outOff[id] ]; @@ -5783,11 +5801,12 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector continue; } const Symbol& cs = ing.symbols[cid]; + const bool via = viaOf( cid ); // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) { - collectCalleeNameRow( nameRows, cs, used, sink ); + collectCalleeNameRow( nameRows, cs, used, sink, via ); ++shown; continue; } @@ -5802,10 +5821,10 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector { continue; } - char hb[ 32 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\">", cs.line ); + char hb[ 48 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}>", cs.line, viaNameAttr( via ) ); // FE-B: via="name" callsBody += "& nodes, std::size_t budgetBytes, const std::vector& outOff, const std::vector& outTargets, + const std::vector& outNameOnly, // FE-B: Graph::outNameOnly — the rows' via="name" bool compress = false, RedactCounts* redact = nullptr, const HashMap* ranges = nullptr, const notes::NoteIndex* noteIndex = nullptr, // L3: field notes — surfaces children on each @@ -6479,7 +6499,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // total=/shown=/capped= block — see emitCalleeCallsBlock above; `calleeRank` decides which // rows survive when it CUTS one, which is far from rare here (CalleeCallsSink::rank). emitCalleeCallsBlock( piece, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank } ); + CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly } ); const std::string bodyNotes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 piece += bodyNotes; used += bodyNotes.size(); // W3-N2: same charge-never-trim rule @@ -6551,6 +6571,10 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect { w.write( kOverCeilingBodyLegend ); // the same rule, for } + if( children.find( " via=\"name\"" ) != std::string::npos ) + { + w.write( viaNameLegendComment() ); // FE-B: the same rule, for a row's via="name" + } w.write( children ); w.write( "" ); w.flush(); @@ -6583,6 +6607,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector& nodes, std::size_t budgetBytes, const std::vector& outOff, const std::vector& outTargets, + const std::vector& outNameOnly, // FE-B: Graph::outNameOnly — the rows' via="name" RedactCounts* redact = nullptr, std::size_t* outShown = nullptr, // rows actually emitted; nullptr ⇒ not recorded const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing @@ -6672,7 +6697,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // the 1-hop callee signatures — the identical block a body carries, charged against the same // running `used` so the row identity bytes and the edge bytes share one budget. emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank } ); + CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly } ); row += ""; children += row; ++shownCount; @@ -6694,6 +6719,10 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector rw::formatTo( open, sizeof( open ), "", shownCount, requestedCount, shownCount < requestedCount ? 1 : 0 ); } + if( children.find( " via=\"name\"" ) != std::string::npos ) + { + w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" + } w.write( open ); w.write( children ); w.write( "" ); diff --git a/src/tracelocus.h b/src/tracelocus.h index 9426f8cdc..29cffffab 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -1232,7 +1232,7 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph } const std::vector bodyIds = traceBodyIds( part, hop ); // LB-A: innermost frame, then the top hop row - packBodies( m, ing, bodyIds, in.bodyBudgetBytes, g.outOff, g.outTargets, in.compress, in.redact, + packBodies( m, ing, bodyIds, in.bodyBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, in.compress, in.redact, /*ranges=*/nullptr, in.notes, // L3: the rank-1 body surfaces notes too /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, in.rootArg, // R-R: root-relative diff --git a/src/verbs_for.h b/src/verbs_for.h index dfe6a6967..2ef1f3b9b 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1922,7 +1922,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR // shell the "budget" branch below already gets for free, rather than a second hand-rolled tag. out.attr = " bundle=\"auto\" bodies=\"0\" reason=\"no_candidates\""; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, nullptr, /*truncateOversizedFirst=*/false, /*withFileContext=*/false, fabRootArg, &lensRank ); }, rw::kBytesPerTokenBody ); @@ -1944,7 +1944,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR const std::size_t autoBodyBudget = std::max( 1, std::min( leftBytes, cfg.packBudgetBytes ) ); rw::EmittedBodies autoEmitted; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &autoEmitted, /*truncateOversizedFirst=*/false, /*withFileContext=*/false, fabRootArg, &lensRank ); }, rw::kBytesPerTokenBody ); @@ -2113,7 +2113,7 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest : std::size_t( 1 ) ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopIds, hopBudget, g.outOff, g.outTargets, redactPtr, /*outShown=*/nullptr, &lensRank, fcRootArg ); }, + { rw::packHops( f, ing, hopIds, hopBudget, g.outOff, g.outTargets, g.outNameOnly, redactPtr, /*outShown=*/nullptr, &lensRank, fcRootArg ); }, // MARKUP rate, not the body rate — and this is an honesty choice, not a copy-paste slip. The body // rate (3.80 B/tok) prices SOURCE TEXT; the compact section contains none, only tags, identifiers // and line numbers, which tokenize like the rest of the bundle. Charging structured markup at the @@ -3233,7 +3233,7 @@ std::optional runForLens( const MainDispatch& d ) detailBodyBudget = std::min( detailBodyBudget, leftBytes ); } detailSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, cfg.compress, redactPtr, + { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, flRootArg, &lensRank ); }, // L3: --detail bodies surface notes too (part of the --for bundle) rw::kBytesPerTokenBody ); @@ -3517,7 +3517,7 @@ std::optional runForLens( const MainDispatch& d ) // directly at the SAME budget — uncharged for that one run, with an alert, never a fabricated number. if( cfg.detail > 0 ) { - rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, + rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, flRootArg, &lensRank ); } ); @@ -3667,7 +3667,7 @@ std::optional runTargetedViews( const MainDispatch& d ) fin( pick.winner ), wsym.ccx, exemplarRootAttr.c_str(), ts( pick.winner ) ? " tested=\"1\"" : "", pick.lowConfidence ? " low_confidence=\"1\"" : "", pick.overCcxBar ? " over_ccx_bar=\"1\"" : "" ); - packBodies( stdout, ing, { pick.winner }, cfg.packBudgetBytes, g.outOff, g.outTargets, cfg.compress, redactPtr, + packBodies( stdout, ing, { pick.winner }, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, tvRootArg ); rw::emitRaw( stdout, "" ); diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 62d43e5d4..c28b87786 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -73,17 +73,18 @@ inline void emitGraphQueryLegend( const rw::IngestResult& ing, const rw::Graph& // carries "d":N, the XML row's d=; a JSON row is read on its own, so the key rides every row, never only on a change. inline void printJsonSymbolRows( const rw::IngestResult& ing, const std::vector& ids, std::size_t begin, std::size_t end, std::string_view rootPrefix = {}, const std::vector* testReach = nullptr, - const std::vector* depth = nullptr ) + const std::vector* depth = nullptr, const std::vector* viaName = nullptr ) { for( std::size_t i = begin; i < end; ++i ) { const rw::Symbol& s = ing.symbols[ ids[i] ]; const std::string_view p = rootPrefix.empty() ? std::string_view( ing.files[ s.fileId ] ) : rw::sarif::rootRelativeUri( ing.files[ s.fileId ], rootPrefix ); - rw::emitTo( stdout, "{}{{\"t\":\"{}\",\"n\":\"{}\",\"p\":\"{}:{}\"{}{}}}", i == begin ? "" : ",", + rw::emitTo( stdout, "{}{{\"t\":\"{}\",\"n\":\"{}\",\"p\":\"{}:{}\"{}{}{}}}", i == begin ? "" : ",", rw::symTag( s.kind ), rw::jsonStr( s.name ).c_str(), rw::jsonStr( p ).c_str(), s.line, depth ? ",\"d\":" + std::to_string( ( *depth )[ ids[i] ] ) : std::string(), - ( testReach && rw::isTestedByReach( ing, *testReach, ids[i] ) ) ? ",\"tested\":true" : "" ); + ( testReach && rw::isTestedByReach( ing, *testReach, ids[i] ) ) ? ",\"tested\":true" : "", + ( viaName && i < viaName->size() && ( *viaName )[ i ] != 0 ) ? ",\"via\":\"name\"" : "" ); // FE-B: the row's hedge } } @@ -219,13 +220,20 @@ std::optional runCallHierarchy( const MainDispatch& d ) // of= and answers with ordinary rows, so the kind still needs its reading in that document. const bool chHasModScope = anyModuleScopeRow( ing, std::span( result ).subspan( pw.begin, pw.end - pw.begin ) ) || anyModuleScopeRow( ing, matches ); + // FE-B: exactly when a row on THIS page is via="name" + bool chHasVia = false; + for( std::size_t i = pw.begin; i < pw.end && !chHasVia; ++i ) + { + chHasVia = rowNameOnly( g, matches, result[ i ], wantCallers ); + } rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap ).active ), rw::declinedCallsLegendWithGate( chRows.declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls= rw::declinedIfaceLegend( chRows.declinedIface > 0 ), // likewise, exactly when declined_iface= is there ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there - + rw::crossKindLegend( !chRows.crossKind.empty() ) ).c_str(), // hono-07: likewise for cross_kind= + + rw::crossKindLegend( !chRows.crossKind.empty() ) // hono-07: likewise for cross_kind= + + rw::viaNameLegend( chHasVia, false, cfg.columnar ) ).c_str(), // FE-B: likewise for via="name" rw::modScopeLegend( chHasModScope ), // #60: likewise, exactly when a t="modscope" row is ( rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ) + rw::valueRefsLegend( !chRows.valueRefs.rows.empty(), wantCallers ) ).c_str(), rw::rootRelPathsLegend( chSingleRoot ), @@ -268,7 +276,12 @@ std::optional runCallHierarchy( const MainDispatch& d ) cfg.pageLimit, cfg.pageOffset, chDiscloseCap ) + rw::graphCountFloorAttrXml( g ) // §H4 §3.4 — every dialect carries the marker + chNextAttr; // P3 (L7): the same next= on the columnar root - emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach ); + std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it + for( std::size_t i = 0; i < page.size(); ++i ) + { + pageVia[ i ] = rowNameOnly( g, matches, page[ i ], wantCallers ) ? 1 : 0; + } + emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, &pageVia ); return 0; } @@ -294,7 +307,12 @@ std::optional runCallHierarchy( const MainDispatch& d ) cfg.pageLimit, cfg.pageOffset, chDiscloseCap, kJsonPageSyntax ), rw::graphCountFloorAttrJson( g ).c_str() ); // §H4 §3.4 — the JSON dialect's spelling of the same marker rw::emitTo( stdout, ",\"{}\":[", tag ); - printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach ); + std::vector chVia( result.size(), 0 ); // FE-B: the same hedge bit the XML rows carry + for( std::size_t i = pw.begin; i < pw.end; ++i ) + { + chVia[ i ] = rowNameOnly( g, matches, result[ i ], wantCallers ) ? 1 : 0; + } + printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia ); rw::emitTo( stdout, "]{}}}", rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); return 0; } @@ -319,8 +337,9 @@ std::optional runCallHierarchy( const MainDispatch& d ) const std::string_view rp = chSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ s.fileId ], chRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — the same absence-meaningful convention tested= // already follows everywhere else (serialize.h/verbs_for.h), so an untested row costs 0 bytes. - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, - macroRoleAttr( s.kind ), rw::isTestedByReach( ing, chTested.testReach, result[i] ) ? " tested=\"1\"" : "" ); + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + macroRoleAttr( s.kind ), rw::isTestedByReach( ing, chTested.testReach, result[i] ) ? " tested=\"1\"" : "", + viaNameAttr( rowNameOnly( g, matches, result[i], wantCallers ) ) ); // FE-B: a name-only edge says so on its row } rw::emitTo( stdout, "{}", rw::valueRefsXml( ing, chRows.valueRefs, wantCallers, chVr, chVrNext ), tag ); return 0; @@ -2156,8 +2175,13 @@ std::optional runPath( const MainDispatch& d ) // Reference-as-value round: with no directed call path, how often to= is used as a VALUE — a run through such a // slot is not a hop the graph can show, so the count is the clue (the callers verb lists the binding sites). const std::size_t pthToValueRefs = rw::toValueRefsCount( ing, path.empty(), dstDefs ); + bool pthHasVia = false; // FE-B: exactly when a hop of THIS path is via="name" + for( std::size_t i = 1; i < path.size() && !pthHasVia; ++i ) + { + pthHasVia = edgeNameOnly( g, path[ i - 1 ], path[ i ] ); + } rw::emitTo( stdout, "{}", rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ).c_str(), + "graph holds none. {}{}{}-->{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) ).c_str(), rw::toValueRefsLegend( pthToValueRefs > 0 ), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); rw::emitTo( stdout, " runPath( const MainDispatch& d ) ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : "" ); } rw::emitTo( stdout, ">" ); - for( NodeId n : path ) + for( std::size_t i = 0; i < path.size(); ++i ) { - const Symbol& s = ing.symbols[n]; + const Symbol& s = ing.symbols[ path[i] ]; const std::string_view rp = pthSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ s.fileId ], pthRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line ); + // FE-B: a hop whose edge INTO it is name-only says so on its row (the first row is no hop) + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + viaNameAttr( i > 0 && edgeNameOnly( g, path[ i - 1 ], path[ i ] ) ) ); } rw::emitTo( stdout, "" ); return 0; @@ -2307,8 +2333,20 @@ struct ImpactView const std::vector& depth; // 0.6.5: transitiveCallersDepth's hop per node — the row's d= std::span byDepth; // 0.6.5: graph.h depthCounts over the FULL reach set — the root's by_depth= const rw::ValueRefRows& valueRefs; // reference-as-value round: SYM's binding sites — never in reaches= + const std::vector& provenReach; // FE-B (D3): graph.h provenCallerReach — a row outside it is via="name" }; +// FE-B: the window's hedge bits in row order (impactRowNameOnly), for the dialects that carry them per entry or per column +inline std::vector impactViaColumn( const ImpactView& v, std::size_t begin, std::size_t end ) +{ + std::vector via( v.show.size(), 0 ); + for( std::size_t i = begin; i < end; ++i ) + { + via[ i ] = rw::impactRowNameOnly( v.provenReach, v.show[ i ] ) ? 1 : 0; + } + return via; +} + // --format=columnar (RESEARCH lever 1): same page window, path-table + parallel arrays. // V1-1: a fixed 160-byte buffer truncated mid-attribute on long escaped symbol names (invalid XML, the // F6 class), and this branch hand-rolled shown=/capped= without the paging half — the one @@ -2353,7 +2391,9 @@ int emitImpactColumnar( const ImpactView& v ) : " lens=\"shown_importers,importers_capped,importers_next\"" ) + rw::renderDisclosure( v.prD, rw::DiscloseAs::XmlAttrs ) // W2-F + rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ); // P3 (L7): the XML root's next=, same set - emitColumnarSymbolRows( stdout, v.ing, "impact", attr.c_str(), rows, v.rootPrefix, v.testReach, &v.depth ); + const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); + const std::vector rowVia( via.begin() + v.page.begin, via.begin() + v.page.end ); // FE-B: in `rows` order + emitColumnarSymbolRows( stdout, v.ing, "impact", attr.c_str(), rows, v.rootPrefix, v.testReach, &v.depth, &rowVia ); return 0; } @@ -2389,7 +2429,8 @@ int emitImpactJson( const ImpactView& v ) v.pageLimit, v.pageOffset, true, kJsonPageSyntax ), rw::graphCountFloorAttrJson( v.g ).c_str(), // §H4 §3.4 rw::renderDisclosure( v.prD, rw::DiscloseAs::JsonKeys ).c_str() ); // W2-F: ONE keyset - printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth ); + const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); // FE-B: the rows' hedge, per entry + printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth, &via ); rw::emitTo( stdout, "],\"import_reach\":[" ); rw::emitImportRowsJson( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitTo( stdout, "]{}}}", rw::valueRefsJson( v.ing, v.valueRefs, true, rw::VrRender{ v.singleRoot, v.rootPrefix }, "vrs", @@ -2425,9 +2466,10 @@ int emitImpactXml( const ImpactView& v ) const std::string_view rp = v.singleRoot ? rw::sarif::rootRelativeUri( v.ing.files[ s.fileId ], v.rootPrefix ) : std::string_view( v.ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= the row's hop depth, run-length (graph.h depthRunAttrXml). - rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, + rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, rw::depthRunAttrXml( v.show, v.depth, i, v.page.begin ), - rw::isTestedByReach( v.ing, *v.testReach, v.show[i] ) ? " tested=\"1\"" : "" ); + rw::isTestedByReach( v.ing, *v.testReach, v.show[i] ) ? " tested=\"1\"" : "", + viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ) ); // FE-B (D3): reached only through a name-only edge } rw::emitImportRowsXml( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitTo( stdout, "{}", rw::valueRefsXml( v.ing, v.valueRefs, true, rw::VrRender{ v.singleRoot, v.rootPrefix }, @@ -2464,6 +2506,7 @@ std::optional runImpact( const MainDispatch& d ) // 0.6.5: the SAME walk, keeping each node's hop depth — the rows' d= and the root's by_depth=. std::vector imDepth; const std::vector reach = rw::transitiveCallersDepth( g, seeds, &imDepth ); + const std::vector imProven = rw::provenCallerReach( g, seeds ); // FE-B (D3): the all-proven part of the reach const auto [ rank, prIters, prConverged ] = rankGraph( g ); const rw::RankDisclosure prD{ prIters, prConverged, true }; // W2-F: the listing is PageRank-ordered within a depth std::vector show = reach; @@ -2514,7 +2557,9 @@ std::optional runImpact( const MainDispatch& d ) rw::emitTo( stdout, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}{}-->", rw::kImpactLegendOpen, rw::kPageRaiseCapClause, reach.empty() ? "" : rw::impactDepthLegend( cfg.columnar ), // 0.6.5: exactly when rows (d=) and by_depth= exist cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend, - rw::impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ).c_str(), // #220: exactly when the root carries them + ( rw::impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them + + rw::viaNameLegend( std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, // FE-B: exactly when a shown row is via="name" + [ & ]( NodeId n ) { return rw::impactRowNameOnly( imProven, n ); } ), true, cfg.columnar ) ).c_str(), rw::testedLensLegend( cfg.columnar ), rw::kImpactTestedPartitionLegend, // A6: the columnar form reads its dense column rw::kTestedLensBlindSpotLegend, // F-02: rides with the partition rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Impact, imUnprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs= @@ -2532,7 +2577,8 @@ std::optional runImpact( const MainDispatch& d ) imPage, // the same window the legend predicate above reads — one expression, not two imports, importPage, importLazyPage, prD, imSingleRoot, imRootPrefix, imRootAttr, imSingleRoot ? cfg.roots[0] : std::string_view(), cfg.pageLimit, cfg.pageOffset, - &imTestReach, imRadiusTested, imRadiusUntested, imDeclinedCalls, imDeclinedIface, g, imDepth, imByDepth, imValueRefs }; + &imTestReach, imRadiusTested, imRadiusUntested, imDeclinedCalls, imDeclinedIface, g, imDepth, imByDepth, imValueRefs, + imProven }; if( cfg.columnar ) { return emitImpactColumnar( view ); } if( cfg.json ) { return emitImpactJson( view ); } From 344259e35f2ea7b381919c609b54c9d6df2638be Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 10:15:27 -0400 Subject: [PATCH 004/229] =?UTF-8?q?resolve:=20receiver-evidence=20fix=20ro?= =?UTF-8?q?und=20=E2=80=94=20qualified=20calls,=20free=20functions,=20buil?= =?UTF-8?q?tin=20names,=20self=20aliases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A qualified call (`A::m()`) keeps the ladder: a qualifier is the canonical tier's evidence axis, not a receiver's (cpptmplscopecheck: `Storage::reset()` stays a split, never a lone pin). - A name-only MEMBER call never reaches a free function in Python, Go, C++, Rust, Ruby, Java, C# and Swift; with nothing left it is external= (builtinbindcheck (N)). - A builtin-named name-only call keeps a CLASS-owned target only with the builtin-method gate's evidence, so the wider name-only tier never hands it every class's method in reach (builtinbindcheck (T)). - `write = self.write; write( b )` resolves through the caller's class (falseedgecheck alias.py). - A bodyless interface or abstract signature is never a resolve target: the call dispatches through it, and the ladder's decline (declined_iface=) stays the honest answer (declinecheck (I)). - lpincheck: the locality pin and the honest split keep their arms on shapes the receiver evidence leaves to the tie-break (pinnedcone.kt, modlevel.m); pinned.kt and modlevel.kt now pin the implicit receiver's resolution, and the tier-1 residual is listed only hedged (via="name"). - CHANGELOG entry. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 28 ++++++++++++++++ src/compactlegend.h | 7 ++++ src/graph.h | 40 ++++++++++++++++++++++- src/receiverevidence.h | 20 +++++++++++- test/lpincheck.sh | 67 +++++++++++++++++++++++++++----------- test/lpinfix/modlevel.kt | 11 +++---- test/lpinfix/modlevel.m | 30 +++++++++++++++++ test/lpinfix/pinned.kt | 10 +++--- test/lpinfix/pinnedcone.kt | 19 +++++++++++ 9 files changed, 199 insertions(+), 33 deletions(-) create mode 100644 test/lpinfix/modlevel.m create mode 100644 test/lpinfix/pinnedcone.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d20aaf5..827dc9ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,34 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — a call bound by name alone keeps its rows, marked `via="name"`; typed receivers resolve + +A member call `x.m()` (or, where the receiver is implicit, a bare `m()`) bound to every in-repo definition spelled +`m` that the name ladder reached, and each such row read as a confident edge with nothing behind it but the name: a +request context's `ctx.onerror()` drawn to the `Application.onerror` its file defines, a WeakMap's `.get()` drawn to +the same file's accessor, a Go struct field's `item.text.Get()` drawn to an unrelated `Merger.Get`, a bare `flush()` +in a Java class with an outside base drawn to an unrelated class's `flush`. + +- **Receivers the source types now resolve** (JavaScript, TypeScript, Python, Go, Java, Kotlin, C#, Swift): `this` / + `self` / `cls` inside a class and `super` / `base` through its bases; a parameter or local whose class is written + (a TS/Python/Go annotation, a Go method receiver, a JS `new Foo()`, Python `Foo()` or Go `Foo{}` initializer, an + import alias of the class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields + whose classes are stated (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields); + Python `feed = parser.feed`; a Ruby `include`d module. +- **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an + implicit receiver's class and bases, a free function in scope, the module a receiver alias names). With none + proven, every same-file and same-directory candidate is listed — a lone global one too, because one candidate is + not evidence — and each row carries `via="name"` (MCP: `"via":"name"`) on `--callees`, `--callers`, `--path`, + `--connect`'s ``, the `` rows of `--expand` and `--for`, and MCP `find_symbol`, + `find_referencing_symbols`, `impact` and `path_between`. An `--impact` row that no all-proven path reaches inherits + it. A legend sentence rides exactly when such a row does and says what it does not mean: not that the edge is + false. Rule 3's include-file narrow no longer decides a member call: the file a caller imports says nothing about + its receiver. +- Measured on the round-1 comparison-table calls for this tool: see the lane report; the gate is + `test/receiverevidencecheck.sh` (212 arms: every surface, CLI/MCP parity, census conservation, and near misses + for every evidence rule). Ingest records the receiver chain (`kParserVer` 142, `kCacheVersion` 29), so a cache + written by an earlier build is re-parsed. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..19f9d0aa0 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -543,6 +543,13 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // root's attribute; a signature row's tested= is the next row's. No earlier sweep saw it because the gate fixture holds // no test (compactlegendcheck (D31) builds the smallest tree that prints one). { "tested", ": a non-test row an indexed test transitively reaches (absent otherwise, never 0)", true, "s" }, + // FE-B (test/receiverevidencecheck.sh): via="name" on a call row — graphlegend.h kViaNameLegend is the full reading. ELEMENT- + // and VALUE-qualified (// whose via= lists name), so impact's importer rows never pull it in; the + // columnar form names it in fields= like the tested column. + { "via", "via=name: the target was matched by name alone (receiver unproven); every by-name candidate in reach is listed; NOT a claim the edge is false", true, "s", MapHeaderRead::No, "name" }, + { "via", ": that callee matched by name alone (receiver unproven); NOT a claim the edge is false", true, "c", MapHeaderRead::No, "name" }, + { "via", ": that edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "e", MapHeaderRead::No, "name" }, + { "fields", " column: 1 = via=name, the row's edge matched by name alone (receiver unproven); NOT a claim the edge is false", true, "cols", MapHeaderRead::No, "via" }, // DEPTH-LABELLED --impact (0.6.5): graph.h transitiveCallersDepth's hop per row. The row prints d= run-length // (graph.h depthRunAttrXml: the window's first row and each depth change), the columnar form a dense column, and // the root by_depth= (graphlegend.h byDepthAttrXml) partitions reaches= by depth; all three are absent at reaches="0". diff --git a/src/graph.h b/src/graph.h index 30ab382c9..f7e6be8da 100644 --- a/src/graph.h +++ b/src/graph.h @@ -185,6 +185,7 @@ inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept { return false; } + EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "the hedge bit is parallel to outTargets (buildGraph allocates it per edge)" ); const auto b = g.outTargets.begin() + g.outOff[ from ]; const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; const auto it = std::lower_bound( b, e, to ); // ascending within a source (buildGraph sorts by (from, to)) @@ -4673,7 +4674,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const rw::SmallVec* nameIds = localsYield ? &reachScratch : baseIds; // FE-B: for a MEMBER call the include graph says which file the caller imports, never what its receiver is — the // included file's definitions join the name-only tier below instead of deciding the call. - const bool hedgeEligible = !scipPinned && !canonical && receiverHedgeLang( r.lang ) && r.role == RefRole::Call; + // a QUALIFIED call (`A::m()`, `Storage::reset()`) names its scope: that is the canonical tier's evidence axis, + // not a receiver's, so a qualifier the canonical tier could not place keeps the ladder exactly as before + const bool hedgeEligible = !scipPinned && !canonical && receiverHedgeLang( r.lang ) && r.role == RefRole::Call && r.qualifier.empty(); memberRule3.clear(); if( !scipPinned && !canonical && !narrowed && it != byName.end() ) { @@ -4849,6 +4852,20 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null cand.swap( provedScratch ); } } + // a name-only MEMBER call never reaches a free function where the language has no way to (Python, Go, C++, + // Rust, Ruby, Java, C#, Swift: a function is reached through a member only as a method, or through a module + // alias, which proves itself above) — JS/TS object properties, Kotlin extensions and C function-pointer fields + // can hold one, so those keep it. Nothing left: no in-repo definition can answer the call (external=). + if( nameOnly && isMemberCallRef( r ) && memberNeverReachesFreeFunction( r.lang ) ) + { + std::erase_if( cand, [ & ]( NodeId c ) { return ownerIsClass( c ).empty() && ing.symbols[ c ].kind == SymKind::Function; } ); + std::erase_if( memberRule3, [ & ]( NodeId c ) { return ownerIsClass( c ).empty() && ing.symbols[ c ].kind == SymKind::Function; } ); + if( cand.empty() ) + { + disposition = vetoExternal( r ); + continue; + } + } } // ---- tier ladder (the name-based fallback) — SKIPPED when SCIP pinned this site, and for Rule 2's class-identity CLAIM (a type fact, not a locality guess) const bool identityClaim = narrowed && narrower.identityClaimFor( r ); @@ -5114,6 +5131,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // another — rubygems' composed_set.rb). Widening tier 1 past the caller would invent a // cross-file edge the SAME-FILE tier already outranked, and `other.each` on a second instance // of the caller's own class is a genuine self-loop, so the honest nothing stands. + // FE-B narrows that floor to calls WITH receiver evidence: a NAME-ONLY call never reaches this + // block (its tier keeps every same-file and same-directory candidate, receiverevidence.h), so + // there the cross-file candidate IS listed — hedged via="name", never as a confident edge. const std::size_t sh = ( c == r.fromSymbol ) ? 0 : localityRank( callerCanon, g.localityKey[c], ( r.recv == RecvKind::None && r.qualifier.empty() ) || r.recv == RecvKind::ThisObj, localityCap ); // path-scoped even for a free function locShare.push_back( sh ); if( sh > bestShare ) @@ -5179,6 +5199,22 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null gateRefused.push_back( c ); } } + if( anyReal && anyAdmitted && nameOnly ) + { + // FE-B: a name-only call's tier is WIDER than the ladder's pick (every same-file and same-directory candidate), + // so "the decision stands" would hand a builtin-named call every class's method in reach. A CLASS-owned + // target still needs the gate's evidence; a module or object-literal member (no class to look for) stays. + std::erase_if( tier, [ & ]( NodeId c ) + { + if( c == r.fromSymbol ) + { + return false; + } + const std::uint32_t owner = builtinGate.ownerOf( c ); + return owner != BuiltinMethodGate::kNoClass && owner != BuiltinMethodGate::kNested + && builtinGate.judge( r, c, chaCones ) != BuiltinMethodGate::Verdict::Admit; + } ); + } if( anyReal && !anyAdmitted ) { if( gateRefused.empty() ) @@ -5414,6 +5450,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } if( !g.outNameOnly.empty() ) { + ASSUME( pos < g.outNameOnly.size(), "outNameOnly is allocated one slot per flattened edge" ); g.outNameOnly[ pos ] = hedged( ( std::uint64_t( e.from ) << 32 ) | e.to ) ? 1u : 0u; } } @@ -8573,6 +8610,7 @@ inline std::vector provenCallerReach( const Graph& g, std::span s.sigEndByte ) { methodsByType[ typeKey( ownerClass[ s.id ], "::", s.name ) ].push_back( s.id ); } @@ -415,6 +428,7 @@ struct ReceiverEvidence // answer is in `found`; false when nothing in the cone defines it. superOnly skips `type` itself (`super.m()`). bool methodOf( std::string_view type, std::string_view name, bool superOnly ) const { + EXPECTS( active, "methodOf reads the tables build() fills" ); found.clear(); if( type.empty() ) { @@ -604,6 +618,10 @@ struct ReceiverEvidence { return false; } + if( isThisRoot( alias->first ) || alias->first == "cls" ) + { + return methodOf( callerClass( scope ), alias->second, false ); // `write = self.write`: the class's own member + } const std::string* type = typedLocal( scope, alias->first ); return type != nullptr && methodOf( unalias( r.fileId, *type ), alias->second, false ); } diff --git a/test/lpincheck.sh b/test/lpincheck.sh index e2870c807..2a8e1bb7f 100755 --- a/test/lpincheck.sh +++ b/test/lpincheck.sh @@ -13,11 +13,17 @@ # re-applies the `Graph::localityKey` tie-break (an unscoped def is compared as `path::name`, not its bare # name) so a module-level function is no longer auto-lost to a same-file class method. # -# THE FIXTURE (test/lpinfix/, 3 Kotlin files — Python until FE-A, whose bare call reaches no method: test/falseedgecheck.sh): -# pinned.kt — `Alpha.run` -> `helper()`; `Alpha.helper` beats `Beta.helper` by scope: ONE edge, `lpin="1"`, no `amb=`. +# THE FIXTURE (test/lpinfix/: Kotlin — Python until FE-A, whose bare call reaches no method: test/falseedgecheck.sh — and +# one Objective-C file, a language FE-B's receiver evidence does not narrow: test/receiverevidencecheck.sh): +# pinnedcone.kt — `Kappa.run` -> `step()`; Kappa's override beats the `Lambda.step` it inherits by scope: ONE edge, +# `lpin="1"`, no `amb=`. (The pin shape; pinned.kt carried it until FE-B.) +# pinned.kt — `Alpha.run` -> `helper()`; since FE-B the implicit receiver PROVES `Alpha.helper` (`Beta.helper` is an +# unrelated class): ONE edge, no `amb=`, and no `lpin=` — evidence decided, not a prior. # tied.kt — `Eps.go` -> `other()`; sibling classes tie: split, `amb="1"`, no `lpin=`. -# modlevel.kt — `Caller.go` -> `compute()`; `Helper.compute` vs top-level `compute`: a full tie under -# localityKey ⇒ split, `amb="1"`, no `lpin=` (was a silent pin on Helper::compute). +# modlevel.m — `-[Caller go]` -> `compute()`; the C function vs `-[Helper compute]`: a full tie under localityKey ⇒ +# split, `amb="1"`, no `lpin=` (was a silent pin on the method). modlevel.kt carried it until FE-B. +# modlevel.kt — `Caller.go` -> `compute()`; since FE-B the implicit receiver reaches the top-level `compute` and never +# `Helper.compute`: ONE edge to the module-level def, no `amb=`, no `lpin=`. # # Exits non-zero on any failure. @@ -45,13 +51,20 @@ MAP="$( cat "$TMP/map.xml" )" row(){ _n="${1##*::}"; _r="${1#*::}"; _s="${_r%::*}"; printf '%s' "$MAP" | tr '<' '\n' | grep "n=\"$_n\" sc=\"$_s\"" | head -1; } # ── (A) the pin is DISCLOSED on its row, and it is still not an amb ────────────────────────────── -RUN_ROW="$( row 'pinned.kt::Alpha::run' )" -printf '%s' "$RUN_ROW" | grep -q 'lpin="1"' && ok "(A) pinned.kt::Alpha::run carries lpin=\"1\" — the locality pin is disclosed" \ - || no "(A) pinned.kt::Alpha::run has no lpin=\"1\": $RUN_ROW" -printf '%s' "$RUN_ROW" | grep -q 'amb=' && no "(A) pinned.kt::Alpha::run carries amb= — the marker inflated amb=: $RUN_ROW" \ +RUN_ROW="$( row 'pinnedcone.kt::Kappa::run' )" +printf '%s' "$RUN_ROW" | grep -q 'lpin="1"' && ok "(A) pinnedcone.kt::Kappa::run carries lpin=\"1\" — the locality pin is disclosed" \ + || no "(A) pinnedcone.kt::Kappa::run has no lpin=\"1\": $RUN_ROW" +printf '%s' "$RUN_ROW" | grep -q 'amb=' && no "(A) pinnedcone.kt::Kappa::run carries amb= — the marker inflated amb=: $RUN_ROW" \ || ok "(A) the pin still contributes nothing to amb=" +N_STEP="$( printf '%s' "$MAP" | tr '>' '\n' | awk '/n="run" sc="Kappa"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="step"' )" +if [ "$N_STEP" = 1 ]; then ok "(A) the pin still emits ONE confident edge"; else no "(A) $N_STEP step edges on Kappa::run, want 1"; fi +# FE-B: the shape pinned.kt carried is now EVIDENCE (an implicit `this` never reaches the unrelated Beta) — still ONE +# edge and no amb=, and no lpin= because no prior picked it +RUN_ROW="$( row 'pinned.kt::Alpha::run' )" +printf '%s' "$RUN_ROW" | grep -q 'lpin=\|amb=' && no "(A2) pinned.kt::Alpha::run carries lpin=/amb= — the implicit receiver should decide it: $RUN_ROW" \ + || ok "(A2) pinned.kt::Alpha::run: neither lpin= nor amb= — the implicit receiver decided" N_HELPER="$( printf '%s' "$MAP" | tr '>' '\n' | awk '/n="run" sc="Alpha"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="helper"' )" -if [ "$N_HELPER" = 1 ]; then ok "(A) the pin still emits ONE confident edge"; else no "(A) $N_HELPER helper edges on Alpha::run, want 1"; fi +if [ "$N_HELPER" = 1 ]; then ok "(A2) ONE helper edge on Alpha::run"; else no "(A2) $N_HELPER helper edges on Alpha::run, want 1"; fi # ── (B) the tied control — a split is not a pin ─────────────────────────────────────────────────── GO_ROW="$( row 'tied.kt::Eps::go' )" @@ -61,14 +74,22 @@ printf '%s' "$GO_ROW" | grep -q 'lpin=' && no "(B) tied.kt::Eps::go carries lpin || ok "(B) no lpin= on the split" # ── (C) the module-level shape — a full tie under localityKey, not a silent pin ────────────────── -CALLER_ROW="$( row 'modlevel.kt::Caller::go' )" -printf '%s' "$CALLER_ROW" | grep -q 'amb="1"' && ok "(C) modlevel.kt::Caller::go is an honest split (amb=\"1\") — the module-level def is no longer auto-lost" \ - || no "(C) modlevel.kt::Caller::go is not amb=\"1\" — Helper::compute still silently wins: $CALLER_ROW" -printf '%s' "$CALLER_ROW" | grep -q 'lpin=' && no "(C) modlevel.kt::Caller::go carries lpin= — still pinned: $CALLER_ROW" \ +# the Objective-C twin: an unscoped row (no sc=), read inside its own group +CALLER_ROW="$( printf '%s' "$MAP" | tr '<' '\n' | awk '/^f p="modlevel.m"/{f=1;next} /^f /{f=0} f && /^s [^>]*n="go"/{print; exit}' )" +printf '%s' "$CALLER_ROW" | grep -q 'amb="1"' && ok "(C) modlevel.m: -[Caller go] is an honest split (amb=\"1\") — the module-level def is no longer auto-lost" \ + || no "(C) modlevel.m: -[Caller go] is not amb=\"1\" — the method still silently wins: $CALLER_ROW" +printf '%s' "$CALLER_ROW" | grep -q 'lpin=' && no "(C) modlevel.m: -[Caller go] carries lpin= — still pinned: $CALLER_ROW" \ || ok "(C) no lpin= on the module-level site" -grep -E '^C locality ' "$TMP/c.tsv" | grep -q 'modlevel.kt::Caller::go' \ - && no "(C) the census still labels modlevel.kt::Caller::go locality-pinned" \ - || ok "(C) the census agrees: no locality row for modlevel.kt::Caller::go" +grep -E '^C locality ' "$TMP/c.tsv" | grep -q 'modlevel.m::go' \ + && no "(C) the census still labels modlevel.m::go locality-pinned" \ + || ok "(C) the census agrees: no locality row for modlevel.m::go" +# FE-B: modlevel.kt's implicit receiver resolves it to the top-level compute — one plain edge, no split, no pin +CALLER_ROW="$( row 'modlevel.kt::Caller::go' )" +printf '%s' "$CALLER_ROW" | grep -q 'amb=\|lpin=' && no "(C2) modlevel.kt::Caller::go carries amb=/lpin= — the implicit receiver should decide it: $CALLER_ROW" \ + || ok "(C2) modlevel.kt::Caller::go: neither amb= nor lpin= — the implicit receiver decided" +grep -E '^C unique ' "$TMP/c.tsv" | grep 'modlevel.kt::Caller::go' | grep -q ' modlevel.kt::compute#' \ + && ok "(C2) the census binds modlevel.kt::Caller::go to the top-level compute alone" \ + || no "(C2) the census does not bind modlevel.kt::Caller::go to the top-level compute alone: $( grep 'modlevel.kt::Caller::go' "$TMP/c.tsv" )" # ── (D) the header counter, and its equality with the census ────────────────────────────────────── HDR="$( printf '%s' "$MAP" | grep -o '' | head -1 )" @@ -155,10 +176,18 @@ class Beta: def ping(self, n): return n PYEOF +# FE-B (test/receiverevidencecheck.sh) moved this residual BY DESIGN: `other.ping( 1 )` has no receiver evidence, so it +# is a NAME-ONLY call, and a name-only call keeps every same-file and same-directory candidate instead of tier 1 alone +# (src/graph.h's stated floor says so). The cross-file Beta.ping is therefore listed — but only HEDGED: via="name" on +# --callees, never a confident edge. What must still hold: the caller never wins (no self edge) and nothing is proven. RESH="$( "$BIN" "$RESD" --no-cache 2>/dev/null | grep -o '' | head -1 )" -printf '%s' "$RESH" | grep -q ' edges=0 ' \ - && ok "(I) stated floor holds: a caller alone in the SAME-FILE tier still yields no edge (tier 1 is untouched)" \ - || no "(I) the tier-1 residual moved — update this arm AND src/graph.h's stated floor: $RESH" +printf '%s' "$RESH" | grep -q ' edges=1 ' \ + && ok "(I) the name-only residual lists ONE edge (the cross-file candidate; the caller's own def stays a dropped self-loop)" \ + || no "(I) the tier-1 residual moved again — update this arm AND src/graph.h's stated floor: $RESH" +RESC="$( cd "$RESD" && "$BIN" . --no-cache --callees=a.py:ping 2>/dev/null )" +printf '%s' "$RESC" | grep -q '' \ + && ok "(I) …and that edge is HEDGED: --callees marks Beta.ping via=\"name\" (no confident cross-file edge)" \ + || no "(I) the residual's cross-file edge is not marked via=\"name\": $( printf '%s' "$RESC" | grep -o ']*/>' | tr '\n' ' ' )" # ── (H) determinism + well-formedness ───────────────────────────────────────────────────────────── # L1 (2026-09-19): the --pin-census run above asks for the full legend; the rerun it is compared to asks for the same. diff --git a/test/lpinfix/modlevel.kt b/test/lpinfix/modlevel.kt index 5eb40af51..a2c61008a 100644 --- a/test/lpinfix/modlevel.kt +++ b/test/lpinfix/modlevel.kt @@ -1,11 +1,10 @@ // The phase-3b repro: a TOP-LEVEL function against a same-file class method. // -// `Caller.go` bare-calls `compute()`. Two defs answer: `Helper.compute` (canonical id -// `modlevel.kt::Helper::compute`) and the top-level `compute` — whose canonical id degrades to the BARE -// NAME `compute`, sharing ZERO segments with any caller. Before phase 4 S6-C pinned `Helper::compute` -// silently on that asymmetry. With `Graph::localityKey` (`modlevel.kt::compute` for the unscoped def) both -// candidates share exactly `modlevel.kt::` — a full tie — so the call is an honest split: `amb="1"` on -// `Caller::go`, no `lpin=`, and `ambiguous=` counts it. (Kotlin: Python, the vehicle until FE-A, now binds the module def.) +// `Caller.go` bare-calls `compute()`. Before phase 4 S6-C pinned `Helper::compute` silently; phase 4's +// `Graph::localityKey` made it a full tie and an honest split. Since FE-B (test/receiverevidencecheck.sh) Kotlin's +// implicit receiver decides it: a bare call inside `Caller` reaches Caller's own members, its bases, or a top-level +// function — never the unrelated `Helper.compute`. ONE plain edge to the top-level `compute`, no `amb=`, no `lpin=`: +// the module-level def is not lost, it is the answer. The honest-split shape lives on in modlevel.m (Objective-C). class Caller { fun go(): Int { return compute() diff --git a/test/lpinfix/modlevel.m b/test/lpinfix/modlevel.m new file mode 100644 index 000000000..cc931792b --- /dev/null +++ b/test/lpinfix/modlevel.m @@ -0,0 +1,30 @@ +// The phase-3b repro, in a language FE-B (test/receiverevidencecheck.sh) does not narrow: a C FUNCTION against a +// same-file Objective-C method. `-[Caller go]` bare-calls `compute()`; the C function `compute` and `-[Helper compute]` +// share exactly `modlevel.m::` with the caller under Graph::localityKey — a full tie — so the call stays an honest split: +// `amb="1"` on `go`, no `lpin=`, and `ambiguous=` counts it. (modlevel.kt was this shape until FE-B: Kotlin's implicit +// receiver now proves the top-level `compute` and never `Helper.compute`, so that file pins the new resolution instead.) +#import + +int compute(void) { + return 6; +} + +@interface Caller : NSObject +- (int)go; +@end + +@interface Helper : NSObject +- (int)compute; +@end + +@implementation Caller +- (int)go { + return compute(); +} +@end + +@implementation Helper +- (int)compute { + return 5; +} +@end diff --git a/test/lpinfix/pinned.kt b/test/lpinfix/pinned.kt index a048607e1..164898e4b 100644 --- a/test/lpinfix/pinned.kt +++ b/test/lpinfix/pinned.kt @@ -1,9 +1,7 @@ -// The S6-C locality tie-break's PINNING shape (same as test/pincensusfix/pinned.kt). -// -// `Alpha.run` makes a BARE (implicit-`this`) `helper()` call. `Alpha.helper` and `Beta.helper` both live in THIS file, -// so the same-file rung keeps both and S6-C decides: `pinned.kt::Alpha::` beats `pinned.kt::`. ONE confident edge, NO -// `amb=` — and, since phase 4 (docs/EVALS.md "Phase 4"), `lpin="1"` on the caller row: the pin is a prior's guess and -// the map now says so instead of dressing it as an evidence-backed resolution. (Python until FE-A, see pincensusfix.) +// Was the S6-C locality tie-break's PINNING shape (same as test/pincensusfix/pinned.kt); since FE-B +// (test/receiverevidencecheck.sh) it is EVIDENCE: `Alpha.run` makes a BARE (implicit-`this`) `helper()` call, and an +// implicit `this` reaches Alpha's own members and its bases, never the unrelated `Beta.helper`. ONE confident edge to +// `Alpha.helper`, NO `amb=`, and NO `lpin=` — nothing was left for a prior to pick. The pin itself is pinnedcone.kt. class Alpha { fun helper(): Int { return 1 diff --git a/test/lpinfix/pinnedcone.kt b/test/lpinfix/pinnedcone.kt new file mode 100644 index 000000000..c13e01704 --- /dev/null +++ b/test/lpinfix/pinnedcone.kt @@ -0,0 +1,19 @@ +// The S6-C locality tie-break's PINNING shape, as FE-B (test/receiverevidencecheck.sh) leaves it. +// +// `Kappa.run` makes a BARE (implicit-`this`) `step()` call. The receiver is `this`, so only Kappa's own `step` and the +// one it inherits from `Lambda` can answer — both are proven candidates, both live in THIS file, and S6-C decides: +// `pinnedcone.kt::Kappa::` beats `pinnedcone.kt::Lambda::`. ONE confident edge, NO `amb=`, and `lpin="1"` on the caller +// row: which of an override and its base a call reaches is still a prior's pick, and the map says so. +open class Lambda { + open fun step(): Int { + return 1 + } +} +class Kappa : Lambda() { + override fun step(): Int { + return 2 + } + fun run(): Int { + return step() + } +} From 4cdf6b85f92f279208cb59468d1aa4394e4f001b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:10:27 -0400 Subject: [PATCH 005/229] resolve: leave Ruby's own method lookup alone; its mixin arm states the contract only Ruby's method lookup (mixins, typed and class receivers) is separate work, so the receiver-evidence rule no longer reads include/extend/prepend directives. Ruby keeps the language-neutral proof (an implicit receiver's class and bases, a top-level def); the logger.rb mixin arm asserts only that stamp is resolved or hedged and that no unrelated render rides along proven. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- src/receiverevidence.h | 31 +++--------------------------- test/receiverevidencecheck.sh | 9 +++++++-- test/receiverevidencefix/README.md | 4 ++-- 4 files changed, 14 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 827dc9ad8..7042e923a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. (a TS/Python/Go annotation, a Go method receiver, a JS `new Foo()`, Python `Foo()` or Go `Foo{}` initializer, an import alias of the class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields whose classes are stated (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields); - Python `feed = parser.feed`; a Ruby `include`d module. + Python `feed = parser.feed`. Ruby keeps only the language-neutral proof below (its own method lookup is separate work). - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an implicit receiver's class and bases, a free function in scope, the module a receiver alias names). With none proven, every same-file and same-directory candidate is listed — a lone global one too, because one candidate is @@ -39,7 +39,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. false. Rule 3's include-file narrow no longer decides a member call: the file a caller imports says nothing about its receiver. - Measured on the round-1 comparison-table calls for this tool: see the lane report; the gate is - `test/receiverevidencecheck.sh` (212 arms: every surface, CLI/MCP parity, census conservation, and near misses + `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses for every evidence rule). Ingest records the receiver chain (`kParserVer` 142, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 12ad18e30..b3977871c 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -10,7 +10,8 @@ // Go method receiver, a JS `new Foo()` / Python `Foo()` / Go `Foo{}` initializer), a constructed receiver // (`new Foo().m()`), a class-name receiver (`Foo.m()`), and a chain of fields whose classes are stated // (`this.bucket = new Schemas()`, Python `self.x = Foo()`, a Go struct field, a Go EMBEDDED field whose methods -// are promoted). A hit is the call's whole answer, exactly like Rule 2. +// are promoted). A hit is the call's whole answer, exactly like Rule 2. Ruby's own method lookup (mixins, typed and +// class receivers) is deliberately NOT modelled here: Ruby keeps only the language-neutral proof below. // 2. ReceiverEvidence::ladderProves — for a call the rules above did not answer, which name-ladder candidates the // language's own lookup can PROVE: an implicit receiver's own class and its bases, a free function in scope, a // definition in the module a receiver alias names. A call left with no proven candidate is NAME-ONLY: its rows @@ -86,8 +87,7 @@ struct ReceiverEvidence std::vector ownerClass; // per symbol: the class that owns it, "" none HashMap> methodsByType; // "Class::name" → its callables HashMap memberType; // "Class#field" → field class ("" = tombstone) - HashMap> embeds; // "Class" → its other bases: Go embedded classes (promotion), - // Ruby include/extend/prepend modules + HashMap> embeds; // "Class" → its other bases: Go embedded classes (promotion) HashMap nameAlias; // "#local" → the class name it imports HashMap localType; // "#var" → class ("" = tombstone) HashMap> methodAlias; // "#var" → ( object var, method ) @@ -221,34 +221,9 @@ struct ReceiverEvidence std::sort( e.begin(), e.end() ); } } - // Ruby: an include/extend/prepend directive inside a class body makes that module one more base of the class (the - // superclass directive at the open is already chaUp's). A symbolic, non-value directive is the only kind read. - void buildRubyMixins() - { - for( const Include& inc : ing.includes ) - { - if( !inc.isSymbolic || inc.isValueUse || inc.fileId >= classesByFile.size() || inc.target.empty() ) - { - continue; - } - const NodeId cls = classAt( inc.fileId, inc.byte, inc.byte + 1 ); - if( cls == kNoNode || ing.symbols[ cls ].lang != Lang::Ruby || ing.symbols[ cls ].sigStartByte == inc.byte ) - { - continue; // outside a class body, or the open's own superclass directive - } - std::string_view target = inc.target; - if( const std::size_t cut = target.rfind( "::" ); cut != std::string_view::npos ) - { - target = target.substr( cut + 2 ); - } - addBase( ing.symbols[ cls ].name, target ); - } - } - void build() { buildOwners(); - buildRubyMixins(); for( const Symbol& s : ing.symbols ) { // a BODIED callable only: an interface's or an abstract class's bodyless signature is the contract a call diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index a70075bf3..41767089b 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -83,7 +83,8 @@ # (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE # the tree (or that imports the name from outside) never proves an unrelated class's method. Near # misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), -# a free / top-level function, a Ruby included module and a Ruby top-level def. +# a free / top-level function and a Ruby top-level def; a Ruby included module is contract-only (resolved +# or hedged, never a false proven row): Ruby's own lookup is separate work. # (H) propagation and parity: the name-only witness `respondWith → reply.send()` is marked in --callees, # --callers, --impact (d=1), --path, --connect, --expand , --for , MCP find_symbol and # find_referencing_symbols; the false witness `respond → ctx.onerror()` is absent-or-marked on every @@ -464,7 +465,11 @@ exactproven cpp callees src/logger.cpp:line "fn render src/logger.cpp" exactproven cpp callees src/logger.cpp:close "method flush src/logger.cpp;method reset src/logger.cpp" exactproven swift callees Sources/App/Logger.swift:line "fn render Sources/App/Logger.swift" exactproven swift callees Sources/App/Logger.swift:close "fn flush Sources/App/Logger.swift;fn reset Sources/App/Logger.swift" -exactproven rb callees lib/app/logger.rb:line "method stamp lib/app/helpers.rb" +# Ruby's own method lookup (mixins, typed and class receivers) is another lane's: these two arms state only the +# contract — a mixin's method is RESOLVED or HEDGED, never dropped and never beside a false proven row — so they hold +# whichever lands first +visible rb callees lib/app/logger.rb:line "stamp lib/app/helpers.rb" +notproven rb callees lib/app/logger.rb:line "render lib/app/exporter.rb" exactproven rb callees lib/app/logger.rb:close "method flush lib/app/logger.rb;method reset lib/app/logger.rb;method top_level_note lib/app/notes.rb" echo "=== (O) graded false callees from an earlier binary (Go, TS, Python repros): red where still present, pinned clean otherwise ===" diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md index b171750f4..a183dc0f8 100644 --- a/test/receiverevidencefix/README.md +++ b/test/receiverevidencefix/README.md @@ -37,7 +37,7 @@ definition beside them, plus near-miss calls whose receiver IS proven and whose a constructed in-repo `Tool`. - `java/ kt/ cs/ cpp/ swift/ rb/` — a bare call inside a class whose base is outside the tree (or whose name comes from an outside import) beside an unrelated class's method of that name. Kept: own members (private - too), an in-repo superclass's member (and Java `super.flush()`), a free / top-level function, a Ruby included module, a Ruby top-level - def. + too), an in-repo superclass's member (and Java `super.flush()`), a free / top-level function, a Ruby top-level + def; a Ruby included module's method is resolved or hedged (the contract only: Ruby's own lookup is separate work). The names are paraphrases of graded false rows; the code is minimal and is never built. From 89b1cc686d9e8d47cfb9aed54b89bf79f527004d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:43:55 -0400 Subject: [PATCH 006/229] =?UTF-8?q?resolve:=20receiver-evidence=20fix=20ro?= =?UTF-8?q?und=202=20=E2=80=94=20gates=20the=20discovery=20suite=20named?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A class-name receiver proves only in languages where `Cls.m()` is a class call (JS/TS, Python, Java, Kotlin, C#, Swift) and only when no local or parameter of the caller is spelled like it (clsrecvcheck C/K/L). - A module alias proves nothing when a local hides it (pymodulealiascheck). - A name-only tier keeps a member call's included-file candidates even where reachableByName set them aside (fnliteralcheck stopCaller), and ranks the extra hedged candidates at a tenth of a share: a guess is listed, not let to move PageRank. - The via="name" legend comment is charged inside the budget walk (estchargecheck #11). - Re-pins where FE-B's resolution is the intended answer, each with its reason in the gate: kotlincheck (the qualified Kotlin -> Java helper now reaches Java's), javamethodrefcheck (the lambda's Widget.makeFn invocation), fieldnarrowcheck / chainguardcheck (typed TS fields resolve), localitycheck (8) (the pick is disclosed via="name"), scipjoincheck (a call through a parameter is a split, never a pin); pincensusfix's pin shape is an override and its base. - cachefuzzcheck's record walker reads the two new ref strings; qschemetrip re-pinned; LIMITS.md regenerated (kWalkCap classified INDEXING); README cap count. - kParserVer 145 (train 25 holds 143/144; the train renumbers). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- README.md | 2 +- docs/LIMITS.md | 18 +++++++++++---- docs/limits_classes.tsv | 1 + src/graph.h | 43 ++++++++++++++++++++++++++++++---- src/ingest_binds.h | 3 ++- src/ingest_cache.h | 6 ++--- src/quality.h | 4 ++-- src/receiverevidence.h | 36 ++++++++++++++++++++++++++++- src/serialize.h | 14 +++++++++-- test/cachefuzzcheck.sh | 2 +- test/chainguardcheck.sh | 21 +++++++++-------- test/fieldnarrowcheck.sh | 19 ++++++++------- test/javamethodrefcheck.sh | 10 ++++---- test/kotlincheck.sh | 46 +++++++++++++++++++++---------------- test/localitycheck.sh | 15 ++++++++---- test/pincensuscheck.sh | 5 ++-- test/pincensusfix/pinned.kt | 8 +++---- test/qschemetrip.hash | 2 +- test/scipjoincheck.sh | 21 +++++++++-------- 20 files changed, 194 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7042e923a..a9487ba3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. its receiver. - Measured on the round-1 comparison-table calls for this tool: see the lane report; the gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses - for every evidence rule). Ingest records the receiver chain (`kParserVer` 142, `kCacheVersion` 29), so a cache + for every evidence rule). Ingest records the receiver chain (`kParserVer` 145, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. diff --git a/README.md b/README.md index 1e1af4329..3d67671cc 100644 --- a/README.md +++ b/README.md @@ -2838,7 +2838,7 @@ 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. The tool has 236 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 237 compile-time caps and 7 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. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index c8fcf6a97..bab3a74b9 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -16,10 +16,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 236 | 94 | 128 | **108** | +| 237 | 95 | 128 | **109** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 236 + 7 is the 243 constants this generator parses out of `src/`. +are not counted as caps, and 237 + 7 is the 244 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -37,8 +37,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. **130 of 236 caps are classified -(45 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 106 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **131 of 237 caps are classified +(46 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 106 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 @@ -88,7 +88,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 94 files that declare a cap — the 236 caps counted above, and no parameter. +One table for each of the 95 files that declare a cap — the 237 caps counted above, and no parameter. ### `src/abicheck.h` @@ -787,6 +787,14 @@ Discloses: **none** | --- | --- | --- | --- | | `kDefaultRecallMaxTokens` | `8000` | — | — | +### `src/receiverevidence.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kWalkCap` | `64` | INDEXING | classes visited per base walk; deterministic (sorted chaUp) | + ### `src/redact.h` Discloses: **none** diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index 60d605bdb..49e3af866 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -142,4 +142,5 @@ kType3MaxTokensForLcs INDEXING kUnitComplexityLowRiskMax BOUNDARY kUnitInterfacingLowRiskMax BOUNDARY kUnitSizeLowRiskMax BOUNDARY +kWalkCap INDEXING kWhereisHits OUTPUT diff --git a/src/graph.h b/src/graph.h index f7e6be8da..423eb4408 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4070,6 +4070,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null ReceiverEvidence recvEv( ing, chaUp, classNames ); { PROFILE_SCOPE_DESCRIBE( "buildGraph/2i: receiver-evidence tables" ); + recvEv.localNames = &fieldNarrow.localNameSet; recvEv.build(); } HashMap nameOnlyEdges; // (from<<32|to) of an edge some call bound by NAME ALONE (no receiver evidence) @@ -4112,6 +4113,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); return !callerCls.empty() && !owner.empty() && owner != callerCls && recvEv.inCone( callerCls, owner ); } + if( recvEv.isLocalName( ref.fromSymbol, ch.root ) || externalVeto.hasLocal( ref, ch.root ) ) + { + return false; // a parameter or local spelled like the module alias hides it (`alias = make(); alias.run()`) + } if( recvEv.moduleNames( ref, ch.root, c ) ) { return true; @@ -4124,8 +4129,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null return true; } } - return !owner.empty() && classNames.contains( std::string( ch.root ) ) && recvEv.typedLocal( ref.fromSymbol, ch.root ) == nullptr - && recvEv.inCone( ch.root, owner ); // a class-name receiver: the class or a base (`Cls.create()`, `Base.m( self )`) + const std::string_view cls = recvEv.unalias( ref.fileId, ch.root ); + return !owner.empty() && recvEv.namesClass( ref, ch.root, cls ) && recvEv.inCone( cls, owner ); // a class-name receiver: the class or a base } if( implicitReceiverLang( ref.lang ) ) { @@ -4814,6 +4819,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // member call's included-file ones) instead of one locality pick, each edge marked via="name" — D1 holds a single // candidate to the same rule, because one candidate is not evidence. bool nameOnly = false; + bool nameOnlyAnyFile = false, nameOnlyAnyDir = false; // which rung the classic ladder would have stopped at (rank weight) if( hedgeEligible && !narrowed && !cand.empty() && r.lang != Lang::Elixir ) { provedScratch.clear(); @@ -4930,7 +4936,16 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null anyDir = anyDir || dir; } } - tierConf = anyFile ? 1.0f : anyDir ? 0.5f : 0.2f; + for( NodeId c : memberRule3 ) // the included file's candidates, even where reachableByName set them aside + { + if( std::find( tier.begin(), tier.end(), c ) == tier.end() ) + { + tier.push_back( c ); + } + } + tierConf = anyFile ? 1.0f : anyDir ? 0.5f : 0.2f; + nameOnlyAnyFile = anyFile; + nameOnlyAnyDir = anyDir; if( tier.empty() ) { if( cand.size() == 1 && !localsYield ) @@ -5335,7 +5350,25 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } } } - const float base = conf / float( nReal ); // split over real (non-self) targets + // FE-B: a name-only tier LISTS every candidate in reach, but RANK keeps the classic ladder's pick — the rung it would + // have stopped at (same file, else same directory) shares the call's weight as before, and the extra hedged + // candidates ride at a tenth of a share: a guess is listed, not let to move PageRank. + const std::uint32_t rdirE = fileDir[ r.fileId ]; + const auto classic = [ & ]( NodeId to ) -> bool + { + if( !nameOnly || ( !nameOnlyAnyFile && !nameOnlyAnyDir ) ) + { + return true; + } + return nameOnlyAnyFile ? ing.symbols[ to ].fileId == r.fileId : fileDir[ ing.symbols[ to ].fileId ] == rdirE; + }; + std::size_t nClassic = 0; + for( NodeId c : tier ) + { + nClassic += ( c != r.fromSymbol && classic( c ) ) ? 1u : 0u; + } + const bool allClassic = nClassic == 0; // the classic rung held only the caller: every target shares the weight + const float base = conf / float( allClassic ? nReal : nClassic ); // split over real (non-self) targets // a qualified written type decided this site by its last name — Rule 2 or 2b narrowed on it, or CHA-lite pruned by it — so every edge it // commits is prov="final-segment" (resolve.h finalSegmentTypeAt, fieldFinalSegmentAt); never a class-identity CLAIM, whose one class was verified const bool finalSegmentType = ( ( receiverTypeNarrowed || censusCone ) && !identityClaim && narrower.finalSegmentTypeAt( r ) ) @@ -5348,7 +5381,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } const std::uint64_t ekey = ( std::uint64_t( r.fromSymbol ) << 32 ) | to; EdgeAcc& e = acc[ ekey ]; - e.confSum += base; + e.confSum += ( allClassic || classic( to ) ) ? base : base * 0.1f; e.nref += 1; if( splitPick ) { diff --git a/src/ingest_binds.h b/src/ingest_binds.h index d4dbb3766..73cb7d0ed 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -2612,7 +2612,8 @@ inline std::string_view annotatedClass( TSNode node, std::string_view src ) inline void pushEvidenceBind( BindCtx& cx, LocalBindKind kind, std::string_view var, std::string_view type, std::string_view imported, std::uint32_t at, bool flag ) { - EXPECTS( isReceiverEvidenceKind( kind ), "only the receiver-evidence kinds ride this emitter" ); + EXPECTS( kind == LocalBindKind::RecvType || kind == LocalBindKind::MemberType || kind == LocalBindKind::MethodAlias || kind == LocalBindKind::NameAlias, + "only the receiver-evidence kinds ride this emitter" ); if( type.empty() || ( var.empty() && !( kind == LocalBindKind::RecvType && flag ) ) ) { return; // a nameless record is kept only for a method's own class (Go `func (T) m()`, JS `T.prototype.m = …`) diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 450879a8e..ce039709b 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -308,11 +308,11 @@ constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiver // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 142; // bump on any grammar/.scm/extraction change - // 142 = lane FE-B (receiver evidence): member calls record memberPath/memberCtor and +constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change + // 145 = lane FE-B (receiver evidence): member calls record memberPath/memberCtor and // Java/Kotlin/C#/Swift member calls are memberCall; typed parameters/locals, field types // method aliases and aliased class imports are RecvType/MemberType/MethodAlias/NameAlias - // bindings. Above train 25's 141; + // bindings. Above train 25's 143/144 (rich); // the train renumbers. // 141 = 2026-10-04 (train 25): cache-key hygiene above every branch build's number. // Two merged lanes changed extraction under their own numbers: FE-A 134/135 diff --git a/src/quality.h b/src/quality.h index 077f3a3a9..a30dee4aa 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,8 +2180,8 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 142; // MUST equal ingest.cpp's kParserVer (gated) - // 142 = lane FE-B (receiver evidence, see kParserVer note; the train renumbers) +constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) + // 145 = lane FE-B (receiver evidence, see kParserVer note; above train 25's 143/144; the train renumbers) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index b3977871c..6f9e7f26d 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -61,6 +61,15 @@ inline bool memberNeverReachesFreeFunction( Lang l ) noexcept || l == Lang::CSharp || l == Lang::Swift; } +// the languages where `Cls.m()` is a call on the CLASS (a static or an unbound method): C++ spells that `Cls::m()` (a +// qualifier, the canonical tier's), so a C++/C/Rust receiver spelled like a class is a variable or a member; Ruby's class +// receivers are its own lookup's question +inline bool classNameReceiverLang( Lang l ) noexcept +{ + return l == Lang::JavaScript || l == Lang::TypeScript || l == Lang::Python || l == Lang::Java || l == Lang::Kotlin + || l == Lang::CSharp || l == Lang::Swift; +} + // a call written ON a receiver — every language's member shape: Python/C++/Ruby record a RecvKind, the rest memberCall inline bool isMemberCallRef( const Reference& r ) noexcept { @@ -98,6 +107,7 @@ struct ReceiverEvidence mutable std::vector frontier; mutable rw::SmallVec found; bool active = false; + const HashMap* localNames = nullptr; // graph.h FieldNarrowTables::localNameSet ReceiverEvidence( const IngestResult& i, const HashMap>& up, const HashMap& classes ) : ing( i ), chaUp( up ), classNames( classes ) {} @@ -350,6 +360,30 @@ struct ReceiverEvidence } return nullptr; } + // is `name` a parameter or local of `from` or of a function enclosing it (any binding the extractor recorded) + bool isLocalName( NodeId from, std::string_view name ) const + { + if( localNames == nullptr ) + { + return false; + } + NodeId cur = from; + for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + { + if( localNames->contains( keyOf( cur, '#', name ) ) || localType.contains( keyOf( cur, '#', name ) ) ) + { + return true; + } + cur = enclosingFn( cur ); + } + return false; + } + // a receiver root that names a CLASS at this site: a class-name receiver language, a class of that name, and no local + // of the caller hiding it (`Interval = make(); Interval.validate( v )`) + bool namesClass( const Reference& r, std::string_view root, std::string_view cls ) const + { + return classNameReceiverLang( r.lang ) && classNames.contains( std::string( cls ) ) && !isLocalName( r.fromSymbol, root ); + } // the class owning the caller — the caller's own owner, else the nearest enclosing function's (a closure in a method) std::string_view callerClass( NodeId from ) const { @@ -530,7 +564,7 @@ struct ReceiverEvidence return unalias( r.fileId, *t ); } const std::string_view cls = unalias( r.fileId, root ); - if( classNames.contains( std::string( cls ) ) && !localType.contains( keyOf( r.fromSymbol, '#', root ) ) ) + if( namesClass( r, root, cls ) ) { isStatic = true; return cls; diff --git a/src/serialize.h b/src/serialize.h index c415d1b3f..9341b6cee 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5566,6 +5566,9 @@ struct CalleeCallsSink // FE-B: Graph::outNameOnly (parallel to outTargets), so a callee row whose edge is name-only carries via="name" — the // same bit --callees prints. nullptr / empty ⇒ no row is marked. const std::vector* nameOnly = nullptr; + // FE-B: set once the block's caller has been charged the via="name" legend comment it will write (packBodies/packHops + // write it ahead of the rows, after the walk) — so the budget the walk honours already holds those bytes + bool* viaLegendCharged = nullptr; }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` @@ -5802,6 +5805,11 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } const Symbol& cs = ing.symbols[cid]; const bool via = viaOf( cid ); + if( via && sink.viaLegendCharged != nullptr && !*sink.viaLegendCharged ) + { + used += viaNameLegendComment().size(); // the legend this row pulls in, charged before the row + *sink.viaLegendCharged = true; + } // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) @@ -6203,6 +6211,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // --expand; nullptr (--around/--exemplar) ⇒ node-id order. // See CalleeCallsSink::rank. { + bool viaLegendCharged = false; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) // budgetBytes == 0 ⇒ UNLIMITED (A3-F2): the MCP `exemplar` verb has no byte budget, and 0 must never // mean "cap at zero bytes" (the cap fired before the first body and emitted a bare ). // Matches buildRecall's "0 = no cap" convention; the CLI always passes a real budget (default 64 KB). @@ -6499,7 +6508,7 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // total=/shown=/capped= block — see emitCalleeCallsBlock above; `calleeRank` decides which // rows survive when it CUTS one, which is far from rare here (CalleeCallsSink::rank). emitCalleeCallsBlock( piece, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly } ); + CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, &viaLegendCharged } ); const std::string bodyNotes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 piece += bodyNotes; used += bodyNotes.size(); // W3-N2: same charge-never-trim rule @@ -6613,6 +6622,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing std::string_view rootArg = {} ) // R-E: same single-root-only root= every verb takes { + bool viaLegendCharged = false; // FE-B: the via="name" legend has been charged against the budget (CalleeCallsSink) if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention { budgetBytes = SIZE_MAX; @@ -6697,7 +6707,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // the 1-hop callee signatures — the identical block a body carries, charged against the same // running `used` so the row identity bytes and the edge bytes share one budget. emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly } ); + CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged } ); row += ""; children += row; ++shownCount; diff --git a/test/cachefuzzcheck.sh b/test/cachefuzzcheck.sh index a7a959db4..31c0e13e5 100755 --- a/test/cachefuzzcheck.sh +++ b/test/cachefuzzcheck.sh @@ -931,7 +931,7 @@ for i in range(n): site32("def.ppAlt", 9 * 4); site32("def.params", 13 * 4) skip(14 * 4 + 6 + 2 * 4); site("def.kind"); site("def.lang"); s(); s(); skip(8) # #150: +1 u8 (scopeRootsStd); cache v26: +2 u32 (fnScopeStart/End) before kind for _ in range(u32()): # refs - skip(4); site("ref.lang"); s(); skip(2); s(); site("ref.recv"); s(); skip(1); s(); s(); site("ref.role"); site32("ref.argCount", 4); skip(12); s() # line, argCount, argCountKnown, viaArrow, #150's qualifierRootsStd, FE-A's memberCall u8 + memberRoot str + skip(4); site("ref.lang"); s(); skip(2); s(); site("ref.recv"); s(); skip(1); s(); s(); site("ref.role"); site32("ref.argCount", 4); skip(12); s(); s(); s() # line, argCount, argCountKnown, viaArrow, #150's qualifierRootsStd, FE-A's memberCall u8 + memberRoot str, FE-B's memberPath + memberCtor strs for _ in range(u32()): # includes skip(3 + 4 + 1); s() for _ in range(u32()): # binds diff --git a/test/chainguardcheck.sh b/test/chainguardcheck.sh index dbaa209d1..c6c157bf8 100755 --- a/test/chainguardcheck.sh +++ b/test/chainguardcheck.sh @@ -211,17 +211,20 @@ GO2="$( "$BIN" "$FIX2" --callees=go --no-cache 2>/dev/null | grep -o '"$TMP/m1" 2>/dev/null diff --git a/test/fieldnarrowcheck.sh b/test/fieldnarrowcheck.sh index 8a5ef8cf7..60d53551a 100755 --- a/test/fieldnarrowcheck.sh +++ b/test/fieldnarrowcheck.sh @@ -210,18 +210,21 @@ PY="$( callees po_go )" && ok "(e-py) po_go() self.member.compute() stays honestly split (annotated attr NOT narrowed — disclosed limit)" \ || no "(e-py) po_go() lost its honest split — Python receiver behavior must be unchanged this round" TS="$( callees to_go )" -( printf '%s\n' "$TS" | grep -q 't.ts:1"' ) && ( printf '%s\n' "$TS" | grep -q 't.ts:2"' ) \ - && ok "(e-ts) to_go() this.member.compute() stays honestly split (TS receivers uncaptured — disclosed limit)" \ - || no "(e-ts) to_go() lost its honest split — TS receiver behavior must be unchanged this round" +# FE-B (test/receiverevidencecheck.sh) reads TS field annotations: `member: THelper;` types `this.member`, so to_go() +# RESOLVES to THelper.compute and TDecoy.compute is no candidate (it was the honest split while TS receivers were +# uncaptured). An UNtyped TS chain stays name-only (receiverevidencecheck's `ctx.req.raw.headers.get()` arm). +( printf '%s\n' "$TS" | grep -q 't.ts:1"' ) && ! ( printf '%s\n' "$TS" | grep -q 't.ts:2"\|via="name"' ) \ + && ok "(e-ts) to_go() this.member.compute() resolves to THelper.compute alone (the annotated field types it — FE-B)" \ + || no "(e-ts) to_go() should resolve to THelper.compute alone, plain: $( printf '%s' "$TS" | tr '\n' ' ' )" -# ── (h) the header gauge agrees with the arms above: exactly the 6 honest splits remain ambiguous -# (expl, unk, freeuse, multi, po_go, to_go — run/ptr/inh_go narrowed, shadowLocal and shadowParam are +# ── (h) the header gauge agrees with the arms above: exactly the 5 honest splits remain ambiguous +# (expl, unk, freeuse, multi, po_go — to_go resolves since FE-B; run/ptr/inh_go narrowed, shadowLocal and shadowParam are # Rule 2; shadowParam was a split until Rule 2 read parameter types, 2026-09-16, which moved this from 7). # Counted from the fixture, not guessed: flip arms above before touching this number. ── AMB="$( printf '%s\n' "$MAP" | grep -o 'ambiguous=[0-9]*' | head -1 )" -[ "$AMB" = "ambiguous=6" ] \ - && ok "(h) header gauge ambiguous=6 — only the honest splits remain" \ - || no "(h) header gauge is '$AMB', expected ambiguous=6 (3 field-typed calls narrowed, 6 honest splits kept)" +[ "$AMB" = "ambiguous=5" ] \ + && ok "(h) header gauge ambiguous=5 — only the honest splits remain" \ + || no "(h) header gauge is '$AMB', expected ambiguous=5 (3 field-typed calls narrowed, to_go resolved, 5 honest splits kept)" # ── (n) same-NAMED class collision (FIX2): conflicting same-named fields tombstone — NEITHER Dup::go narrows ── MAP2="$( "$BIN" "$FIX2" --no-cache --legend=full 2>/dev/null | tr '>' '\n' )" diff --git a/test/javamethodrefcheck.sh b/test/javamethodrefcheck.sh index 396d67f32..4da9e6997 100755 --- a/test/javamethodrefcheck.sh +++ b/test/javamethodrefcheck.sh @@ -216,7 +216,9 @@ expect_callers makeFn "genericTypeMethod lambdaForm nestedTypeMethod typeMethod" # set above is satisfied by an edge pointing anywhere — and before the receiver-resolution fix every # one of these references landed on the caller's own file's A.makeFn, via the bare-name ladder and # the locality tie-break. Each reference must resolve to the member of the type it named. -expect_callers Widget.java:makeFn "genericTypeMethod typeMethod" +# FE-B (test/receiverevidencecheck.sh): the lambda's INVOCATION `Widget.makeFn( item )` is a class-name receiver, so it +# resolves to Widget's member too — it used to land on A.makeFn by the same file-locality pin the references had. +expect_callers Widget.java:makeFn "genericTypeMethod lambdaForm typeMethod" expect_callers Outer.java:makeFn "nestedTypeMethod" # The r9 shadow pass is C++/ObjC evidence. A Java call never resolves to a local, so a parameter @@ -294,9 +296,9 @@ echo "callers_set(makeFn) after mutation = [${S1}]" # names no definition anywhere, so it produces no edge rather than one to a plausible neighbour. S2="$( callers_set "$MUT" Widget.java:makeFn )" echo "callers_set(Widget.java:makeFn) after mutation = [${S2}]" -[ "$S2" = "genericTypeMethod" ] \ - && ok "mutation leaves Widget.java:makeFn with only genericTypeMethod" \ - || no "mutation expected Widget.java:makeFn = [genericTypeMethod], got [$S2]" +[ "$S2" = "genericTypeMethod lambdaForm" ] \ + && ok "mutation leaves Widget.java:makeFn with genericTypeMethod and the untouched lambda invocation (FE-B: a class-name receiver)" \ + || no "mutation expected Widget.java:makeFn = [genericTypeMethod lambdaForm], got [$S2]" S3="$( callers_set "$MUT" Outer.java:makeFn )" [ "$S3" = "nestedTypeMethod" ] \ && ok "mutation leaves Outer.java:makeFn untouched (nestedTypeMethod)" \ diff --git a/test/kotlincheck.sh b/test/kotlincheck.sh index 459914ebf..4297bbe68 100755 --- a/test/kotlincheck.sh +++ b/test/kotlincheck.sh @@ -31,7 +31,7 @@ # fun greet(): String -> square(2) -- member function, CROSS-FILE call # } # fun Int.doubled(): Int -- extension function -# fun useJavaHelper(): Int -> JavaBridge.helper(5) -- Kotlin -> ???, QUALIFIED on a name both define (§5 trade-off) +# fun useJavaHelper(): Int -> JavaBridge.helper(5) -- Kotlin -> ???, QUALIFIED on a name both define (§5: Java's, by its class-name receiver, since FE-B) # fun useJavaOnly(): Int -> JavaBridge.javaOnly(3) -- Kotlin -> Java, qualified, a name only Java defines (§3) # fun ambiguousCall(): Int -> helper(5) -- Kotlin -> ???, BARE (the collision probe) # fun runAll(): Int -> of, greet, doubled, useJavaHelper, useJavaOnly, ambiguousCall @@ -53,13 +53,13 @@ # fixture and split across three directories alike (§14a). # - JVM BRIDGE, Kotlin -> Java: useJavaOnly's `JavaBridge.javaOnly(3)` reaches the Java method — a # name only Java defines, which is the case the bridge exists for (§3). -# - THE COLLISION, by rule: `helper` is defined in both languages, so both Kotlin sites — the BARE -# ambiguousCall() and the QUALIFIED useJavaHelper() — bind Kotlin's Extra.helper: the bridge admits -# the other JVM language only when the caller's own defines no candidate (graph.h -# keepOwnJvmLanguageCandidates). The qualified site is the disclosed trade-off: Kotlin receivers do -# not narrow candidates yet (ingest_binds.h). What this replaced reported both helpers at -# ambiguous=2 in this flat fixture — and silently bound NEITHER once the files sat in different -# directories, the same tier-3 drop that deleted Java edges on square/retrofit (§14). +# - THE COLLISION, by rule: `helper` is defined in both languages. The BARE ambiguousCall() binds Kotlin's Extra.helper +# (hedged via="name": nothing imports the object's member): the bridge admits the other JVM language only when the +# caller's own defines no candidate (graph.h keepOwnJvmLanguageCandidates). The QUALIFIED useJavaHelper() was the +# disclosed trade-off until FE-B (Kotlin receivers did not narrow); its class-name receiver now proves Java's +# JavaBridge.helper. What this replaced reported both helpers at ambiguous=2 in this flat fixture — and silently +# bound NEITHER once the files sat in different directories, the same tier-3 drop that deleted Java edges on +# square/retrofit (§14). # - A same-name Kotlin/Java pair of TYPES is kept as two definitions even when one side has no body # (model.h isDefinitionNotDeclaration, §11 and §13); the positional body fallback in ingest_sidecap.h is what # gives a bodied Kotlin definition its body span. @@ -262,9 +262,9 @@ echo "=== 5. THE COLLISION: a same-name Kotlin/Java pair binds the caller's OWN # ═══════════════════════════════════════════════════════════════════════════ # helper is defined in both languages (Util.kt's Extra.helper, JavaBridge.java's helper). The bridge admits the other JVM # language only when the caller's own defines no candidate of the name (graph.h keepOwnJvmLanguageCandidates), so BOTH -# Kotlin call sites — the bare ambiguousCall() and the QUALIFIED useJavaHelper()'s `JavaBridge.helper(5)` — bind Kotlin's -# Extra.helper. The qualified one is the disclosed trade-off, pinned so it cannot move silently: Kotlin receivers do not -# narrow candidates yet (ingest_binds.h), so `JavaBridge.` is evidence this resolver cannot read. What this replaced: both +# Kotlin call sites — the bare ambiguousCall() and the QUALIFIED useJavaHelper()'s `JavaBridge.helper(5)` — bound Kotlin's +# Extra.helper until FE-B: the qualified one was the disclosed trade-off (Kotlin receivers did not narrow). FE-B's class-name +# receiver closes it (see below). What this replaced: both # sites bound BOTH helpers at ambiguous=2 — but only because this fixture is one directory; split across three they # reached NEITHER, with no amb= and no unresolved= (§14a runs that split layout). USES_HELPER="$( "$BIN" "$FIX" --uses=helper --no-cache 2>/dev/null )" @@ -272,12 +272,17 @@ echo "$USES_HELPER" | grep -q 'defs="2"' && ok '--uses=helper: defs="2" — both || no "--uses=helper: expected defs=2: $( echo "$USES_HELPER" | grep -o ']*>' )" UTIL_HELPER_CALLERS="$( "$BIN" "$FIX" --callers="Util.kt:helper" --no-cache 2>/dev/null )" HELPER_CALLERS="$( "$BIN" "$FIX" --callers="JavaBridge.java:helper" --no-cache 2>/dev/null )" -echo "$UTIL_HELPER_CALLERS" | grep -q 'count="2"' && echo "$UTIL_HELPER_CALLERS" | grep -q 'n="useJavaHelper"' && echo "$UTIL_HELPER_CALLERS" | grep -q 'n="ambiguousCall"' \ - && ok "--callers=Util.kt:helper: count=2 — the bare ambiguousCall AND the qualified useJavaHelper both bind Kotlin's Extra.helper" \ - || no "--callers=Util.kt:helper: expected both Kotlin call sites: $( echo "$UTIL_HELPER_CALLERS" | grep -o ']*>' )" -echo "$HELPER_CALLERS" | grep -q 'count="0"' \ - && ok "--callers=JavaBridge.java:helper: count=0 — no Kotlin site reaches Java's helper while Kotlin defines one (the trade-off)" \ - || no "--callers=JavaBridge.java:helper: expected count=0 — a Kotlin site reached Java's helper although Kotlin defines one: $( echo "$HELPER_CALLERS" | grep -o ']*>' )" +# FE-B (test/receiverevidencecheck.sh) closed the trade-off: a Kotlin class-name receiver is evidence now, so the QUALIFIED +# `JavaBridge.helper(5)` reaches Java's JavaBridge.helper — proven, no via= — while the BARE ambiguousCall() keeps the +# own-language-first rule (it never reaches Java's helper) and binds Kotlin's Extra.helper only HEDGED: a top-level +# function's bare call reaches an object's member through an import, which this file does not have. +echo "$UTIL_HELPER_CALLERS" | grep -q 'count="1"' && echo "$UTIL_HELPER_CALLERS" | grep -q '' \ + && ! echo "$UTIL_HELPER_CALLERS" | grep -q 'n="useJavaHelper"' \ + && ok "--callers=Util.kt:helper: count=1 — the bare ambiguousCall, hedged via=\"name\"; the qualified useJavaHelper no longer lands here" \ + || no "--callers=Util.kt:helper: expected only the bare call, hedged: $( echo "$UTIL_HELPER_CALLERS" | grep -oE ']*>|]*/>' | tr '\n' ' ' )" +echo "$HELPER_CALLERS" | grep -q 'count="1"' && echo "$HELPER_CALLERS" | grep -q '' \ + && ok "--callers=JavaBridge.java:helper: count=1 — the qualified useJavaHelper, proven by its class-name receiver; the bare site never reaches Java (own language first)" \ + || no "--callers=JavaBridge.java:helper: expected exactly the qualified useJavaHelper, proven: $( echo "$HELPER_CALLERS" | grep -oE ']*>|]*/>' | tr '\n' ' ' )" # ═══════════════════════════════════════════════════════════════════════════ echo @@ -919,9 +924,10 @@ for L in flat split; do callerRows "$ROOTL" JavaBridge.java:javaOnly | grep -q 'n="useJavaOnly"' \ && ok "$L: Kotlin -> Java — useJavaOnly reaches JavaBridge.javaOnly, a name only Java defines" \ || no "$L: Kotlin -> Java — useJavaOnly does not reach JavaBridge.javaOnly" - [ "$( callersCount "$ROOTL" Util.kt:helper )" = 'count="2"' ] && [ "$( callersCount "$ROOTL" JavaBridge.java:helper )" = 'count="0"' ] \ - && ok "$L: both Kotlin helper(5) calls bind Kotlin's Extra.helper and none reaches Java's (own language first)" \ - || no "$L: helper — Util.kt:helper $( callersCount "$ROOTL" Util.kt:helper ), JavaBridge.java:helper $( callersCount "$ROOTL" JavaBridge.java:helper ); expected count=2 and count=0" + [ "$( callersCount "$ROOTL" Util.kt:helper )" = 'count="1"' ] && [ "$( callersCount "$ROOTL" JavaBridge.java:helper )" = 'count="1"' ] \ + && callerRows "$ROOTL" JavaBridge.java:helper | grep -q 'n="useJavaHelper"' \ + && ok "$L: the bare helper(5) binds Kotlin's Extra.helper (own language first) and the qualified JavaBridge.helper(5) reaches Java's (FE-B)" \ + || no "$L: helper — Util.kt:helper $( callersCount "$ROOTL" Util.kt:helper ), JavaBridge.java:helper $( callersCount "$ROOTL" JavaBridge.java:helper ); expected count=1 and count=1 (useJavaHelper)" done # 14b. Response.body beside an unrelated Kotlin body() in another directory. The Java callers must equal the same tree's diff --git a/test/localitycheck.sh b/test/localitycheck.sh index abf15765d..8525e3c11 100755 --- a/test/localitycheck.sh +++ b/test/localitycheck.sh @@ -30,8 +30,8 @@ # 6b control — a `std::unique_ptr` member, which Rule 2b reads since test/fieldnarrowcheck.sh arm p, narrows # `owned_->pick()` to Target::pick alone (receiver-rule). Arm 6 held that member until then, as the unreadable type. # 7 a TYPED receiver whose type defines no such method (`Shape& other`) splits (RED: Decoy::pick) -# 8 control — the FILE credit stays: {the caller itself, Target::peek} still pins Target::peek by locality, with -# the disclosure (census mech=locality). Skipping the tie-break for these receivers moved no target on seven +# 8 control — {the caller itself, Target::peek} answers Target::peek; since FE-B the call is name-only, so the +# disclosure is via="name" on the row (census mech=unique), no longer the locality pin. Skipping the tie-break for these receivers moved no target on seven # corpora either, but relabelled every such site `unique` and dropped its lpin= disclosure. # 9 control — a typed receiver Rule 2 narrows is untouched (receiver-rule, Target::pick alone) # Exits non-zero on any failure. @@ -163,9 +163,14 @@ expectSite "(6)" Wrapper::forward pick "pick@1 pick@15 pick@6" split expectSite "(6b)" Wrapper::forwardOwned pick "pick@1" receiver-rule # ── 7) a TYPED receiver whose type defines no `pick`: Rule 2 declines, CHA-lite's cone keeps nothing and degrades. expectSite "(7)" Decoy::typedNoMethod pick "pick@1 pick@15 pick@6" split -# ── 8) control — the FILE credit stays: the tier is {Decoy::peek itself, Target::peek}; the caller scores zero, so -# Target::peek is the locality pick and stays disclosed as one. -expectSite "(8)" Decoy::peek peek "peek@1" locality +# ── 8) control — the target stays: the tier is {Decoy::peek itself, Target::peek}; the caller is never its own answer, so +# Target::peek is the one row. Since FE-B (test/receiverevidencecheck.sh) an untyped receiver's call is NAME-ONLY and +# never reaches the tie-break, so the disclosure moved from the census's `locality` pin to the row itself: via="name" +# (the census labels a lone name-only target `unique`, as it labels a lone global). +expectSite "(8)" Decoy::peek peek "peek@1" unique +"$BIN" "$RFIX" --callees=Decoy::peek --no-cache 2>/dev/null | grep -q '' \ + && ok "(8) Decoy::peek(): the Target::peek row says via=\"name\" — the pick is disclosed on the row" \ + || no "(8) Decoy::peek(): the Target::peek row is not marked via=\"name\"" # ── 9) control — Rule 2 narrows a typed local before the tie-break: one precise edge, untouched. expectSite "(9)" Decoy::typedLocal pick "pick@1" receiver-rule diff --git a/test/pincensuscheck.sh b/test/pincensuscheck.sh index 206391d61..08ee58637 100755 --- a/test/pincensuscheck.sh +++ b/test/pincensuscheck.sh @@ -20,8 +20,9 @@ # # THE FIXTURE (test/pincensusfix/, 2 Kotlin files, ~40 lines; Python until FE-A, whose bare call reaches no method — # test/falseedgecheck.sh) reproduces both shapes in the smallest form: -# pinned.kt — `Alpha.run` bare-calls `helper()`; `Alpha.helper` and `Beta.helper` both live in this -# file, so tier 1 keeps BOTH and S6-C decides: `pinned.kt::Alpha::` beats `pinned.kt::`. +# pinned.kt — `Alpha.run` bare-calls `helper()`; `Alpha.helper` and its BASE `Beta.helper` both live in this +# file (Beta became Alpha's base with FE-B, whose implicit receiver proves only the class and its +# bases), so tier 1 keeps BOTH and S6-C decides: `pinned.kt::Alpha::` beats `pinned.kt::Beta::`. # ONE edge, NO `amb=` — the silent pin. # tied.kt — `Eps.go` bare-calls `other()`; `Gamma.other` and `Delta.other` are SIBLINGS, so both # share exactly `tied.kt::` and NEITHER is more local. The tier stays full, the call diff --git a/test/pincensusfix/pinned.kt b/test/pincensusfix/pinned.kt index f04df9459..14f001b52 100644 --- a/test/pincensusfix/pinned.kt +++ b/test/pincensusfix/pinned.kt @@ -7,16 +7,16 @@ // Alpha.helper is pinned (the right answer: `this.helper()`), ONE confident edge is emitted, and `amb=` is NOT // incremented. That silent commitment is what the census exists to make visible. // -class Alpha { - fun helper(): Int { +class Alpha : Beta() { + override fun helper(): Int { return 1 } fun run(): Int { return helper() } } -class Beta { - fun helper(): Int { +open class Beta { + open fun helper(): Int { return 2 } } diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 1950ed0e5..dc466e816 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -d1a2878ce09f5e2ed41e4e0b223abb5e3bac34d560210a513f05da12bf44e6c8 +1d723069b171c4c05a635896aa97b8d2002e0c413ad6fa3a61a13e1b7a695877 diff --git a/test/scipjoincheck.sh b/test/scipjoincheck.sh index 5001f9a65..c6431945a 100755 --- a/test/scipjoincheck.sh +++ b/test/scipjoincheck.sh @@ -19,7 +19,9 @@ # THE FIXTURE (test/scipjoinfix/: a.py, b.py, index.scip from make_index.py — line numbers there): # * local trap `local 0` def on a.py:2 (sum's line) + `local 0` ref on b.py:6 (inside K.run) # * parameter trap `Box#go().(model)` def on a.py:9 (go's own line); `model(1)` on a.py:10 resolves -# to that PARAMETER in SCIP, while ripwire's tier {Box.model, model} is S6-C-pinned +# to that PARAMETER in SCIP, while ripwire's tier {Box.model, model} was S6-C-pinned until FE-B +# (test/receiverevidencecheck.sh): a call THROUGH a parameter is name-only now, so the tier is +# kept whole — a disclosed split, both rows via="name" — and the join reads that split # * external `sum(vals)` on a.py:8 resolves to `builtins/sum().` in SCIP; ripwire -> a.py::sum # * control `helper()` on b.py:6 resolves to b.py::helper in both # @@ -43,9 +45,9 @@ echo "scipjoincheck: BIN=$BIN CORPUS=$CORPUS" # ── (A) the fixture reproduces the shapes: a locality pin and two unique resolutions ────────────── "$BIN" "$CORPUS" $EXC --no-cache --pin-census="$TMP/plain.tsv" >"$TMP/plain.xml" 2>"$TMP/plain.err" || no "(A) plain run failed" -awk -F'\t' '$1=="C" && $2=="locality" && $6 ~ /^a\.py::Box::go#/ && $7=="model" && $8 ~ /^a\.py::Box::model#/' "$TMP/plain.tsv" | grep -q . \ - && ok "(A) Box.go -> model is S6-C locality-pinned to Box.model (the shape under audit)" \ - || { no "(A) Box.go -> model is not a locality pin to Box.model"; grep '^C' "$TMP/plain.tsv" | sed 's/^/ /'; } +awk -F'\t' '$1=="C" && $2=="split" && $6 ~ /^a\.py::Box::go#/ && $7=="model" && $8 ~ /a\.py::Box::model#/ && $8 ~ /\|a\.py::model#/' "$TMP/plain.tsv" | grep -q . \ + && ok "(A) Box.go -> model is the whole name-only tier {Box.model, model} (the shape under audit; FE-B: never a pin through a parameter)" \ + || { no "(A) Box.go -> model is not the split over Box.model and model"; grep '^C' "$TMP/plain.tsv" | sed 's/^/ /'; } awk -F'\t' '$1=="C" && $2=="unique" && $6 ~ /^a\.py::Box::total#/ && $7=="sum"' "$TMP/plain.tsv" | grep -q . \ && ok "(A) Box.total -> sum resolves uniquely to a.py::sum" || no "(A) Box.total -> sum is not a unique resolution" awk -F'\t' '$1=="C" && $6 ~ /^b\.py::K::run#/ && $7=="helper"' "$TMP/plain.tsv" | grep -q . \ @@ -94,13 +96,14 @@ printf '%s' "$LINE" | grep -q '(1/1)' && ok "(F) stderr: SCIP matched 1/1 intern # ── (G) the harness reads both definitions ──────────────────────────────────────────────────────── python3 "$ROOT/bench/scip_pin_precision.py" --bin "$BIN" --repo "$CORPUS" --scip "$IDX" --exclude make_index.py \ --workdir "$TMP" --label join --json "$TMP/join.json" >"$TMP/harness.out" 2>&1 || no "(G) harness failed: $( tail -3 "$TMP/harness.out" )" -if python3 - "$TMP/join.json" <<'PY'; then ok "(G) harness: locality covered=1 precision=0.000 (full oracle), in-repo-only covered=0"; else no "(G) harness readout wrong: $( cat "$TMP/harness.out" | tail -12 )"; fi +if python3 - "$TMP/join.json" <<'PY'; then ok "(G) harness: the parameter site reads as split covered=1 precision=0.000 (full oracle), in-repo-only covered=0; locality covers none"; else no "(G) harness readout wrong: $( cat "$TMP/harness.out" | tail -12 )"; fi import json, sys j = json.load( open( sys.argv[ 1 ] ) ) -loc = j[ "rows" ][ "locality" ] -assert loc[ "covered" ] == 1 and loc[ "confirmed" ] == 0, loc -assert loc[ "covered_inrepo" ] == 0, loc -assert loc[ "sentinel_nondef" ] == 1 and loc[ "sentinel_external" ] == 0, loc +spl = j[ "rows" ][ "split" ] +assert spl[ "covered" ] == 1 and spl[ "confirmed" ] == 0, spl +assert spl[ "covered_inrepo" ] == 0, spl +assert spl[ "sentinel_nondef" ] == 1 and spl[ "sentinel_external" ] == 0, spl +assert j[ "rows" ].get( "locality", {} ).get( "covered", 0 ) == 0, j[ "rows" ].get( "locality" ) uni = j[ "rows" ][ "unique" ] assert uni[ "sentinel_external" ] == 1, uni PY From a4b2c12d2233822b8c4361c73d6e642448d84283 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:44:23 -0400 Subject: [PATCH 007/229] resolve: a language's own method lookup can prove a name-ladder survivor languageLookupProved is the seam PR #373's Ruby lookup sets where it cuts the candidates to what self can reach: its survivors are proven by that lookup and never marked via="name". Nothing sets it on this branch; the #373 integration is two assignments at its cut sites. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graph.h b/src/graph.h index 423eb4408..76952beeb 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4682,6 +4682,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // a QUALIFIED call (`A::m()`, `Storage::reset()`) names its scope: that is the canonical tier's evidence axis, // not a receiver's, so a qualifier the canonical tier could not place keeps the ladder exactly as before const bool hedgeEligible = !scipPinned && !canonical && receiverHedgeLang( r.lang ) && r.role == RefRole::Call && r.qualifier.empty(); + // A language's OWN method lookup that cut the candidates to what the receiver can reach (Ruby's RubySelfReach, PR #373) + // sets this: its survivors are proven by that lookup, never name-only. No language sets it on this branch yet. + bool languageLookupProved = false; memberRule3.clear(); if( !scipPinned && !canonical && !narrowed && it != byName.end() ) { @@ -4820,7 +4823,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // candidate to the same rule, because one candidate is not evidence. bool nameOnly = false; bool nameOnlyAnyFile = false, nameOnlyAnyDir = false; // which rung the classic ladder would have stopped at (rank weight) - if( hedgeEligible && !narrowed && !cand.empty() && r.lang != Lang::Elixir ) + if( hedgeEligible && !narrowed && !languageLookupProved && !cand.empty() && r.lang != Lang::Elixir ) { provedScratch.clear(); if( !memberRule3.empty() && !isMemberCallRef( r ) ) From 7942b3735867fccbf970a84907f029a5e6cc5d7c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:56:30 -0400 Subject: [PATCH 008/229] value references: prototype parameters, self references, per-site callees rows, the depth cut Fixes to the reference-as-value rows, from CodeRabbit's second pass on #372. - A parameter declares its name only inside the function that owns the list. A C prototype, a function-pointer typedef or parameter, a C++ member declaration, a Go func type or interface method and a TS function type, `declare function` or interface signature open no scope, and their parameter names used to land in the enclosing scope. At file scope they hid the function in the whole file: `signal( SIGINT, handler )` lost its row beside `typedef void (*fn_t)( int handler )`. - A value reference made inside the function itself (`timer_set( tick )` in tick) no longer keeps it off --dead-code, as the call graph drops a recursive self-call. --safe-delete's dead_code_candidate= reads the same rule; the row is still served. - --callees value rows are per binding site: two calls through one slot are one row, and sites= counts distinct binding sites, not calls. - The value walk's depth cap (kVrMaxDepth, 512) is disclosed. A file whose walk stopped there gets one depth-cut record (cached with its references), and every answer that reads value references for that language family carries value_refs_depth_capped=N and value_refs_depth_at=FILE:LINE (the first cut): the callers/callees roots, --impact, --safe-delete, --dead-code, --json and the MCP twins. A 300-link else-if chain is enough to reach it. - The remaining written key slots (a C `[expr]` designator, a numeric key, an identifier key) go through vrClean like every other slot. - The --callees sort ends on the definition id (a total order). Gate: test/recallshapecheck.sh arms RP, RS, RD, RZ and RK. LIMITS: kVrMaxDepth is INDEXING and its file now discloses value_refs_depth_capped. Co-Authored-By: Claude Opus 5.5 --- docs/LIMITS.md | 12 +-- docs/limits_classes.tsv | 1 + src/compactlegend.h | 5 +- src/graphlegend.h | 7 +- src/ingest_valuerefs.h | 159 +++++++++++++++++++++++++++++--- src/mcpverbs.h | 11 ++- src/model.h | 6 ++ src/valuerefindex.h | 92 ++++++++++++++++--- src/valuerefs.h | 44 ++++++++- src/verbs_navigate.h | 31 ++++--- src/verbs_quality.h | 17 +++- test/recallshapecheck.sh | 191 +++++++++++++++++++++++++++++++++++++++ 12 files changed, 523 insertions(+), 53 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index fbeb13a73..4bd789a33 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,7 +17,7 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 236 | 94 | 135 | **101** | +| 236 | 94 | 137 | **99** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they are not counted as caps, and 236 + 7 is the 243 constants this generator parses out of `src/`. @@ -38,8 +38,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. **130 of 236 caps are classified -(45 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 106 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **131 of 236 caps are classified +(46 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 105 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 @@ -505,11 +505,11 @@ Discloses: **none** ### `src/ingest_valuerefs.h` -Discloses: **none** +Discloses: `value_refs_depth_capped` | constant | value | class | note | | --- | --- | --- | --- | -| `kVrMaxDepth` | `512` | — | the value-uses pass's own depth guard (ingest_sidecap.h kSideDepthUses) | +| `kVrMaxDepth` | `512` | INDEXING | the value-uses pass's own depth guard (ingest_sidecap.h kSideDepthUses); a cut is disclosed as value_refs_depth_capped= (valuerefs.h) | | `kVrTextCap` | `96` | — | a written slot / callee longer than this is cut with "…" | ### `src/jsrunner.h` @@ -938,7 +938,7 @@ Discloses: `name_ladder_capped` ### `src/valuerefs.h` -Discloses: `capped` +Discloses: `capped`, `value_refs_depth_capped` | constant | value | class | note | | --- | --- | --- | --- | diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index 60d605bdb..b5a6c6e87 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -142,4 +142,5 @@ kType3MaxTokensForLcs INDEXING kUnitComplexityLowRiskMax BOUNDARY kUnitInterfacingLowRiskMax BOUNDARY kUnitSizeLowRiskMax BOUNDARY +kVrMaxDepth INDEXING kWhereisHits OUTPUT diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..45e27512a 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -337,7 +337,10 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "to", "vr to= def=: the function used as a value and its definition; sites=N binding sites one to=/through= pair joins", false, "vr" }, { "through", "through=: the written callee this function may call it through", false, "vr" }, { "to_value_refs", "to_value_refs=N: to= is used as a value N times (matched by name); a run through such a slot is not a proven call and no hop here" }, - { "value-ref-excluded", "value-ref-excluded=N: internal functions kept off because a table, field or argument holds them (matched by name, not a proven call); a floor" }, + { "value-ref-excluded", "value-ref-excluded=N: internal functions kept off because another function's table, field or argument holds them (matched by name, not a proven call); a floor" }, + // The value walk's depth cut (ingest_valuerefs.h kVrMaxDepth, graphlegend.h kValueRefsDepthLegend): absent when none. + { "value_refs_depth_capped", "value_refs_depth_capped=N: N files nest past the value walk's 512-level cap; a value use below it is unseen, so value_refs= and every reading on it are floors" }, + { "value_refs_depth_at", "value_refs_depth_at=: the first such cut, file:line; read the code there" }, { "role", "u role=value: the function is used as a VALUE there (stored or passed), matched by name; not a proven call", false, "u", MapHeaderRead::No, "value" }, // #220 part 1: the FILE graph's gauge (graphlegend.h importsUnresolvedAttrXml), absent at zero, on the --deps/--arch/ // --impact roots and the MCP impact twin. What it means for the numbers is the reading BESIDE it, never this row: diff --git a/src/graphlegend.h b/src/graphlegend.h index b35e988e2..d73deab88 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -618,11 +618,16 @@ inline constexpr const char* kValueRefsCalleesLegend = inline constexpr const char* kValueRefsReachLegend = "value_refs=N (absent when 0) counts rows: SYM is USED AS A VALUE at bind= (stored into into= or passed), matched by name. It is not a proven call: reaches= and impact_reaches= exclude them, and a caller that runs it through that slot is not in the radius. is their window; : the enclosing symbol, the binding site, where it lands, functions that may call through it. "; inline constexpr const char* kValueRefsSafeDeleteLegend = - "value_refs=N (absent when 0): SYM is USED AS A VALUE N times (a table, field or argument holds it; matched by name, not a proven call). Each such site (a decorator row aside: a fact about the definition) is in uses=; any row keeps dead_code_candidate at 0 and risk off none-found: deleting SYM breaks the table even though no call reaches it. / list them. "; + "value_refs=N (absent when 0): SYM is USED AS A VALUE N times (a table, field or argument holds it; matched by name, not a proven call). Each such site (a decorator row aside: a fact about the definition) is in uses=; any row keeps risk off none-found, and any row made outside SYM itself keeps dead_code_candidate at 0: deleting SYM breaks the table even though no call reaches it (a function that only stores ITSELF, timer_set( tick ) inside tick, can still be a candidate, as a recursive self-call is no caller). / list them. "; inline constexpr const char* kToValueRefsLegend = "to_value_refs=N (absent when 0): to= is USED AS A VALUE N times (stored or passed, matched by name); a run through such a slot is not a proven call and is no hop here: the callers verb on to= lists the binding sites and called_by=. "; inline constexpr const char* kUsesValueRoleLegend = "role=\"value\" (reference-as-value round): the function is USED AS A VALUE there, stored into a table, field or variable or passed as an argument, matched by name with the callers verb's own visibility rules. It is not a proven call; the callers verb shows where the value lands and who may call through it. A decorator row is a fact about the definition, not a site: the callers verb lists it, this verb does not. "; +// The value walk's depth cut (ingest_valuerefs.h kVrMaxDepth, valuerefs.h valueRefsCountAttrXml): rides EVERY answer +// that carries value_refs_depth_capped=, whether value_refs= is there or not — a zero beside it is the floor it names. +inline constexpr const char* kValueRefsDepthLegend = + "value_refs_depth_capped=N (absent when 0): N indexed files of this language nest deeper than the value-reference walk's 512-level cap (a long else-if chain, a deep expression), and a function used as a value below that depth is not seen: value_refs= (absent or not) is a floor there, and a reading built on it (dead_code_candidate=, risk=none-found, the dead-code list) may miss a table that holds the function. value_refs_depth_at= is the first cut, file:line: read the code there for value uses. "; +inline const char* valueRefsDepthLegend( bool on ) noexcept { return on ? kValueRefsDepthLegend : ""; } inline const char* usesValueRoleLegend( bool on ) noexcept { return on ? kUsesValueRoleLegend : ""; } inline const char* valueRefsLegend( bool on, bool callersSide ) noexcept { diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index 47969ca49..d00ec8c87 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -34,6 +34,10 @@ // Through: name=the container/parameter called through; fieldName=the written callee (through=); // composeRel=the key (".k", "[k]", "*" computed, "" bare); qualifier="p" (a parameter: argCount is its // index), "l" (a local container) or "f" (a file-scope container). +// Depth cut (role Value, at most one per file, after its rows): name="" (no identifier is empty); qualifier="d" +// (model.h kValueRefDepthCutScope); argCount=the subtrees kVrMaxDepth stopped the walk above (saturating); +// startByte/line=the first one. Not a row: valuerefindex.h counts it, and every answer that reads value +// references discloses it (value_refs_depth_capped= / value_refs_depth_at=). // A local/file Through survives only when the same scope fed that container a function value (the filter at // scope exit / file end), so an ordinary `x.m()` adds nothing; a parameter Through always survives — its // values arrive from callers elsewhere. @@ -41,7 +45,8 @@ // FLOORS (stated in the legend and pinned by recallshapecheck): a member or qualified value (obj.f, self.f, // ns::f, &Cls::m) and a function as the OBJECT of a member access (f.bind, f.name) are not rows; an import // alias is a local binding; a macro body is opaque; classes are not functions; Python attribute calls never -// count as a call through a dict; nodes deeper than kVrMaxDepth are not visited. +// count as a call through a dict; nodes deeper than kVrMaxDepth are not visited — DISCLOSED, never silent: the file's +// depth-cut record (below) makes value_refs= / --dead-code say the count is a floor and name the first cut site. namespace rw { @@ -51,7 +56,7 @@ namespace using VrFam = ValueRefFamily; // model.h: the one armed-language table the resolver indexes too -inline constexpr std::uint32_t kVrMaxDepth = 512; // the value-uses pass's own depth guard (ingest_sidecap.h kSideDepthUses) +inline constexpr std::uint32_t kVrMaxDepth = 512; // the value-uses pass's own depth guard (ingest_sidecap.h kSideDepthUses); a cut is disclosed as value_refs_depth_capped= (valuerefs.h) inline constexpr std::size_t kVrTextCap = 96; // a written slot / callee longer than this is cut with "…" // A written slot or callee, made attribute-safe: whitespace runs collapse to one space (an XML attribute may not @@ -146,6 +151,23 @@ struct VrScope std::vector pending; // calls through a container declared here, awaiting `fed` }; +// The walk's DISCLOSE sink for the depth cap: the subtrees kVrMaxDepth stopped it above, and the first one's site. +struct VrDepthCut +{ + enum class DisclosureWhy : std::uint8_t + { + SubtreesBelowMaxDepth, // nodes past kVrMaxDepth were not visited: their value references are not captured + }; + std::uint32_t subtrees = 0; + std::uint32_t startByte = 0; + std::uint32_t line = 0; + bool disclosed = false; + void disclose( DisclosureWhy ) noexcept + { + disclosed = true; + } +}; + struct VrAnc { TSNode node {}; @@ -203,10 +225,17 @@ class ValueRefWalk enter( root, 0 ); for( ;; ) { - if( m_anc.size() < kVrMaxDepth && ts_tree_cursor_goto_first_child( &cur ) ) + if( m_anc.size() < kVrMaxDepth ) { - enter( ts_tree_cursor_current_node( &cur ), ts_tree_cursor_current_field_id( &cur ) ); - continue; + if( ts_tree_cursor_goto_first_child( &cur ) ) + { + enter( ts_tree_cursor_current_node( &cur ), ts_tree_cursor_current_field_id( &cur ) ); + continue; + } + } + else if( ts_node_child_count( m_anc.back().node ) > 0 ) + { + noteDepthCut( m_anc.back().node ); // the cap stops the walk ABOVE these children: one subtree not visited } bool done = false; for( ;; ) @@ -241,6 +270,10 @@ class ValueRefWalk m_out.push_back( std::move( t ) ); } } + if( m_depthCut.subtrees > 0 ) + { + emitDepthCut(); + } } private: @@ -711,6 +744,106 @@ class ValueRefWalk return s; } + // A C declarator wrapper between a parameter list and the node that owns the declarator chain. + static bool isDeclaratorWrapper( const char* t ) noexcept + { + return kindIs( t, "function_declarator" ) || kindIs( t, "abstract_function_declarator" ) || kindIs( t, "pointer_declarator" ) + || kindIs( t, "abstract_pointer_declarator" ) || kindIs( t, "reference_declarator" ) + || kindIs( t, "abstract_reference_declarator" ) || kindIs( t, "parenthesized_declarator" ) + || kindIs( t, "abstract_parenthesized_declarator" ) || kindIs( t, "attributed_declarator" ) + || kindIs( t, "array_declarator" ) || kindIs( t, "abstract_array_declarator" ); + } + + // Is node kind `t` (about to be entered; its parent is m_anc.back()) a parameter list, or a parameter of one, that + // belongs to no function this walk has opened? A parameter declares its name only inside the function that OWNS the + // list — the innermost scope, a function, whose own declarator / `parameters` holds it. A prototype (`void + // set_handler( void (*handler)( int ) );`), a function-pointer typedef or parameter (`typedef void (*fn_t)( int sig + // );`), a C++ member declaration, a Go func type or interface method (`type H func( w Writer )`) and a TS function type, + // signature or `declare function` open no scope, so their names used to land in the ENCLOSING scope — the file scope + // at top level — and hid every same-named function there (`signal( SIGINT, handler )` lost its row). Python has no + // bodiless signature. A C parameter list NOT under a function declarator (a C++ catch clause, a template parameter + // list) keeps declaring into the innermost scope, as before. + bool foreignParameters( const char* t ) const noexcept + { + if( m_anc.size() < 2 ) + { + return false; + } + const VrScope& s = m_scopes.back(); + const VrAnc& parent = m_anc.back(); + switch( m_fam ) + { + case VrFam::C: + { + if( !( kindIs( t, "parameter_declaration" ) || kindIs( t, "optional_parameter_declaration" ) ) || !kindIs( parent.kind, "parameter_list" ) ) + { + return false; + } + std::size_t j = m_anc.size() - 2; + if( !kindIs( m_anc[j].kind, "function_declarator" ) && !kindIs( m_anc[j].kind, "abstract_function_declarator" ) ) + { + return false; // a catch clause, a template parameter list: unchanged + } + while( j > 0 && isDeclaratorWrapper( m_anc[j].kind ) ) + { + --j; + } + return !( s.isFunction && ts_node_eq( m_anc[j].node, s.node ) ); + } + case VrFam::Go: + { + if( !( kindIs( t, "parameter_declaration" ) || kindIs( t, "variadic_parameter_declaration" ) ) || !kindIs( parent.kind, "parameter_list" ) ) + { + return false; + } + return !( s.isFunction && ts_node_eq( m_anc[ m_anc.size() - 2 ].node, s.node ) ); // receiver, parameters and named results + } + case VrFam::Js: + { + return kindIs( t, "formal_parameters" ) && !( s.isFunction && ts_node_eq( parent.node, s.node ) ); + } + case VrFam::Py: + case VrFam::None: break; + } + return false; + } + + // ── the depth cut ────────────────────────────────────────────────────────────────────────────────── + // A node at kVrMaxDepth whose children the walk does not visit: a value reference below it is never captured. The + // first one's site is kept as the clue the answers print (value_refs_depth_at=). + void noteDepthCut( TSNode n ) noexcept + { + if( m_depthCut.subtrees == 0 ) + { + m_depthCut.startByte = ts_node_start_byte( n ); + m_depthCut.line = ts_node_start_point( n ).row + 1; + } + if( m_depthCut.subtrees < UINT32_MAX ) + { + ++m_depthCut.subtrees; + } + } + + // The file's ONE depth-cut record (RECORD SHAPE in the header comment): the cut is disclosed through it on every + // answer that reads value references (valuerefs.h value_refs_depth_capped=, --dead-code), and it is cached with the + // file's other references, so a warm run discloses it too. + void emitDepthCut() + { + EXPECTS( m_depthCut.subtrees > 0, "a depth-cut record is written only when the cap stopped the walk" ); + DISCLOSE( m_depthCut, VrDepthCut::DisclosureWhy::SubtreesBelowMaxDepth, + "value references: subtrees below kVrMaxDepth were not visited (value_refs_depth_capped=)" ); + RawRef r; + r.fileId = m_fileId; + r.startByte = m_depthCut.startByte; + r.line = m_depthCut.line; + r.lang = m_lang; + r.role = RefRole::Value; + r.qualifier = std::string( 1, kValueRefDepthCutScope ); // name stays "": no identifier is empty + r.argCount = static_cast( std::min( m_depthCut.subtrees, 0xFFFFu ) ); + r.argCountKnown = true; + m_out.push_back( std::move( r ) ); + } + // ── the walk ─────────────────────────────────────────────────────────────────────────────────────── void enter( TSNode n, TSFieldId field ) { @@ -734,7 +867,10 @@ class ValueRefWalk m_scopes.push_back( openScope( n, a.kind, sk ) ); a.opensScope = true; } - harvest( n, a.kind, m_scopes.back().decls ); + if( !foreignParameters( a.kind ) ) + { + harvest( n, a.kind, m_scopes.back().decls ); + } m_anc.push_back( a ); const std::size_t i = m_anc.size() - 1; if( m_fam == VrFam::Py && kindIs( a.kind, "decorated_definition" ) ) @@ -959,12 +1095,12 @@ class ValueRefWalk if( kindIs( kt, "field_designator" ) ) { const std::string nm( text( ts_node_named_child( k, 0 ) ) ); - into = "." + nm; + into = "." + vrClean( nm ); key = "." + nm; return; } const std::string w( text( ts_node_named_child( k, 0 ) ) ); - into = "[" + w + "]"; + into = "[" + vrClean( w ) + "]"; // `[A +\n 1] = f`: an expression, maybe multi-line — written like every other slot key = "[" + w + "]"; return; } @@ -1003,7 +1139,7 @@ class ValueRefWalk } if( vrKeyKind( kt ) == VrKeyKind::Number ) { - into = "[" + std::string( w ) + "]"; + into = "[" + vrClean( w ) + "]"; // the written slot is capped; the match key stays whole (no two keys collide) key = "[" + std::string( w ) + "]"; return; } @@ -1013,7 +1149,7 @@ class ValueRefWalk key = "*"; return; } - into = "." + std::string( w ); // JS property_identifier / Go struct field name + into = "." + vrClean( w ); // JS property_identifier / Go struct field name, capped as above key = "." + std::string( w ); } @@ -1095,7 +1231,7 @@ class ValueRefWalk return false; } const auto [ cont, id ] = containerOf( j - 1 ); - s.into = cont + "." + std::string( text( self.node ) ); + s.into = cont + "." + vrClean( text( self.node ) ); s.container = id; s.key = "." + std::string( text( self.node ) ); s.scope = id.empty() ? 'x' : 'f'; @@ -1538,6 +1674,7 @@ class ValueRefWalk std::vector m_anc; std::vector m_fileFed; std::vector m_filePending; + VrDepthCut m_depthCut; TSFieldId m_fValue = 0, m_fKey = 0, m_fLeft = 0, m_fRight = 0, m_fFunction = 0, m_fName = 0, m_fDeclarator = 0; TSFieldId m_fConsequence = 0, m_fAlternative = 0, m_fParameters = 0, m_fParameter = 0, m_fObject = 0, m_fProperty = 0, m_fField = 0; TSFieldId m_fArgument = 0, m_fOperand = 0, m_fAttribute = 0, m_fOperator = 0, m_fPattern = 0, m_fDefinition = 0; diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..16aad8682 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -812,7 +812,11 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& const ValueRefRows& callerSide = referencingOnly ? chRows.valueRefs : chCallers.valueRefs; if( referencingOnly ) { - out += valueRefsCountKeyJson( callerSide.rows.size() ); + out += valueRefsCountKeyJson( ing, callerSide, vr ); // + the depth cut that makes it a floor, as on the CLI + } + else + { + out += valueRefsDepthKeyJson( ing, callerSide.depthCut, vr ); // the --callers/--callees roots' depth cut (one defs set) } out += valueRefsJson( ing, callerSide, true, vr, "valueRefs", "--uses=" + name ); if( !referencingOnly ) @@ -2743,7 +2747,8 @@ inline std::optional impactText( const std::string& root, const std unprovenDefsVerbLegend( UnprovenDefsVerb::Impact, unprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs=, as on the CLI declinedCallsLegendWithGate( declinedCalls > 0, g.gateDeclinedCalls > 0 ), // exactly when the root carries declined_calls=, as on the CLI declinedIfaceLegend( declinedIface > 0 ), // likewise declined_iface=, as on the CLI - valueRefsReachLegend( !imValueRefs.rows.empty() ), // exactly when the root carries value_refs=, as on the CLI + ( std::string( valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs=, as on the CLI + + valueRefsDepthLegendFor( imValueRefs ) ).c_str(), // ... and the depth disclosure, as on the CLI graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), renderDisclosure( prD, DiscloseAs::LegendClause ).c_str() ); // r27-emitters §P2.1: the listing is capped at 40 by rank. Without shown=/capped= a 40-row answer to // "is it safe to change X?" reads as the WHOLE blast radius when it can be 3% of it. Same attributes, @@ -2763,7 +2768,7 @@ inline std::optional impactText( const std::string& root, const std ex( symbol ).c_str(), seeds.size(), reach.size(), unprovenDefsAttrXml( unprovenDefs ).c_str(), // H1: where the CLI root carries it byDepthAttrXml( byDepth ), // 0.6.5: the CLI root's by_depth= imports.xmlAttrs.c_str(), radiusTested, radiusUntested, ( declinedCallsAttrXml( declinedCalls ) + declinedIfaceAttrXml( declinedIface ) ).c_str(), - valueRefsCountAttrXml( imValueRefs.rows.size() ), imRootAttr.c_str(), + valueRefsCountAttrXml( ing, imValueRefs, VrRender{ imSingleRoot, imRootPrefix } ), imRootAttr.c_str(), pageDisclosure( ipab, sizeof( ipab ), shownRows, show.size(), ipw.end, page.limit, page.offset, true ), graphCountFloorAttrXml( g ).c_str(), renderDisclosure( prD, DiscloseAs::XmlAttrs ).c_str(), // M15: gauge + marker nextAttrXml( nextFlag( "--safe-delete=", symbol ) ).c_str() ); // P3 (L7): the CLI twin's next=, same root attribute set (mcpclidiffcheck) diff --git a/src/model.h b/src/model.h index cb58e3082..a844418a6 100644 --- a/src/model.h +++ b/src/model.h @@ -366,6 +366,9 @@ inline bool isJsTsBuiltinMember( std::string_view ctor, std::string_view name ) // field reuse (graph.h valueRefIndex reads exactly these): fieldName = the slot as written (into=), // recvVar = the simple container identifier, composeRel = the normalised key, qualifier = scope char + // file-shadow flag, argCount = the argument index. +// ONE Value record per file may instead be the DEPTH CUT (qualifier = kValueRefDepthCutScope, name = "", +// which no identifier is): the walk stopped at its depth cap there and argCount subtrees were not visited +// (saturating), line = the first one. It is no row; valuerefindex.h counts it for the answers' depth disclosure. // Through — a call THROUGH a value: a called parameter, `tbl[k](…)` / `tbl.k(…)` on a container that received a // function value. name = the container, fieldName = the written callee, composeRel = the key, // qualifier = p|l|f, argCount = the parameter index. Joined to Value rows only (called_by=/through=, a @@ -405,6 +408,9 @@ inline ValueRefFamily valueRefFamily( Lang l ) noexcept { return enumTableAt( kValueRefFamilyOfLang, l, ValueRefFamily::None ); } +// The scope char (qualifier[0]) of a file's depth-cut Value record (see RefRole::Value above): written by +// src/ingest_valuerefs.h, read by src/valuerefindex.h — distinct from every row scope (f l a p x). +inline constexpr char kValueRefDepthCutScope = 'd'; // Essential-complexity ev_why= reason vocabulary (the essential-complexity design note, §5.1). PUBLIC the // moment it ships (test/attrvocabcheck.sh's standing posture; test/essentialcxcheck.sh pins the spellings): diff --git a/src/valuerefindex.h b/src/valuerefindex.h index 251723360..baead0148 100644 --- a/src/valuerefindex.h +++ b/src/valuerefindex.h @@ -75,7 +75,11 @@ class ValueRefIndex for( std::uint32_t i = 0; i < ing.references.size(); ++i ) { const Reference& r = ing.references[i]; - if( r.role == RefRole::Value ) + if( r.role == RefRole::Value && scopeOf( r ) == kValueRefDepthCutScope ) + { + m_depthCuts.push_back( i ); // the file's depth-cut record: no row, a disclosure (depthCutsFor) + } + else if( r.role == RefRole::Value ) { m_values.push_back( i ); if( !r.recvVar.empty() ) @@ -138,6 +142,9 @@ class ValueRefIndex // `@property` or `@functools.wraps` hands the function back to the same name, so it is still reached only by a // call. A registering decorator (`@app.route`, `@register`) is the value use this exclusion exists for. The row // itself is still served (ruling 5); the wrapper FLOOR for rows is a follow-up. + // A reference made INSIDE `def` itself (`static void tick( void ) { timer_set( tick ); }`) does not count either: it + // can only run once something else reaches `def`, exactly as the call graph drops a recursive self-call (the CSR has + // no self-loops). The row is still served and is still a use site (--safe-delete's uses=). bool isValueReferenced( NodeId def ) const { const auto it = m_valuesByTarget.find( def ); @@ -147,10 +154,46 @@ class ValueRefIndex } return std::ranges::any_of( it->second, [ & ]( std::uint32_t k ) { - return !vrIsWrapperDecorator( m_ing.references[ m_values[k] ].fieldName ); + const Reference& r = m_ing.references[ m_values[k] ]; + return r.fromSymbol != def && !vrIsWrapperDecorator( r.fieldName ); } ); } + // The value walk's depth cut (ingest_valuerefs.h kVrMaxDepth) as it bears on an answer about `defs`: how many files + // of the defs' language families stopped their walk at the cap (a value reference below it was never captured, so a + // value_refs= count is a FLOOR there), and the first such record by path then line (the clue, value_refs_depth_at=). + // `everyFamily` asks about every armed family (--dead-code's whole-tree answer). Only functions and methods have + // value references, so a def of another kind brings no family in, and no def brings none. + struct DepthCuts + { + std::uint32_t files = 0; + std::uint32_t first = UINT32_MAX; // a reference index, UINT32_MAX when files == 0 + }; + DepthCuts depthCutsFor( std::span defs, bool everyFamily = false ) const + { + DepthCuts out; + for( const std::uint32_t i : m_depthCuts ) + { + const Reference& c = m_ing.references[i]; + const VrLangFamily fam = valueRefFamily( c.lang ); + const bool relevant = everyFamily || std::ranges::any_of( defs, [ & ]( NodeId d ) + { + const Symbol& s = m_ing.symbols[d]; + return ( s.kind == SymKind::Function || s.kind == SymKind::Method ) && valueRefFamily( s.lang ) == fam; + } ); + if( !relevant ) + { + continue; + } + ++out.files; + if( out.first == UINT32_MAX || vrCutLess( c, m_ing.references[ out.first ] ) ) + { + out.first = i; + } + } + return out; + } + // The resolved targets of value reference `refIdx` (an index into ing.references). std::vector targetsOf( std::uint32_t refIdx ) const { @@ -309,6 +352,11 @@ class ValueRefIndex } private: + bool vrCutLess( const Reference& a, const Reference& b ) const + { + return a.fileId != b.fileId ? m_ing.files[a.fileId] < m_ing.files[b.fileId] : a.line < b.line; + } + // A value key "#N" (an argument position) or "#name" (a keyword / a parameter default) against a parameter Through. static bool paramMatches( std::string_view valueKey, const Reference& through ) noexcept { @@ -558,6 +606,7 @@ class ValueRefIndex HashMap> m_fnByName; std::vector m_pyMember; // per symbol: 1 = a Python def directly in its class (empty: no Python) std::vector m_values; // Value reference indices, ascending + std::vector m_depthCuts; // the files' depth-cut records (no row), ascending std::vector> m_targets; // parallel to m_values HashMap> m_valuesByTarget; // target def → positions in m_values HashMap> m_valueByContainer; // "file#container" → Value refs @@ -584,6 +633,7 @@ struct ValueRefRow struct ValueRefRows { std::vector rows; // every row, served order; the window is the caller's + ValueRefIndex::DepthCuts depthCut; // the walk's depth cut over the answered defs' families (value_refs_depth_capped=) }; inline bool vrBindLess( const IngestResult& ing, const Reference& a, const Reference& b ) @@ -599,6 +649,7 @@ inline bool vrBindLess( const IngestResult& ing, const Reference& a, const Refer inline ValueRefRows valueRefCallerRows( const IngestResult& ing, const ValueRefIndex& idx, std::span defs ) { ValueRefRows out; + out.depthCut = idx.depthCutsFor( defs ); for( const std::uint32_t r : idx.valueRefsTo( defs ) ) { ValueRefRow row; @@ -653,15 +704,22 @@ inline bool vrCalleeRowLess( const IngestResult& ing, const ValueRefRow& a, cons { return ing.files[ta.fileId] < ing.files[tb.fileId]; } - return ta.line < tb.line; + if( ta.line != tb.line ) + { + return ta.line < tb.line; + } + return a.to < b.to; // two same-named definitions on one line (C++ overloads): their rows render alike, the order is still total } // --callees side: the functions `fns` store/pass as values (through= absent unless the same function also calls // through that very slot), and the functions they may call through a parameter or a container (through= the written -// callee; one row per (to, through), bind= its first site, sites= the count). +// callee; one row per (to, through), bind= its first site, sites= the number of distinct binding sites). +// Rows are per BINDING SITE, never per call through it: two calls `tbl.k(); tbl.k();` through one slot are one row, and +// `TABLE.k()` called twice through a slot one site fills is sites=1, not 2. inline ValueRefRows valueRefCalleeRows( const IngestResult& ing, const ValueRefIndex& idx, std::span fns ) { ValueRefRows out; + out.depthCut = idx.depthCutsFor( fns ); for( const std::uint32_t v : idx.madeIn( fns, RefRole::Value ) ) { for( const NodeId t : idx.targetsOf( v ) ) @@ -672,7 +730,8 @@ inline ValueRefRows valueRefCalleeRows( const IngestResult& ing, const ValueRefI out.rows.push_back( std::move( row ) ); } } - std::vector via; + std::vector via; + std::vector> viaSites; // parallel to via: the distinct binding sites (Value refs) each row joins for( const std::uint32_t t : idx.madeIn( fns, RefRole::Through ) ) { const Reference& tr = ing.references[t]; @@ -680,14 +739,18 @@ inline ValueRefRows valueRefCalleeRows( const IngestResult& ing, const ValueRefI { for( const NodeId target : idx.targetsOf( v ) ) { - // the same function stores AND calls through the slot: the stored row carries through= + // the same function stores AND calls through the slot: the stored row carries through= (the first + // written callee); a second call through the same slot and callee is that row again, not a new one auto same = std::find_if( out.rows.begin(), out.rows.end(), [ & ]( const ValueRefRow& r ) { - return r.ref == v && r.to == target && r.through.empty(); + return r.ref == v && r.to == target && ( r.through.empty() || r.through == tr.fieldName ); } ); if( same != out.rows.end() ) { - same->through = tr.fieldName; + if( same->through.empty() ) + { + same->through = tr.fieldName; + } continue; } auto grouped = std::find_if( via.begin(), via.end(), [ & ]( const ValueRefRow& r ) @@ -696,11 +759,16 @@ inline ValueRefRows valueRefCalleeRows( const IngestResult& ing, const ValueRefI } ); if( grouped != via.end() ) { + std::vector& sites = viaSites[ static_cast( grouped - via.begin() ) ]; + const auto at = std::ranges::lower_bound( sites, v ); + if( at == sites.end() || *at != v ) + { + sites.insert( at, v ); // a NEW binding site for this (to, through): counted once, whatever the calls + } if( vrBindLess( ing, ing.references[v], ing.references[grouped->ref] ) ) { grouped->ref = v; } - ++grouped->sites; continue; } ValueRefRow row; @@ -708,12 +776,14 @@ inline ValueRefRows valueRefCalleeRows( const IngestResult& ing, const ValueRefI row.to = target; row.through = tr.fieldName; via.push_back( std::move( row ) ); + viaSites.push_back( { v } ); } } } - for( ValueRefRow& r : via ) + for( std::size_t i = 0; i < via.size(); ++i ) { - out.rows.push_back( std::move( r ) ); + via[i].sites = static_cast( viaSites[i].size() ); + out.rows.push_back( std::move( via[i] ) ); } std::sort( out.rows.begin(), out.rows.end(), [ & ]( const ValueRefRow& a, const ValueRefRow& b ) { return vrCalleeRowLess( ing, a, b ); } ); return out; diff --git a/src/valuerefs.h b/src/valuerefs.h index 7a9a5c81b..913f1f0fb 100644 --- a/src/valuerefs.h +++ b/src/valuerefs.h @@ -171,8 +171,48 @@ inline std::string valueRefsJson( const IngestResult& ing, const ValueRefRows& r return out; } -inline std::string valueRefsCountAttrXml( std::size_t n ) { return countFieldOrEmpty( "value_refs", n, /*json=*/false ); } -inline std::string valueRefsCountKeyJson( std::size_t n ) { return countFieldOrEmpty( "value_refs", n, /*json=*/true ); } +// The depth cut over the answered definitions (ValueRefRows::depthCut, ingest_valuerefs.h kVrMaxDepth): files of their +// language family whose value walk stopped at the cap — value_refs_depth_capped="N" — and the first cut, file:line — +// value_refs_depth_at=. "" when there is none, so an answer over an uncut tree is byte-identical. +inline std::string valueRefsDepthAttrXml( const IngestResult& ing, const ValueRefIndex::DepthCuts& cut, const VrRender& rr ) +{ + if( cut.files == 0 ) + { + return {}; + } + EXPECTS( cut.first < ing.references.size(), "a depth cut counted over N > 0 files names its first record" ); + const Reference& r = ing.references[cut.first]; + std::vector esc; + return " value_refs_depth_capped=\"" + std::to_string( cut.files ) + "\" value_refs_depth_at=\"" + + std::string( escapeXml( vrPath( ing, r.fileId, rr ) + ":" + std::to_string( r.line ), esc ) ) + "\""; +} +inline std::string valueRefsDepthKeyJson( const IngestResult& ing, const ValueRefIndex::DepthCuts& cut, const VrRender& rr ) +{ + if( cut.files == 0 ) + { + return {}; + } + EXPECTS( cut.first < ing.references.size(), "a depth cut counted over N > 0 files names its first record" ); + const Reference& r = ing.references[cut.first]; + return ",\"value_refs_depth_capped\":" + std::to_string( cut.files ) + ",\"value_refs_depth_at\":\"" + + jsonStr( vrPath( ing, r.fileId, rr ) + ":" + std::to_string( r.line ) ) + "\""; +} + +// value_refs= beside the counts (absent at zero), then the depth cut that makes it a floor (absent when none) — present +// even when value_refs= is absent: a zero there means "none found above the cut", never "none". +inline std::string valueRefsCountAttrXml( const IngestResult& ing, const ValueRefRows& rows, const VrRender& rr ) +{ + return countFieldOrEmpty( "value_refs", rows.rows.size(), /*json=*/false ) + valueRefsDepthAttrXml( ing, rows.depthCut, rr ); +} +inline std::string valueRefsCountKeyJson( const IngestResult& ing, const ValueRefRows& rows, const VrRender& rr ) +{ + return countFieldOrEmpty( "value_refs", rows.rows.size(), /*json=*/true ) + valueRefsDepthKeyJson( ing, rows.depthCut, rr ); +} +// The legend clause for the depth attributes, exactly when an answer about these rows carries them. +inline const char* valueRefsDepthLegendFor( const ValueRefRows& rows ) noexcept +{ + return valueRefsDepthLegend( rows.depthCut.files > 0 ); +} } // namespace rw diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 62d43e5d4..1405c0119 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -227,7 +227,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + rw::crossKindLegend( !chRows.crossKind.empty() ) ).c_str(), // hono-07: likewise for cross_kind= rw::modScopeLegend( chHasModScope ), // #60: likewise, exactly when a t="modscope" row is - ( rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ) + rw::valueRefsLegend( !chRows.valueRefs.rows.empty(), wantCallers ) ).c_str(), + ( rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ) + rw::valueRefsLegend( !chRows.valueRefs.rows.empty(), wantCallers ) + + rw::valueRefsDepthLegendFor( chRows.valueRefs ) ).c_str(), // exactly when the root carries the depth disclosure rw::rootRelPathsLegend( chSingleRoot ), rw::multiRootTableLegend( ing.rootLabels.size() >= 2 ) ); } @@ -239,8 +240,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) // The tier-3 declines count= does not include (callhierarchy.h), on every dialect; absent at zero. const std::string chDeclinedAttr = rw::declinedCallsAttrXml( chRows.declinedCalls ) + rw::declinedIfaceAttrXml( chRows.declinedIface ); // Reference-as-value round: value_refs= beside count= (never in it), absent at zero; the window after the rows. - const std::string chValueRefsAttr = rw::valueRefsCountAttrXml( chRows.valueRefs.rows.size() ); const rw::VrRender chVr { chSingleRoot, chRootPrefix }; + const std::string chValueRefsAttr = rw::valueRefsCountAttrXml( ing, chRows.valueRefs, chVr ); // + the depth cut that makes it a floor const std::string chVrNext = "--uses=" + std::string( sym ); // --format=columnar (RESEARCH lever 1): the same page window, re-encoded as a path-table + parallel @@ -289,7 +290,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) if( chSingleRoot ) { rw::emitTo( stdout, ",\"root\":\"{}\"", jsonStr( cfg.roots[0] ).c_str() ); } rw::emitTo( stdout, ",\"hop_tested\":{},\"hop_untested\":{}{}{}", chTested.tested, chTested.untested, rw::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedIfaceKeyJson( chRows.declinedIface ), // A6; then the XML root's declined_calls=/declined_iface= - rw::valueRefsCountKeyJson( chRows.valueRefs.rows.size() ) ); + rw::valueRefsCountKeyJson( ing, chRows.valueRefs, chVr ) ); rw::emitTo( stdout, "{}{}", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, cfg.pageLimit, cfg.pageOffset, chDiscloseCap, kJsonPageSyntax ), rw::graphCountFloorAttrJson( g ).c_str() ); // §H4 §3.4 — the JSON dialect's spelling of the same marker @@ -847,6 +848,7 @@ struct SafeDeleteLegendFlags std::size_t declinedCalls = 0; // declined_calls= on the root (graph.h declinedCallsNaming) bool gateDeclined = false; // the builtin-method gate declined a call in this graph std::size_t valueRefs = 0; // value_refs= on the root (valuerefs.h): SYM is used as a value + bool valueRefDepthCut = false; // the depth disclosure is on the root: the value walk stopped at its depth cap }; inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs, std::size_t ambiguousCallers, std::string_view risk, @@ -864,6 +866,7 @@ inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs declinedClause += "none-found beside declined_calls= is not a safety reading: those calls may reach this definition. "; } declinedClause += rw::valueRefsSafeDeleteLegend( flags.valueRefs > 0 ); // exactly when the root carries value_refs= + declinedClause += rw::valueRefsDepthLegend( flags.valueRefDepthCut ); // exactly when it carries the depth disclosure rw::emitTo( stdout, "", // Reference-as-value round: beside its hyphenated siblings above, exactly when the root carries it. - valueRefExcluded > 0 ? "value-ref-excluded=N (absent when 0): internal functions kept off this list because a table, " - "field or argument holds them as a VALUE (matched by name; it is not a proven call; the callers verb " - "lists the sites). One entity, one reason: a def a reason above already excluded is counted there only. " - : "", + ( std::string( valueRefExcluded > 0 ? "value-ref-excluded=N (absent when 0): internal functions kept off this list because " + "a table, field or argument in ANOTHER function or at file scope holds them as a VALUE " + "(matched by name; it is not a proven call; the callers verb lists the sites); a function " + "that only stores itself is still listed. One entity, one reason: a def a reason above " + "already excluded is counted there only. " + : "" ) + + rw::valueRefsDepthLegend( dcDepthCut.files > 0 ) ).c_str(), // exactly when the root carries the depth disclosure rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str() ); // §P15/§P16: candidates is already deterministically sorted (path asc, line asc, name asc) and used to // print every candidate unconditionally — completeness was the whole contract, matching --uses' shape, @@ -2179,7 +2185,8 @@ std::optional runQualityViews( const MainDispatch& d ) // 0.6.6 D4: runner-root-excluded= is absent at 0, so a tree with no Python test/decorated root is byte-identical const std::string runnerRootAttr = ( runnerRootExcluded == 0 ? std::string() : std::format( " runner-root-excluded=\"{}\"", runnerRootExcluded ) ) + ( decoratedExcluded == 0 ? std::string() : std::format( " decorated-excluded=\"{}\"", decoratedExcluded ) ) - + ( valueRefExcluded == 0 ? std::string() : std::format( " value-ref-excluded=\"{}\"", valueRefExcluded ) ); + + ( valueRefExcluded == 0 ? std::string() : std::format( " value-ref-excluded=\"{}\"", valueRefExcluded ) ) + + rw::valueRefsDepthAttrXml( ing, dcDepthCut, rw::VrRender{ qvSingleRoot, qvRootPrefix } ); // absent when no cut rw::emitTo( stdout, "", candidates.size(), registerMacroExcluded, runnerRootAttr, dcFilterAttr.c_str(), diff --git a/test/recallshapecheck.sh b/test/recallshapecheck.sh index 640842f2b..5b5bf0984 100755 --- a/test/recallshapecheck.sh +++ b/test/recallshapecheck.sh @@ -1615,6 +1615,197 @@ vri_arm "RV3 MCP session: a chmod rebuilds the index with unchanged content — vri_arm "RV4 MCP session: a new non-source file rebuilds the index — the value-ref index is rebuilt too (vri=1), same rows" 's["got"][3] == ["1","1"] and s["rows"][3] == s["rows"][0]' vri_arm "RV5 MCP session: the server exited 0 with no sanitizer report (meaningful on an ASan build)" 's["rc"] == 0 and s["san"] == 0' +# ── re-review fixes (CodeRabbit's second pass on the reference-as-value round) ───────────────────────────────────── +# RP: a parameter declares its name only inside the function that OWNS the list — a prototype, a function-pointer +# typedef or parameter, a C++ member declaration, a Go func type / interface method and a TS function type, `declare +# function` or interface signature hide nothing (they used to land in the enclosing scope: at file scope they hid the +# function in the whole file). Near misses: a defined function's own parameter still hides it. +# RS: a function that only stores ITSELF (`timer_set( tick )` inside tick) is still dead — the CSR drops a recursive +# self-call too; one stored by another function is not. --safe-delete reads the same rule; the row is still served. +# RD: --callees rows are per binding site: two calls through one slot are one row, and sites= counts binding sites. +# RZ: the value walk's depth cap is disclosed (value_refs_depth_capped= / value_refs_depth_at=) on every answer that +# reads value references for the cut file's language family — absent below the cap, absent for another family. +# RK: every written key slot is capped and newline-free (a C `[expr]` designator, a numeric key, an identifier key). +echo "-- re-review fixes" +mkdir -p "$FX/proto" "$FX/protogo" "$FX/protots" "$FX/self" "$FX/dup" "$FX/deep" "$FX/deepneg" "$FX/keys" +printf 'void handler(int sig) { (void)sig; }\n' >"$FX/proto/a.c" +cat >"$FX/proto/b.c" <<'EOF' +#include +extern void handler(int); +void set_handler(void (*handler)(int)); /* @P_PROTO */ +typedef void (*fn_t)(int handler); /* @P_TYPEDEF */ +void install(void) { signal(SIGINT, handler); } /* @P_USE */ +void wrap(void (*handler)(int)) { signal(SIGINT, handler); } /* @P_PARAM */ +void wrap2(void (*cb)(int handler)) { signal(SIGINT, handler); } /* @P_INNER */ +EOF +cat >"$FX/proto/c.cpp" <<'EOF' +void hook(int x) { (void)x; } +int reg(void (*f)(int)); +struct S { void set(int hook); int go() { return reg(hook); } }; // @P_MEMBER +EOF +printf 'module example.com/p\n\ngo 1.22\n' >"$FX/protogo/go.mod" +printf 'package p\n\nfunc w(x int) int { return x }\n' >"$FX/protogo/a.go" +cat >"$FX/protogo/b.go" <<'EOF' +package p + +type H func(w int) int // @G_TYPE + +type I interface{ Do(w int) int } // @G_IFACE + +func Use() H { return w } // @G_USE + +func Run(w int) H { _ = w; return nil } // @G_PARAM +EOF +printf 'export function onDone(x: number) { return x; }\n' >"$FX/protots/a.ts" +cat >"$FX/protots/b.ts" <<'EOF' +import { onDone } from './a'; +type Cb = (onDone: number) => void; // @T_TYPE +declare function later(onDone: number): void; // @T_DECL +interface Hooks { fire(onDone: number): void; } // @T_IFACE +export function go(): (x: number) => number { return onDone; } // @T_USE +export function wrapT(onDone: number) { return [onDone]; } // @T_PARAM +EOF +arm "RP1 callers handler: a prototype's and a typedef's parameter named handler hide nothing in the file" \ + proto --callers=a.c:handler attr:value_refs=2 nvr:2 'vr:bind=@P_USE;into=signal#arg1' 'vr:bind=@P_INNER;into=signal#arg1' +arm "RP2 callers handler (near miss): a defined function's own parameter still hides it" proto --callers=a.c:handler attr:value_refs=2 'novr:bind=@P_PARAM' +arm "RP3 callers hook (C++): a member DECLARATION's parameter is no class member — the method body still sees ::hook" \ + proto --callers=c.cpp:hook attr:value_refs=1 'vr:bind=@P_MEMBER;into=reg#arg0' +arm "RP4 callers w (Go): a func type's and an interface method's parameter hide nothing; a function's own parameter does" \ + protogo --callers=a.go:w attr:value_refs=1 nvr:1 'vr:bind=@G_USE;into=(return)' 'novr:bind=@G_PARAM' +arm "RP5 callers onDone (TS): a function type's, a declare function's and an interface signature's parameter hide nothing; a function's own parameter does" \ + protots --callers=a.ts:onDone attr:value_refs=1 nvr:1 'vr:bind=@T_USE;into=(return)' 'novr:bind=@T_PARAM' + +cat >"$FX/self/t.c" <<'EOF' +void timer_set(void (*f)(void)); +static void tick(void) { timer_set(tick); } /* @S_TICK */ +static void tock(void) { timer_set(tock); } +void boot(void) { timer_set(tock); } /* @S_BOOT */ +EOF +arm "RS1 dead-code: tick, stored only by itself, is listed; value-ref-excluded= counts tock alone" \ + self --dead-code attr:count=1 nd:1 'd:n=tick' 'nod:n=tock' attr:value-ref-excluded=1 "legend:another function's" +arm "RS2 safe-delete tick: a dead-code candidate again — its self-row is still served and still a use site" \ + self --safe-delete=tick attr:dead_code_candidate=1 attr:value_refs=1 attr:uses=1 'vr:bind=@S_TICK;in_id=tick' +arm "RS3 safe-delete tock (near miss): another function stores it — not a candidate" \ + self --safe-delete=tock attr:dead_code_candidate=0 attr:value_refs=2 'vr:bind=@S_BOOT;in_id=boot' + +cat >"$FX/dup/t.js" <<'EOF' +function f() { return 1; } +function g() { return 2; } +function h() { return 3; } +const TABLE = { k: g }; // @D_TABLE +const T2 = { a: h, b: h }; +function store() { const tbl = {}; tbl.k = f; tbl.k(); tbl.k(); return tbl; } // @D_STORE +function store2() { const t = {}; t.k = f; t.j = f; t.k(); t.j(); return t; } +function run() { TABLE.k(); TABLE.k(); } +function run2(x) { T2[x](); T2[x](); } +module.exports = { store, store2, run, run2 }; +EOF +arm "RD1 callees store: two calls through one stored slot are ONE row" \ + dup --callees=store attr:value_refs=1 el:vrs:total=1 nvr:1 'vr:to=f;bind=@D_STORE;into=tbl.k;through=tbl.k;sites=-' +arm "RD2 callees run: two calls through a slot one site fills — sites= absent (one binding site)" \ + dup --callees=run attr:value_refs=1 nvr:1 'vr:to=g;bind=@D_TABLE;through=TABLE.k;sites=-' +arm "RD3 callees run2 (near miss): two binding sites behind one computed slot, called twice — sites=2, not 4" \ + dup --callees=run2 attr:value_refs=1 nvr:1 'vr:to=h;through=T2[x];sites=2' +arm "RD4 callees store2 (near miss): two binding sites in one function are two rows" \ + dup --callees=store2 attr:value_refs=2 nvr:2 'vr:to=f;into=t.k;through=t.k' 'vr:to=f;into=t.j;through=t.j' +cli dup --callees=store "$TMP/cli.dupstore.xml" +mcp_call find_symbol '{"path":"'"$FX/dup"'","symbol":"store"}' >"$TMP/mcp.dupstore.json" +verdict "RD5 MCP find_symbol(store).valueCallees == CLI --callees (one row)" "$( python3 "$TMP/par.py" json "$TMP/cli.dupstore.xml" "$TMP/mcp.dupstore.json" valueCallees )" +cli dup --callees=run2 "$TMP/cli.duprun2.xml" +mcp_call find_symbol '{"path":"'"$FX/dup"'","symbol":"run2"}' >"$TMP/mcp.duprun2.json" +verdict "RD6 MCP find_symbol(run2).valueCallees == CLI --callees (sites=2)" "$( python3 "$TMP/par.py" json "$TMP/cli.duprun2.xml" "$TMP/mcp.duprun2.json" valueCallees )" + +python3 - "$FX" <<'EOF' +import os, sys +fx = sys.argv[1] +s = "static int handler(int x) { return x; }\nint reg(int (*f)(int));\nint pick(int k) {\n if (k == 0) return 0;\n" +for i in range( 1, 300 ): + s += " else if (k == %d) return %d;\n" % ( i, i ) +s += " else return reg(handler);\n}\n" +open( os.path.join( fx, "deep", "elif.c" ), "w" ).write( s ) # a 300-link else-if chain nests past the 512-level cap +open( os.path.join( fx, "deep", "side.py" ), "w" ).write( "def py_fn():\n return 1\n\nPT = [py_fn]\n" ) +open( os.path.join( fx, "deepneg", "near.c" ), "w" ).write( + "static int handler(int x) { return x; }\nint reg(int (*f)(int));\nint install(void) { return reg(" + + "(" * 200 + "handler" + ")" * 200 + "); }\n" ) # deep, but under the cap +EOF +arm "RZ1 callers handler: the use sits below the value walk's depth cap — no row, and the root says the count is a floor" \ + deep --callers=handler attr:defs=1 noattr:value_refs attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' 'legend:value_refs_depth_at=' +cli deep --callers=handler "$TMP/cli.deep.xml" +res="$( python3 - "$TMP/cli.deep.xml" <<'EOF' +import re, sys +m = re.search( r'value_refs_depth_at="elif\.c:([0-9]+)"', open( sys.argv[1], encoding="utf-8" ).read() ) +print( "OK" if m and 5 <= int( m.group( 1 ) ) <= 303 else "FAIL value_refs_depth_at is not a line of the else-if chain: %r" % ( m.group( 0 ) if m else None ) ) +EOF +)" +verdict "RZ2 value_refs_depth_at= names the cut file and a line inside the chain (the clue where to read)" "$res" +arm "RZ3 dead-code: handler is listed AND the root says a table below the cap would be unseen" \ + deep --dead-code 'd:n=handler' attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' +arm "RZ4 safe-delete / impact carry the same disclosure (every surface that reads value references)" \ + deep --safe-delete=handler attr:dead_code_candidate=1 attr:value_refs_depth_capped=1 'legend:value_refs_depth_capped=' +arm "RZ4b impact handler: the disclosure beside reaches=" deep --impact=handler attr:value_refs_depth_capped=1 'legend:value_refs_depth_capped=' +arm "RZ5 callers py_fn (near miss): a Python function — the C file's cut is another family's, no disclosure" \ + deep --callers=py_fn attr:value_refs=1 noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +arm "RZ6 callers handler (near miss): 200 levels deep is under the cap — the row, no disclosure" \ + deepneg --callers=handler attr:value_refs=1 nvr:1 noattr:value_refs_depth_capped +arm "RZ6b dead-code (near miss): under the cap, handler is held and no disclosure rides" \ + deepneg --dead-code attr:value-ref-excluded=1 'nod:n=handler' noattr:value_refs_depth_capped +( cd "$FX/deep" && "$BIN" . --callers=handler --no-cache --json >"$TMP/j.deep.json" 2>/dev/null ) +mcp_call find_referencing_symbols '{"path":"'"$FX/deep"'","symbol":"handler"}' >"$TMP/mcp.deep.json" +mcp_call find_symbol '{"path":"'"$FX/deep"'","symbol":"handler"}' >"$TMP/mcp.deep2.json" +res="$( python3 - "$TMP/cli.deep.xml" "$TMP/j.deep.json" "$TMP/mcp.deep.json" "$TMP/mcp.deep2.json" <<'EOF' +import json, re, sys +x = open( sys.argv[1], encoding="utf-8" ).read() +want = ( re.search( r'value_refs_depth_capped="([0-9]+)"', x ), re.search( r'value_refs_depth_at="([^"]*)"', x ) ) +if not all( want ): + print( "FAIL the CLI XML carries no depth disclosure" ); raise SystemExit +want = ( int( want[0].group( 1 ) ), want[1].group( 1 ) ) +for f in sys.argv[2:]: + raw = open( f ).read() + try: + j = json.loads( raw ) + except ValueError: + print( "FAIL %s is not JSON: %r" % ( f.rsplit( "/", 1 )[-1], raw[:120] ) ); raise SystemExit + got = ( j.get( "value_refs_depth_capped" ), j.get( "value_refs_depth_at" ) ) + if got != want: + print( "FAIL %s carries %r, the CLI %r" % ( f.rsplit( "/", 1 )[-1], got, want ) ); raise SystemExit +print( "OK" ) +EOF +)" +verdict "RZ7 CLI --json, MCP find_referencing_symbols and find_symbol carry the CLI's value_refs_depth_capped / _at" "$res" +cache="$TMP/deepcache"; mkdir -p "$cache" +( cd "$FX/deep" && TMPDIR="$cache/" "$BIN" . --callers=handler >"$TMP/deep.cold.xml" 2>/dev/null; TMPDIR="$cache/" "$BIN" . --callers=handler >"$TMP/deep.warm.xml" 2>/dev/null ) +if grep -q 'value_refs_depth_capped="1"' "$TMP/deep.warm.xml" && cmp -s "$TMP/deep.cold.xml" "$TMP/deep.warm.xml"; then + ok "RZ8 a WARM run (cached facts) discloses the cut exactly as the cold run did" +else + no "RZ8 the warm run lost or changed the depth disclosure: $( grep -o 'value_refs_depth[a-z_]*="[^"]*"' "$TMP/deep.warm.xml" | tr '\n' ' ' )" +fi +( cd "$FX/deep" && "$BIN" . --callers=handler --no-cache --legend=full >"$TMP/full.deep.xml" 2>/dev/null ) +run_check "RZ9 callers legend (full) defines value_refs_depth_capped= and value_refs_depth_at=" "$FX/deep" "$TMP/full.deep.xml" \ + 'attr:value_refs_depth_capped=1' 'legend:value_refs_depth_capped=' 'legend:value_refs_depth_at=' + +python3 - "$FX" >"$TMP/keys.expect" <<'EOF' +import os, sys +fx = sys.argv[1] +open( os.path.join( fx, "keys", "k.c" ), "w" ).write( + "static int fk(int x) { return x; }\nstatic int fs(int x) { return x; }\nenum { A = 1, B = 2 };\n" + "static int (*tk[8])(int) = { [ A +\n B ] = fk };\n" + "static int (*ts[8])(int) = { [ 2 ] = fs };\n" ) +num, ident = "1" * 300, "a" * 300 +open( os.path.join( fx, "keys", "k.js" ), "w" ).write( + "function jn() { return 1; }\nfunction ji() { return 2; }\n" + "const JN = { %s: jn };\nconst JI = { %s: ji };\nmodule.exports = { JN, JI };\n" % ( num, ident ) ) +E = "…" +print( "tk[A + B]" ) # the designator expression, its newline collapsed — no in the attribute +print( "ts[2]" ) # a short designator: whole +print( "JN[" + "1" * 96 + E + "]" ) # a 300-digit numeric key: capped +print( "JI." + "a" * 96 + E ) # a 300-character identifier key: capped +EOF +i=0; while IFS= read -r want; do i=$(( i + 1 )); kexp[$i]="$want"; done <"$TMP/keys.expect" +arm "RK1 callers fk: a multi-line C designator [A + B] is written on one line" keys --callers=fk attr:value_refs=1 "vr:into=${kexp[1]}" +arm "RK2 callers fs (near miss): a short designator is whole" keys --callers=fs attr:value_refs=1 "vr:into=${kexp[2]}" +arm "RK3 callers jn: a 300-digit numeric key is capped" keys --callers=jn attr:value_refs=1 "vr:into=${kexp[3]}" +arm "RK4 callers ji: a 300-character identifier key is capped" keys --callers=ji attr:value_refs=1 "vr:into=${kexp[4]}" + echo if [ "$fail" -eq 0 ]; then echo "recallshapecheck: ALL PASS ($n CLI arms + parity)"; else echo "recallshapecheck: FAIL"; fi exit "$fail" From f0071cd6fd1b2634e2da417c802726c6ebf5f2bb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:56:48 -0400 Subject: [PATCH 009/229] go.mod: a quoted replace names the same module as the bare one The go.mod grammar lets a module path or a directory be an interpreted string ("example.com/x") or a raw one (`example.com/x`). The module census read a replace line's sides unquoted, so `replace "example.com/legacy" => "./legacy"` (or a raw string, single-line or grouped) put nothing in the tree and the call into it lost its true edge. One reader, goModToken (resolve.h), now reads a path the way the grammar spells it, for the module line, the census's replace lines and resolve.h's cross-root replace aliases. Gate: test/falseedgecheck.sh root goreplace/quoted/: a quoted and a raw-string local replace keep their edges; near miss, a quoted module replaced by another quoted module stays outside (a C external census row). Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 16 +++++------ src/resolve.h | 28 ++++++++++++++----- test/falseedgecheck.sh | 12 +++++++- test/falseedgefix/goreplace/quoted/app/app.go | 6 ++++ test/falseedgefix/goreplace/quoted/app/raw.go | 6 ++++ .../goreplace/quoted/app/remote.go | 6 ++++ test/falseedgefix/goreplace/quoted/go.mod | 13 +++++++++ .../goreplace/quoted/qlegacy/lib/lib.go | 4 +++ .../goreplace/quoted/qraw/lib/lib.go | 4 +++ 9 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 test/falseedgefix/goreplace/quoted/app/app.go create mode 100644 test/falseedgefix/goreplace/quoted/app/raw.go create mode 100644 test/falseedgefix/goreplace/quoted/app/remote.go create mode 100644 test/falseedgefix/goreplace/quoted/go.mod create mode 100644 test/falseedgefix/goreplace/quoted/qlegacy/lib/lib.go create mode 100644 test/falseedgefix/goreplace/quoted/qraw/lib/lib.go diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..5a188a073 100644 --- a/src/graph.h +++ b/src/graph.h @@ -3352,11 +3352,10 @@ inline std::string goModulePathOf( std::string_view text ) while( !line.empty() && ( line.front() == ' ' || line.front() == '\t' ) ) { line.remove_prefix( 1 ); } if( !line.starts_with( "module" ) || line.size() < 7 || ( line[ 6 ] != ' ' && line[ 6 ] != '\t' ) ) { continue; } line.remove_prefix( 7 ); - line = line.substr( 0, line.find( "//" ) ); // the comment goes FIRST: `module x // c` and `module "x" // c` are x - while( !line.empty() && ( line.front() == ' ' || line.front() == '\t' ) ) { line.remove_prefix( 1 ); } - while( !line.empty() && ( line.back() == ' ' || line.back() == '\t' || line.back() == '\r' ) ) { line.remove_suffix( 1 ); } - if( line.size() >= 2 && line.front() == '"' && line.back() == '"' ) { line = line.substr( 1, line.size() - 2 ); } - return std::string( line ); + // The comment goes FIRST: `module x // c` and `module "x" // c` are x. goModToken (resolve.h) then reads the + // path as the go.mod grammar spells it — bare, an interpreted string or a raw (backquoted) one — the reader the + // replace lines below and resolve.h's replace aliases share, so the three spellings cannot drift apart. + return std::string( goModToken( line.substr( 0, line.find( "//" ) ) ) ); } return {}; } @@ -3386,11 +3385,12 @@ inline std::vector goModuleTreePaths( std::string_view text ) { continue; } - const std::string_view left = trimWs( line.substr( 0, arrow ) ); - const std::string_view target = trimWs( line.substr( arrow + 2 ) ); + // either side may be quoted (`"X" => "./x"`, a raw `X`); goModToken also drops a version: `X v1.2.3 => ./x` + const std::string_view left = goModToken( line.substr( 0, arrow ) ); + const std::string_view target = goModToken( line.substr( arrow + 2 ) ); if( !left.empty() && !target.empty() && ( target.front() == '.' || target.front() == '/' ) ) { - paths.emplace_back( left.substr( 0, left.find_first_of( " \t" ) ) ); // drop a version: `X v1.2.3 => ./x` + paths.emplace_back( left ); } } return paths; diff --git a/src/resolve.h b/src/resolve.h index 653e31787..60154a638 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -923,6 +923,25 @@ inline std::string_view trimWs( std::string_view s ) noexcept return s; } +// The first token of a go.mod directive operand — a module path or a directory — as the go.mod grammar spells it: bare +// (up to whitespace), an interpreted string ("example.com/x") or a raw string (`example.com/x`), the quotes removed. "" +// when the operand is empty or its quote never closes. Both go.mod readers (this file's replace aliases, graph.h's module +// census) read a path through it, so a quoted spelling names the same module as the bare one. +inline std::string_view goModToken( std::string_view s ) noexcept +{ + s = trimWs( s ); + if( s.empty() ) + { + return {}; + } + if( s.front() == '"' || s.front() == '`' ) + { + const std::size_t close = s.find( s.front(), 1 ); + return close == std::string_view::npos ? std::string_view() : s.substr( 1, close - 1 ); + } + return s.substr( 0, s.find_first_of( " \t" ) ); +} + // Parse go.mod `replace OLD [ver] => NEW [ver]` directives (both single-line and grouped `replace ( … )`). // A filesystem NEW target (`./…`, `../…`, or absolute) is resolved relative to the go.mod's root dir; when it // ESCAPES `rootReal` (points at a sibling root) we mint a Go alias: module-path `OLD` → the on-disk dir. @@ -930,11 +949,6 @@ inline std::string_view trimWs( std::string_view s ) noexcept inline void parseGoModReplaces( const std::string& bytes, std::uint32_t fromRoot, const std::string& rootReal, std::vector& out ) { - const auto firstTok = []( std::string_view s ) noexcept - { - const std::size_t sp = s.find_first_of( " \t" ); - return sp == std::string_view::npos ? s : s.substr( 0, sp ); - }; bool inGroup = false; std::size_t i = 0; while( i < bytes.size() ) @@ -984,8 +998,8 @@ inline void parseGoModReplaces( const std::string& bytes, std::uint32_t fromRoot { continue; } - const std::string_view mod = firstTok( trimWs( body.substr( 0, arrow ) ) ); - const std::string_view tgt = firstTok( trimWs( body.substr( arrow + 2 ) ) ); + const std::string_view mod = goModToken( body.substr( 0, arrow ) ); // `"example.com/x" => "../x"` is x => ../x + const std::string_view tgt = goModToken( body.substr( arrow + 2 ) ); if( mod.empty() || tgt.empty() ) { continue; diff --git a/test/falseedgecheck.sh b/test/falseedgecheck.sh index 7a123d41c..4c7d9afb8 100755 --- a/test/falseedgecheck.sh +++ b/test/falseedgecheck.sh @@ -35,7 +35,9 @@ # (`lib.Shape()` via example.com/vendored); a function passed as a value. Root gonomod/ has NO go.mod: its # full-path import of an in-tree package keeps its edge (an unknown module path proves nothing outside). # Root gomodcomment/: a `module x // c` line and a nested `module "y" // c` are x and y (near miss: cmx is -# not under cm); root goreplace/: a module only a local `replace` names (its directory has no go.mod). +# not under cm); root goreplace/: a module only a local `replace` names (its directory has no go.mod), also +# spelled quoted (goreplace/quoted/: `"x" => "./x"`, a raw-string path; near miss: a quoted module replaced by +# another quoted MODULE stays outside). # (B) JS: JSON.stringify, `const { stringify } = JSON`, `new URL()`, Buffer.from, `globalThis.fetch`, console/Math/ # Object/Array/Promise members, require('destroy'), require('supertest'), a receiver from require('qs') # and a name destructured from require('cookie') never reach an in-repo function, getter, method or object @@ -251,6 +253,13 @@ lacks gomodcomment callees app/near.go:Near "Shape lib/lib.go" # goreplace/ keeps the replace-only shape the go/ root had before it became buildable Go: legacy/ has no go.mod, so # the replace line is the only source of example.com/legacy (twin of the old UseVendored arm) exactly goreplace callees app/app.go:UseLegacy "fn Shape legacy/lib/lib.go" +# goreplace/quoted/: the go.mod grammar lets either side of a replace be quoted — an interpreted string or a raw +# (backquoted) one, single-line or grouped. Each is the same local replace as the bare spelling. Near miss: a quoted +# module replaced by another quoted MODULE (not a directory) stays outside the tree — reading the quote itself as the +# start of a local path would put it in. +exactly goreplace callees quoted/app/app.go:UseQuoted "fn Quote quoted/qlegacy/lib/lib.go" +exactly goreplace callees quoted/app/raw.go:UseRaw "fn Raw quoted/qraw/lib/lib.go" +lacks goreplace callees quoted/app/remote.go:UseRemote "Quote quoted/qlegacy/lib/lib.go" echo "=== (B) JS: globals, required packages, accessors ===" lacks js callees lib/response.js:length "stringify lib/query.js" @@ -372,6 +381,7 @@ externals tsimport src/remote.ts pull fetch externals js lib/selfalias.js onMessage process externals ts src/selfalias.ts onTick process externals gomodcomment app/near.go Near Shape +externals goreplace quoted/app/remote.go UseRemote Quote # a Python name no import, local or module def binds and that is no builtin: no in-repo target, but nothing proves it is # outside the tree either (a closure variable, a star import of an unresolved module) — so it is no census row at all # (counted unresolved=), never a C external row and never a bound one diff --git a/test/falseedgefix/goreplace/quoted/app/app.go b/test/falseedgefix/goreplace/quoted/app/app.go new file mode 100644 index 000000000..62a12b338 --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/app/app.go @@ -0,0 +1,6 @@ +package app + +import "example.com/qlegacy/lib" + +// UseQuoted calls through an import a quoted local replace puts in this tree: a true edge. +func UseQuoted(n int) int { return lib.Quote(n) } diff --git a/test/falseedgefix/goreplace/quoted/app/raw.go b/test/falseedgefix/goreplace/quoted/app/raw.go new file mode 100644 index 000000000..87391737b --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/app/raw.go @@ -0,0 +1,6 @@ +package app + +import "example.com/qraw/lib" + +// UseRaw calls through an import a backquoted local replace puts in this tree: a true edge. +func UseRaw(n int) int { return lib.Raw(n) } diff --git a/test/falseedgefix/goreplace/quoted/app/remote.go b/test/falseedgefix/goreplace/quoted/app/remote.go new file mode 100644 index 000000000..74f22db55 --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/app/remote.go @@ -0,0 +1,6 @@ +package app + +import lib "example.com/qremote/lib" + +// UseRemote calls a module replaced by another MODULE, not a directory: outside the tree, no edge to the in-tree Quote. +func UseRemote(n int) int { return lib.Quote(n) } diff --git a/test/falseedgefix/goreplace/quoted/go.mod b/test/falseedgefix/goreplace/quoted/go.mod new file mode 100644 index 000000000..23055db7e --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/go.mod @@ -0,0 +1,13 @@ +module example.com/rq + +go 1.22 + +// Not buildable Go on purpose, as ../go.mod: the quoted spellings Go's go.mod grammar allows for a local replace. +replace "example.com/qlegacy" => "./qlegacy" + +replace ( + `example.com/qraw` => ./qraw +) + +// A quoted module replaced by another MODULE (not a directory) is not this tree's code. +replace "example.com/qremote" => "example.com/qfork" v1.0.0 diff --git a/test/falseedgefix/goreplace/quoted/qlegacy/lib/lib.go b/test/falseedgefix/goreplace/quoted/qlegacy/lib/lib.go new file mode 100644 index 000000000..56af67342 --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/qlegacy/lib/lib.go @@ -0,0 +1,4 @@ +package lib + +// Quote is in this tree under a module path only a QUOTED local replace maps here. +func Quote(n int) int { return n * 5 } diff --git a/test/falseedgefix/goreplace/quoted/qraw/lib/lib.go b/test/falseedgefix/goreplace/quoted/qraw/lib/lib.go new file mode 100644 index 000000000..c933adf49 --- /dev/null +++ b/test/falseedgefix/goreplace/quoted/qraw/lib/lib.go @@ -0,0 +1,4 @@ +package lib + +// Raw is in this tree under a module path only a raw-string (backquoted) local replace maps here. +func Raw(n int) int { return n * 7 } From e14b455cd46cb09faa772053ad929a22a91275f6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:56:48 -0400 Subject: [PATCH 010/229] falseedgecheck: pin the unresolved counts the census arms rely on unresolved_site passes when the census has NO row for a call, so a call that extraction dropped would pass it too (CodeRabbit on #372): run_all's process(), siblings.py's helper() and find_type() were each proven only by an absence. Each root that names unresolved sites now pins its census `unresolved=` to exactly that many (py 2, c 1): a dropped call lowers the count and fails, and a new unresolved call in the fixture fails until an arm names it. The pin refuses a missing count and an empty site list rather than passing. Co-Authored-By: Claude Opus 5.5 --- test/falseedgecheck.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/test/falseedgecheck.sh b/test/falseedgecheck.sh index 4c7d9afb8..aa770b3c3 100755 --- a/test/falseedgecheck.sh +++ b/test/falseedgecheck.sh @@ -72,7 +72,8 @@ # (F) propagation: --callers and --impact of the in-repo decoys no longer list the false callers. # (G) disclosure: every call the arms above unbind is a `C external` census row (empty targets) — except a Python bare # name nothing binds and no builtin table holds (run_all's `process`, siblings.py's `helper`), which has no -# in-repo target and no proof of an outside one: it is unresolved=, no census row at all; each root's map +# in-repo target and no proof of an outside one: it is unresolved=, no census row at all, and the py and c +# roots pin their census unresolved= to exactly the sites named (an omitted call cannot pass); each root's map # header external= equals its census `# dispositions external=` and is at least the number of expected # external rows; every root's dispositions still sum to calls= with unaccounted=0. # (H) MCP twins (fresh TMPDIR cache): find_referencing_symbols / find_symbol name exactly the rows the CLI's @@ -387,6 +388,7 @@ externals goreplace quoted/app/remote.go UseRemote Quote # (counted unresolved=), never a C external row and never a bound one unresolved_site(){ local r="$1" file="$2" caller="$3" callee="$4" + printf '%s %s %s\n' "$file" "$caller" "$callee" >>"$TMP/$r.nunres" # the pin below counts the sites named per root if python3 - "$TMP/$r.tsv" "$file" "$caller" "$callee" <<'PY' import sys census, path, caller, callee = sys.argv[ 1: ] @@ -405,6 +407,20 @@ unresolved_site py src/ui/widget.py run_all process unresolved_site py src/ui/siblings.py run helper externals py src/ui/widget.py read_config open unresolved_site c copy.c classify find_type +# unresolved_site reads an ABSENT census row, so a call extraction dropped would pass it too (CodeRabbit on #372). Each +# root that names unresolved sites therefore pins its census `unresolved=` to exactly the number it names: a dropped call +# lowers the count (red), and a new unresolved call in the fixture raises it (red until an arm names that site too). +unresolved_pinned(){ + local r="$1" want got + want="$( sort -u "$TMP/$r.nunres" 2>/dev/null | wc -l | tr -d ' ' )" + got="$( grep -m1 '^# dispositions ' "$TMP/$r.tsv" 2>/dev/null | grep -oE ' unresolved=[0-9]+' | grep -oE '[0-9]+$' )" + if [ -z "$got" ]; then no "(G) ($r) the census dispositions carry no unresolved= count — the pin cannot be read" + elif [ "$want" -lt 1 ]; then no "(G) ($r) no unresolved site is named for this root — the pin would be vacuous" + elif [ "$got" -ne "$want" ]; then no "(G) ($r) census unresolved=$got, but this gate names $want unresolved site(s): $( tr '\n' ';' <"$TMP/$r.nunres" )" + else ok "(G) ($r) census unresolved=$got == the $want site(s) named above: each was extracted and left unresolved"; fi +} +unresolved_pinned py +unresolved_pinned c externals c window.c elapsed clock externals rs src/lib.rs draw render for r in $ROOTS; do From f239515b2c68c980b0b3d632ce71b953fa04ec57 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:59:55 -0400 Subject: [PATCH 011/229] kParserVer 143 -> 145, kQSnapCacheScheme 17 -> 18; CHANGELOG and the dictionary figures Two extraction changes in the value-reference pass (a prototype's parameter declares nothing; a depth-cut record per cut file) and the capped key slots change index content: kParserVer 145, with quality.h's mirror. Not 144: 143's full-use file tag is 144 (parserVerFor adds 1), so a lean 144 would read those blobs as its own. The dead set moved (a function that only stores itself is dead again): kQSnapCacheScheme 18. qschemetrip re-pinned with a RE-PIN LOG entry; no assertion changed. The legend dictionary gained the two depth-cut definitions and a reworded value-ref-excluded one: dictv=beedfcd755c9457f entries=786, read from the binary. The showcase capture's --legend-dict figures are corrected in place to those values and docs/COMMANDS.md regenerated from it (docs_commands_build.py --bin build/ripwire). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 27 ++++++++++++++----- docs/COMMANDS.md | 2 +- docs/captures/COMMANDS_showcase_2026-09-14.md | 4 +-- src/ingest_cache.h | 10 ++++++- src/quality.h | 8 ++++-- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 5 ++++ 7 files changed, 45 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..c3579d6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,18 @@ call-shaped references, so on such a function `--callers`, `--callees`, `--impac tree, django and webpack, 30 were identical. The 3 that differed are in scope: a read row became `role="value"`; a callee answer gained 2 rows; `--dead-code` dropped 4 functions that a `NODE_SET_METHOD` argument or a CommonJS `module.exports` table holds. +- **Depth.** The capture does not descend past 512 levels of nesting (a 300-link C `else if` chain reaches it). That + cut is disclosed, never silent: every answer that reads value references for the cut file's language — the + callers/callees roots, `--impact`, `--safe-delete`, `--dead-code`, `--json` and the MCP twins — carries + `value_refs_depth_capped=N` (files cut) and `value_refs_depth_at=FILE:LINE` (the first cut), so a missing row or a + listed dead function there reads as a floor. +- **Second review.** A parameter of a prototype, a function-pointer typedef or parameter, a C++ member declaration, a + Go func type or interface method, or a TS function type, `declare function` or interface signature no longer hides a + same-named function in the enclosing scope (`signal( SIGINT, handler )` beside `typedef void (*fn_t)( int handler )` + was no row). A function that only stores ITSELF (`timer_set( tick )` inside tick) is no longer kept off `--dead-code` + or `dead_code_candidate`, as a recursive self-call is no caller. `--callees` rows are per binding site: two calls + through one slot are one row, and `sites=` counts binding sites, not calls. A C `[expr]` designator, a numeric key + and an identifier key are capped like every other written slot. - **Cost.** A cold default map costs +3.6% CPU on this tree, +6.3% on django and +5.2% on webpack (median of 5, `sim/refval_cpu.sh`). - **Cache.** `kParserVer` moves. @@ -513,15 +525,17 @@ per page instead. Gate: `test/impactdepthcheck.sh`. ### Changed — the versions this release moves, stated once -`kParserVer` 124 → 143 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier +`kParserVer` 124 → 145 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier span fix and the TypeScript `await f(x)` / `!f(x)` calls each took a number of their own on their branches, as did the false-edge resolution (134, 135) and the value-reference rows (140); 141 sat above every number a branch build of unreleased work had used, and the review fixes to the global-object shadow and the value-reference slot text take 143, -above 141's full-use file tag 142, so no cache such a build wrote is read as this release's), `kCacheVersion` 25 → 28 -(the function-literal fix's record changes, then the false-edge fix's member-call fields) and `kQSnapCacheScheme` 15 → 17 +above 141's full-use file tag 142, so no cache such a build wrote is read as this release's; the second review's +value-reference fixes take 145, above 143's full-use tag 144), `kCacheVersion` 25 → 28 +(the function-literal fix's record changes, then the false-edge fix's member-call fields) and `kQSnapCacheScheme` 15 → 18 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions -held as values). Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=04d7833c60d2d5bd entries=784`. +held as values, then on a function that only stores itself). Every ingest cache written by an earlier build is refused +and re-indexed once, and every cached quality snapshot is recomputed. The session legend dictionary is +`dictv=beedfcd755c9457f entries=786`. ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition @@ -535,7 +549,8 @@ the result as a confident edge. Three shapes are now resolved the way the langua - a JavaScript/TypeScript call on a global object (`JSON.parse`, `Buffer.from`, `crypto.subtle.verify`) or to a global function (`fetch`) in a file that neither imports nor declares that name; - a call through `require( 'pkg' )`, `import * as ns from 'pkg'` or a name destructured from a global object, and a Go - call through an import whose path no go.mod in the tree contains. + call through an import whose path no go.mod in the tree contains (a go.mod path is read as its grammar spells it: + bare, quoted or a raw string, with a trailing comment dropped, so `replace "x" => "./x"` puts x in the tree). Such a call has no edge and is counted `external=` where the language proves the target is outside the tree (a builtin, a global, an outside package or `use`, a Go predeclared function, a C library name), `unresolved=` otherwise. A name an import binds from inside the tree, and every implicit-receiver language (Java, C#, C++, Kotlin, Swift, Ruby, diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..daf177f4b 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4398,7 +4398,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=beedfcd755c9457f entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..6ae2466b8 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -5351,7 +5351,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=beedfcd755c9457f entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5381,7 +5381,7 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [755 more display lines; full output is 77894 bytes on 785 raw line(s)] +… [757 more display lines; full output is 78143 bytes on 787 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` diff --git a/src/ingest_cache.h b/src/ingest_cache.h index e13079f71..26ecff27c 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -302,7 +302,15 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 143; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change + // 145 = 2026-10-04 (train 25 re-review fixes): three extraction changes — + // a parameter of a prototype, function-pointer typedef, Go func type or TS + // function type no longer declares its name in the enclosing scope; a file + // whose value walk stops at kVrMaxDepth gains ONE depth-cut Value record; + // the remaining written key slots (a C `[expr]` designator, a numeric or + // identifier key) go through vrClean. 145, not 144: 143's RICH file tag is + // 144 (parserVerFor below adds 1). The train renumbers if another lane + // takes 145/146. // 143 = 2026-10-04 (train 25 review fixes): two extraction changes — // a declaration named like the global object (`var self = this`, a // parameter `window`) is now a JsShadow binding, and a value-reference diff --git a/src/quality.h b/src/quality.h index 5961d1b00..a30468496 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,8 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 143; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) + // 145 = 2026-10-04 (train 25 re-review fixes; see the kParserVer note) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) @@ -3378,7 +3379,10 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // argument holds as a VALUE (valuerefindex.h ValueRefIndex::isValueReferenced, checked after every other exemption, // counted as value-ref-excluded= like --dead-code): the dead SET moved, as in v9/v12/v15. kParserVer moved with // the extraction (the Value/Through rows) and its mirror moved with it. Bumped 16 -> 17. -constexpr std::uint32_t kQSnapCacheScheme = 17; +// v18 (2026-10-04, train 25 re-review fixes) — isValueReferenced ignores a value reference made inside the function +// itself (`timer_set( tick )` in tick), as the CSR drops a recursive self-call: the dead SET moved, as in v9/v12/v15/v17. +// kParserVer moved too (145: prototype parameters, the depth-cut record). Bumped 17 -> 18. +constexpr std::uint32_t kQSnapCacheScheme = 18; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 2192e4759..1f78abd6d 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -32240f4552ee695a54f885409b67c60585d6fc7c1f3cffc362313f6c772dafcc +a4e37a094bcb61237281c6d9203daed614cfbef2e15e81b9c484c5bef4735858 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index eaf642263..b93342110 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,11 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-04, train 25 re-review fixes: RE-DERIVED with UPDATE_GOLDEN=1 (hash a4e37a094b…735858). kParserVer 143 -> 145 +# (a prototype's / func type's parameter declares nothing in the enclosing scope; a file's value walk cut at +# kVrMaxDepth gains a depth-cut record; the remaining written key slots are capped) with quality.h's mirror; 145 +# because 143's full-use file tag is 144. kQSnapCacheScheme 17 -> 18: the dead set moved (a function that only +# stores itself is no longer value-referenced). No assertion changed. # 2026-10-04, train 25 review fixes: RE-DERIVED with UPDATE_GOLDEN=1 (hash 32240f4552…2dafcc). kParserVer 141 -> 143 (two # extraction changes: a declaration named like the JS global object is a JsShadow binding; value-reference slot text # is cut on a UTF-8 boundary and JS string keys are capped) with quality.h's mirror; 143 because 141's full-use file From 8d3ea27efb742295c88ee6dedb282f224d622787 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:16:53 -0400 Subject: [PATCH 012/229] recallshapecheck: RD4 calls its two slots in reverse order With the calls in source order, a --callees join that matched a stored row by function alone (not by binding site) still came out right by order luck; called in reverse, it puts each through= on the other row, so RD4 now fails that mutation (as RF4 does). Co-Authored-By: Claude Opus 5.5 --- test/recallshapecheck.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/recallshapecheck.sh b/test/recallshapecheck.sh index 5b5bf0984..23bc7de2b 100755 --- a/test/recallshapecheck.sh +++ b/test/recallshapecheck.sh @@ -1695,7 +1695,7 @@ function h() { return 3; } const TABLE = { k: g }; // @D_TABLE const T2 = { a: h, b: h }; function store() { const tbl = {}; tbl.k = f; tbl.k(); tbl.k(); return tbl; } // @D_STORE -function store2() { const t = {}; t.k = f; t.j = f; t.k(); t.j(); return t; } +function store2() { const t = {}; t.k = f; t.j = f; t.j(); t.k(); return t; } function run() { TABLE.k(); TABLE.k(); } function run2(x) { T2[x](); T2[x](); } module.exports = { store, store2, run, run2 }; @@ -1706,7 +1706,7 @@ arm "RD2 callees run: two calls through a slot one site fills — sites= absent dup --callees=run attr:value_refs=1 nvr:1 'vr:to=g;bind=@D_TABLE;through=TABLE.k;sites=-' arm "RD3 callees run2 (near miss): two binding sites behind one computed slot, called twice — sites=2, not 4" \ dup --callees=run2 attr:value_refs=1 nvr:1 'vr:to=h;through=T2[x];sites=2' -arm "RD4 callees store2 (near miss): two binding sites in one function are two rows" \ +arm "RD4 callees store2 (near miss): two binding sites in one function are two rows, each with its own call (called in reverse order)" \ dup --callees=store2 attr:value_refs=2 nvr:2 'vr:to=f;into=t.k;through=t.k' 'vr:to=f;into=t.j;through=t.j' cli dup --callees=store "$TMP/cli.dupstore.xml" mcp_call find_symbol '{"path":"'"$FX/dup"'","symbol":"store"}' >"$TMP/mcp.dupstore.json" From 635b275fdb9ec5c968246a016875b2a958a09b76 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:16:53 -0400 Subject: [PATCH 013/229] --dead-code help and ARCHITECTURE: a self-stored function, the depth disclosure Two sentences the value-reference fixes made incomplete: --dead-code's help and ARCHITECTURE said any value use keeps a function off the list; a function that only stores itself is listed now. Both also name the depth-cut disclosure (value_refs_depth_capped= / value_refs_depth_at=). docs/COMMANDS.md regenerated from the binary (docs_commands_build.py --bin build/ripwire). The MCP tools/list descriptions are unchanged: tools/list is 46,732 B under a 46,750 B ceiling that moves only for a declared argument, never for prose; the two JSON keys are defined in the legend dictionary (ripwire://legend-dict). Co-Authored-By: Claude Opus 5.5 --- docs/ARCHITECTURE.md | 5 ++++- docs/COMMANDS.md | 2 +- src/cli.h | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 80c7fd288..8cc741b10 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -378,7 +378,10 @@ call graph's visibility (same file first; C `static` stays in its file; JS/TS/Py import; Go stays in its package) and serves them as separate `` rows with `value_refs=N`: where the value lands (`into=`), and which functions may call through that slot (`called_by=`/`through=`). A row is a clue to follow, not a proven call; `--dead-code` and `--safe-delete` treat a value use as -a reason a function is not dead. +a reason a function is not dead (one made by another function or at file scope: a function that only +stores itself stays dead, as a recursive self-call is no caller). The capture stops at 512 levels of +nesting; a file it cut carries one depth-cut record, and every answer that reads value references for +that file's language says so (`value_refs_depth_capped=`, `value_refs_depth_at=`). ### graph — the CSR diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index daf177f4b..197e9d6fd 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -2774,7 +2774,7 @@ $ ./build/ripwire . --owners **Answers:** list internal functions with no caller anywhere in the indexed tree internal source functions with no caller found in the indexed tree — a name-based graph reading, not a confidence score (dynamic dispatch, reflection and macro-generated callers are invisible to it); -=DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed. A symbol whose definition is produced by a SELF-REGISTERING test/benchmark macro is never reported: doctest TEST_CASE/ TEST_CASE_FIXTURE/SCENARIO, gtest TEST/TEST_F/TEST_P, Catch2, Google Benchmark — a static initializer registers them, so a name-based call graph cannot see the caller and every one of them would be a false positive. Extend the list for your own framework with `.ripwire_config`'s one key, `register_macros = NAME[, NAME...]` (one directive per line, # comments). The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0. Exempt from dead-code only — such a symbol still participates in clone detection. A function a table, field or argument holds as a VALUE (--callers' rows) is not reported either: value-ref-excluded="N" counts them, absent at 0 (matched by name, not a proven call). A LEADING ./ anchors DIR at the repo ROOT (=./src matches only the top-level src/ subtree); a bare name (=src) matches that component ANYWHERE in the tree, including nested (test/fixture/src/…) +=DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed. A symbol whose definition is produced by a SELF-REGISTERING test/benchmark macro is never reported: doctest TEST_CASE/ TEST_CASE_FIXTURE/SCENARIO, gtest TEST/TEST_F/TEST_P, Catch2, Google Benchmark — a static initializer registers them, so a name-based call graph cannot see the caller and every one of them would be a false positive. Extend the list for your own framework with `.ripwire_config`'s one key, `register_macros = NAME[, NAME...]` (one directive per line, # comments). The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0. Exempt from dead-code only — such a symbol still participates in clone detection. A function a table, field or argument holds as a VALUE (--callers' rows) is not reported either: value-ref-excluded="N" counts them, absent at 0 (matched by name, not a proven call); a function that only stores ITSELF still is. A value walk cut by deep nesting is disclosed: value_refs_depth_capped="N" files, value_refs_depth_at= the first cut (the list may over-report). A LEADING ./ anchors DIR at the repo ROOT (=./src matches only the top-level src/ subtree); a bare name (=src) matches that component ANYWHERE in the tree, including nested (test/fixture/src/…) **Try it** diff --git a/src/cli.h b/src/cli.h index 373fd9ed9..51109ea37 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1694,7 +1694,9 @@ inline constexpr char kHelpHead[] = " is DISCLOSED, never silent: register-macro-excluded=\"N\" rides the report and prints even at 0. Exempt from dead-code only —\n" " such a symbol still participates in clone detection.\n" " A function a table, field or argument holds as a VALUE (--callers' rows) is not reported\n" - " either: value-ref-excluded=\"N\" counts them, absent at 0 (matched by name, not a proven call).\n" + " either: value-ref-excluded=\"N\" counts them, absent at 0 (matched by name, not a proven call);\n" + " a function that only stores ITSELF still is. A value walk cut by deep nesting is disclosed:\n" + " value_refs_depth_capped=\"N\" files, value_refs_depth_at= the first cut (the list may over-report).\n" " A LEADING ./ anchors DIR at the repo ROOT (=./src matches only the top-level src/ subtree); a bare name (=src) matches that\n" " component ANYWHERE in the tree, including nested (test/fixture/src/…)\n" " --quality-baseline snapshot today's complexity, clones and dead code as the floor to measure against\n" From 7ec3682fe339f7795da35aaa0c9596ca2a1c633c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:20:54 -0400 Subject: [PATCH 014/229] profilePmc: PMC_DIAG speaks std::format, so -DPROFILE_PMC_VERBOSE=1 builds again PMC_DIAG expanded to rw::emitRaw( stderr, "prof::pmc: " , args... ), but emitRaw is (stream, text) only since the std::print conversion, so the arming trace could not be compiled in at all: the one build that explains why counters did not arm failed first. It now expands to rw::emitTo with the prefix concatenated onto a std::format string, and the 14 call sites move from %s/%u/%d/%llu/%zd to {}. pmccheck.sh gains a verbose-compile arm: the harness must build and run under -DPROFILE_PMC_VERBOSE=1, with the trace on stderr and none on stdout. On the base tree that arm fails to compile (no matching function for emitRaw); here it passes. Build-flag only: the default build never expands the macro, so no output moves. Co-Authored-By: Claude Sonnet 5.5 --- src/infra/profilePmc.h | 31 ++++++++++++++++--------------- test/pmccheck.sh | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/infra/profilePmc.h b/src/infra/profilePmc.h index 869fed7a4..9f5394343 100644 --- a/src/infra/profilePmc.h +++ b/src/infra/profilePmc.h @@ -79,7 +79,8 @@ struct Snapshot #endif #if PROFILE_PMC_VERBOSE - #define PMC_DIAG( ... ) rw::emitRaw( stderr, "prof::pmc: " __VA_ARGS__ ) + // `fmt` is a std::format string (rw::emitTo's spelling, `{}` not `%s`); the prefix is concatenated onto the literal. + #define PMC_DIAG( fmt, ... ) rw::emitTo( stderr, "prof::pmc: " fmt __VA_OPT__( , ) __VA_ARGS__ ) #else #define PMC_DIAG( ... ) ( (void) 0 ) #endif @@ -238,7 +239,7 @@ inline bool resolve_event( kpep_db* db, const char* alias, kpep_event** out ) no // try the alias verbatim first (some DBs expose the friendly name directly) if( g_api.kpep_db_event( db, alias, out ) == 0 && *out ) { - PMC_DIAG( "resolved '%s' -> '%s' (verbatim)\n", alias, alias ); + PMC_DIAG( "resolved '{}' -> '{}' (verbatim)\n", alias, alias ); return true; } @@ -258,7 +259,7 @@ inline bool resolve_event( kpep_db* db, const char* alias, kpep_event** out ) no } if( g_api.kpep_db_event( db, probe, out ) == 0 && *out ) { - PMC_DIAG( "resolved '%s' -> '%s'\n", alias, probe ); + PMC_DIAG( "resolved '{}' -> '{}'\n", alias, probe ); return true; } } @@ -291,7 +292,7 @@ inline void ensure_global_init() noexcept { std::call_once( g_once, []() noexcept { - PMC_DIAG( "init: euid=%u (root needed unless entitled)\n", unsigned( geteuid() ) ); + PMC_DIAG( "init: euid={} (root needed unless entitled)\n", unsigned( geteuid() ) ); if( !load_api() ) { @@ -326,7 +327,7 @@ inline void ensure_global_init() noexcept kpep_event* ev = nullptr; if( !resolve_event( g_perf.db, kDefaultSelection[ i ], &ev ) ) { - PMC_DIAG( "skip '%s' (not in this core's DB)\n", kDefaultSelection[ i ] ); + PMC_DIAG( "skip '{}' (not in this core's DB)\n", kDefaultSelection[ i ] ); continue; } @@ -334,7 +335,7 @@ inline void ensure_global_init() noexcept const int rc = g_api.kpep_config_add_event( g_perf.cfg, &ev, 0, &err ); if( rc != 0 ) { - PMC_DIAG( "skip '%s' (add rc=%d err=%u; PMC budget?)\n", kDefaultSelection[ i ], rc, err ); + PMC_DIAG( "skip '{}' (add rc={} err={}; PMC budget?)\n", kDefaultSelection[ i ], rc, err ); continue; } @@ -403,7 +404,7 @@ inline void ensure_global_init() noexcept } g_perf.ok = true; - PMC_DIAG( "OK: %u events armed, %u raw counters\n", g_perf.event_count, g_perf.counter_count ); + PMC_DIAG( "OK: {} events armed, {} raw counters\n", g_perf.event_count, g_perf.counter_count ); } ); } @@ -439,7 +440,7 @@ ALWAYS_INLINE Snapshot read() noexcept if( !logged ) { logged = true; - PMC_DIAG( "first read: rc=%d cc=%u raw[0..2]=%llu,%llu,%llu\n", rc, g_perf.counter_count, + PMC_DIAG( "first read: rc={} cc={} raw[0..2]={},{},{}\n", rc, g_perf.counter_count, (unsigned long long) raw[ 0 ], (unsigned long long) raw[ 1 ], (unsigned long long) raw[ 2 ] ); } #endif @@ -627,7 +628,7 @@ inline bool open_group( ThreadCounters& tc, const unsigned* tableIndices, unsign const long fd = sys_perf_event_open( &attr, 0, -1, groupFd, 0 ); if( fd < 0 ) { - PMC_DIAG( "open '%s' failed (errno=%d) — column dropped%s\n", desc.alias, errno, isLeader ? ", leadership passes to the next event that opens" : "" ); + PMC_DIAG( "open '{}' failed (errno={}) — column dropped{}\n", desc.alias, errno, isLeader ? ", leadership passes to the next event that opens" : "" ); continue; // graceful per-event skip, leader included — same rule as the kperf side } @@ -647,7 +648,7 @@ inline bool arm_and_verify( ThreadCounters& tc, GroupRead* out ) noexcept if( ::ioctl( tc.fds[ 0 ], PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP ) != 0 || ::ioctl( tc.fds[ 0 ], PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP ) != 0 ) { - PMC_DIAG( "group reset/enable ioctl failed (errno=%d)\n", errno ); + PMC_DIAG( "group reset/enable ioctl failed (errno={})\n", errno ); return false; } @@ -660,7 +661,7 @@ inline bool arm_and_verify( ThreadCounters& tc, GroupRead* out ) noexcept const ssize_t got = ::read( tc.fds[ 0 ], out, sizeof( *out ) ); if( got < ssize_t( 3 * sizeof( std::uint64_t ) ) || out->nr != tc.fd_count ) { - PMC_DIAG( "group verify read got=%zd nr=%llu (want %u fds) — over PMU budget?\n", + PMC_DIAG( "group verify read got={} nr={} (want {} fds) — over PMU budget?\n", got, ( unsigned long long ) ( got > 0 ? out->nr : 0 ), tc.fd_count ); return false; } @@ -680,7 +681,7 @@ inline bool map_value_indices( ThreadCounters& tc, const GroupRead& probe ) noex std::uint64_t id = 0; if( ::ioctl( tc.fds[ slot ], PERF_EVENT_IOC_ID, &id ) != 0 ) { - PMC_DIAG( "PERF_EVENT_IOC_ID failed for slot %u (errno=%d)\n", slot, errno ); + PMC_DIAG( "PERF_EVENT_IOC_ID failed for slot {} (errno={})\n", slot, errno ); return false; } @@ -696,7 +697,7 @@ inline bool map_value_indices( ThreadCounters& tc, const GroupRead& probe ) noex } if( !found ) { - PMC_DIAG( "id %llu for slot %u missing from group read\n", ( unsigned long long ) id, slot ); + PMC_DIAG( "id {} for slot {} missing from group read\n", ( unsigned long long ) id, slot ); return false; } } @@ -735,7 +736,7 @@ inline void select_and_arm_first_thread( ThreadCounters& tc ) noexcept g_perf.event_count = openedCount; g_perf.ok = true; tc.ok = true; - PMC_DIAG( "OK: %u events armed (pinned group)\n", openedCount ); + PMC_DIAG( "OK: {} events armed (pinned group)\n", openedCount ); return; } @@ -765,7 +766,7 @@ inline void select_and_arm_first_thread( ThreadCounters& tc ) noexcept candidates[ candidateCount++ ] = openedRows[ i ]; } } - PMC_DIAG( "dropped '%s', retrying with %u events\n", kEvents[ openedRows[ dropIndex ] ].alias, candidateCount ); + PMC_DIAG( "dropped '{}', retrying with {} events\n", kEvents[ openedRows[ dropIndex ] ].alias, candidateCount ); } tc.close_all(); diff --git a/test/pmccheck.sh b/test/pmccheck.sh index d3b007c23..6d42edb5f 100755 --- a/test/pmccheck.sh +++ b/test/pmccheck.sh @@ -107,4 +107,27 @@ if [ "$STATE" = "INACTIVE" ]; then echo " PASS quiet degrade (INACTIVE, stderr carries only the report)" fi +# ── diagnostics compile: -DPROFILE_PMC_VERBOSE=1 must build (PMC_DIAG once spelled printf varargs against an emitter that +# had become (stream, text) only — the trace for "which arming step failed" was uncompilable exactly when wanted) and +# must stay on stderr: stdout is the data stream. The run's own contract is the same one asserted above. ────────────── +VBIN="$WORK/pmccheckharness_verbose" +if ! "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra -pthread \ + -DPROFILE_ENABLED=1 -DPROFILE_AUTO_REPORT=0 -DPROFILE_PMC_VERBOSE=1 \ + -I"$ROOT/src/infra" -I"$ROOT/src" -I"$ROOT/third_party" \ + "$HARNESS" -o "$VBIN" 2> "$WORK/cc_verbose.log"; then + echo " FAIL verbose diagnostics (-DPROFILE_PMC_VERBOSE=1 failed to compile)"; sed 's/^/ /' "$WORK/cc_verbose.log" | head -10; exit 2 +fi +if ! "$VBIN" > "$WORK/out_verbose.log" 2> "$WORK/err_verbose.log"; then + echo " FAIL verbose diagnostics (harness contract violated under -DPROFILE_PMC_VERBOSE=1)"; exit 2 +fi +# the real backends (Apple Silicon, Linux) always trace something while arming; the inert stubs elsewhere cannot +case "$( uname -s )-$( uname -m )" in + Darwin-arm64|Linux-*) NEED_TRACE=yes ;; + *) NEED_TRACE=no ;; +esac +if grep -q 'prof::pmc:' "$WORK/out_verbose.log" || { [ "$NEED_TRACE" = yes ] && ! grep -q 'prof::pmc:' "$WORK/err_verbose.log"; }; then + echo " FAIL verbose diagnostics (the arming trace must reach stderr, and only stderr)"; exit 2 +fi +echo " PASS verbose diagnostics (-DPROFILE_PMC_VERBOSE=1 compiles; the trace is on stderr, not stdout)" + echo "pmccheck: PASS" From dc9a36571ea99603f2cb7905145b4c4ff0bace94 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:36:50 -0400 Subject: [PATCH 015/229] =?UTF-8?q?test:=20forspinecheck=20+=20forsigspanc?= =?UTF-8?q?heck=20=E2=80=94=20--for=20caller=20spine,=20e=3D=20end=20line,?= =?UTF-8?q?=20hop=20slots,=20code=20over=20docs=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gates written before the code they measure, with one shared fixture (test/forcompletefix: C, JS, Python, TS roots, each indexed on its own). forspinecheck: a --for answer names who drives its top seeds — the nearest proven callers up to 2 hops (a scan routine's refresh check and main loop, a dispatcher loop) and the tables a seed is stored in (a struct-array of draw function pointers, a key table, an object-literal handler map) as via="value" rows, names and file:line only, in the answer's tail. Near misses: a C function-pointer field call, an untyped JS/Python parameter receiver never show as a plain caller row; a test caller and a module export are never rows. Every row must be backed by the same binary's --callers answer. forsigspancheck: rows carry e= (the definition's last line) right after l=, absent when the extent is unknown (extent_suspect, doc rows); a hop slot never goes to a row whose only callee edge was bound by name; the defining code outranks markdown headings that repeat the question. Both are red on the pre-change binary for the arms the change adds; the near-miss arms that must stay true are green. Gate count 664 -> 666. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 2 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/forcompletefix/README.md | 3 + test/forcompletefix/c/src/board.c | 18 + test/forcompletefix/c/src/gauge.c | 40 ++ test/forcompletefix/c/src/keys.c | 35 ++ test/forcompletefix/c/src/main.c | 10 + test/forcompletefix/c/src/monitor.c | 41 +++ test/forcompletefix/c/src/monitor.h | 66 ++++ test/forcompletefix/c/src/sampler.c | 29 ++ test/forcompletefix/c/src/sink.c | 23 ++ test/forcompletefix/c/src/window.c | 27 ++ test/forcompletefix/c/test/sampler_test.c | 9 + test/forcompletefix/js/lib/queue.js | 45 +++ test/forcompletefix/js/lib/worker.js | 26 ++ test/forcompletefix/js/package.json | 1 + test/forcompletefix/py/src/pump/__init__.py | 0 test/forcompletefix/py/src/pump/app.py | 16 + test/forcompletefix/py/src/pump/cache.py | 20 + test/forcompletefix/py/src/pump/loop.py | 21 ++ test/forcompletefix/py/src/pump/resolver.py | 15 + test/forcompletefix/ts/docs/context.md | 23 ++ test/forcompletefix/ts/package.json | 1 + test/forcompletefix/ts/src/application.ts | 23 ++ test/forcompletefix/ts/src/context.ts | 15 + test/forcompletefix/ts/src/registry.ts | 7 + test/forcompletefix/ts/src/request.ts | 15 + test/forsigspancheck.sh | 352 ++++++++++++++++++ test/forspinecheck.sh | 387 ++++++++++++++++++++ test/regression.sh | 2 +- 32 files changed, 1279 insertions(+), 9 deletions(-) create mode 100644 test/forcompletefix/README.md create mode 100644 test/forcompletefix/c/src/board.c create mode 100644 test/forcompletefix/c/src/gauge.c create mode 100644 test/forcompletefix/c/src/keys.c create mode 100644 test/forcompletefix/c/src/main.c create mode 100644 test/forcompletefix/c/src/monitor.c create mode 100644 test/forcompletefix/c/src/monitor.h create mode 100644 test/forcompletefix/c/src/sampler.c create mode 100644 test/forcompletefix/c/src/sink.c create mode 100644 test/forcompletefix/c/src/window.c create mode 100644 test/forcompletefix/c/test/sampler_test.c create mode 100644 test/forcompletefix/js/lib/queue.js create mode 100644 test/forcompletefix/js/lib/worker.js create mode 100644 test/forcompletefix/js/package.json create mode 100644 test/forcompletefix/py/src/pump/__init__.py create mode 100644 test/forcompletefix/py/src/pump/app.py create mode 100644 test/forcompletefix/py/src/pump/cache.py create mode 100644 test/forcompletefix/py/src/pump/loop.py create mode 100644 test/forcompletefix/py/src/pump/resolver.py create mode 100644 test/forcompletefix/ts/docs/context.md create mode 100644 test/forcompletefix/ts/package.json create mode 100644 test/forcompletefix/ts/src/application.ts create mode 100644 test/forcompletefix/ts/src/context.ts create mode 100644 test/forcompletefix/ts/src/registry.ts create mode 100644 test/forcompletefix/ts/src/request.ts create mode 100755 test/forsigspancheck.sh create mode 100755 test/forspinecheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 46492fcb1..446c99c33 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -197,6 +197,8 @@ "forrankordercheck.sh": 77.9, "forrootlegendcheck.sh": 8.6, "forsectioncollapsecheck.sh": 5.2, +"forsigspancheck.sh": 6.0, +"forspinecheck.sh": 8.0, "freshclonecheck.sh": 170.8, "freshnesscheck.sh": 4.4, "g1configcheck.sh": 0.8, diff --git a/README.md b/README.md index 26dd08715..295421fbe 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-664 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +666 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **664 gate scripts** and is the authoritative list; +`test/regression.sh` names **666 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..f8c5be3f8 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` | 664 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 666 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **664 gate scripts**, all of which exist on disk. +naming **666 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 664. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 666. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..aad58c82a 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 = [ - ["664 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["666 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "664", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "666", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["666 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/forcompletefix/README.md b/test/forcompletefix/README.md new file mode 100644 index 000000000..d81351ce8 --- /dev/null +++ b/test/forcompletefix/README.md @@ -0,0 +1,3 @@ +Fixture roots for test/forspinecheck.sh (the --for caller spine and value-reference owners) and +test/forsigspancheck.sh (the e= end line on rows, hop-slot choice, code ranked above docs). +Each subdirectory (c/, js/, py/, ts/) is indexed on its own. The code is minimal, written for the gates, and never built. diff --git a/test/forcompletefix/c/src/board.c b/test/forcompletefix/c/src/board.c new file mode 100644 index 000000000..c82102cd4 --- /dev/null +++ b/test/forcompletefix/c/src/board.c @@ -0,0 +1,18 @@ +#include "monitor.h" + +void Board_onKey( Board* b, int key ) +{ + Widget* w = b->focus; + if( w && w->klass->onKey ) + { + w->klass->onKey( w, key ); + } +} + +void Board_paint( Board* b ) +{ + for( int i = 0; i < b->gaugeCount; i++ ) + { + Gauge_render( &b->gauges[ i ] ); + } +} diff --git a/test/forcompletefix/c/src/gauge.c b/test/forcompletefix/c/src/gauge.c new file mode 100644 index 000000000..dc6f973d2 --- /dev/null +++ b/test/forcompletefix/c/src/gauge.c @@ -0,0 +1,40 @@ +#include +#include "monitor.h" + +static void Gauge_drawBar( Gauge* g ) +{ + for( int i = 0; i < g->value; i++ ) + { + putchar( '|' ); + } +} + +static void Gauge_drawText( Gauge* g ) +{ + printf( "%d", g->value ); +} + +static void Gauge_drawDots( Gauge* g ) +{ + for( int i = 0; i < g->value; i += 10 ) + { + putchar( '.' ); + } +} + +const GaugeStyle Gauge_modes[] = { + { .label = "bar", .draw = Gauge_drawBar }, + { .label = "text", .draw = Gauge_drawText }, + { .label = "dots", .draw = Gauge_drawDots }, +}; + +void Gauge_setStyle( Gauge* g, int style ) +{ + g->style = style; + g->paint = Gauge_modes[ style ].draw; +} + +void Gauge_render( Gauge* g ) +{ + g->paint( g ); +} diff --git a/test/forcompletefix/c/src/keys.c b/test/forcompletefix/c/src/keys.c new file mode 100644 index 000000000..1b61604e7 --- /dev/null +++ b/test/forcompletefix/c/src/keys.c @@ -0,0 +1,35 @@ +#include "monitor.h" + +typedef void ( *KeyAction )( Board* b ); + +static KeyAction keyTable[ 128 ]; + +static void actionCycleStyle( Board* b ) +{ + for( int i = 0; i < b->gaugeCount; i++ ) + { + Gauge_setStyle( &b->gauges[ i ], ( b->gauges[ i ].style + 1 ) % 3 ); + } +} + +static void actionFreeze( Board* b ) +{ + b->gaugeCount = 0; +} + +void Keys_bind( void ) +{ + keyTable[ 's' ] = actionCycleStyle; + keyTable[ 'f' ] = actionFreeze; +} + +static void BoardWidget_onKey( Widget* w, int key ) +{ + KeyAction fn = keyTable[ key & 127 ]; + if( fn ) + { + fn( ( Board* )w->state ); + } +} + +const WidgetClass BoardWidget_class = { .onKey = BoardWidget_onKey }; diff --git a/test/forcompletefix/c/src/main.c b/test/forcompletefix/c/src/main.c new file mode 100644 index 000000000..ef1398cb5 --- /dev/null +++ b/test/forcompletefix/c/src/main.c @@ -0,0 +1,10 @@ +#include "monitor.h" + +int main( int argc, char** argv ) +{ + ( void )argc; + ( void )argv; + Monitor* m = Monitor_new(); + Monitor_loop( m ); + return 0; +} diff --git a/test/forcompletefix/c/src/monitor.c b/test/forcompletefix/c/src/monitor.c new file mode 100644 index 000000000..3dac55a95 --- /dev/null +++ b/test/forcompletefix/c/src/monitor.c @@ -0,0 +1,41 @@ +#include +#include "monitor.h" + +static void Monitor_refresh( Monitor* m ) +{ + if( m->stale ) + { + Sampler_collectAll( m->sampler ); + m->stale = 0; + } + Board_paint( m->board ); +} + +void Monitor_loop( Monitor* m ) +{ + while( !m->quit ) + { + Monitor_refresh( m ); + int key = Input_read(); + if( key == 'q' ) + { + m->quit = 1; + } + else if( key >= 0 ) + { + Board_onKey( m->board, key ); + } + } +} + +Monitor* Monitor_new( void ) +{ + Monitor* m = calloc( 1, sizeof( Monitor ) ); + m->stale = 1; + return m; +} + +int Input_read( void ) +{ + return -1; +} diff --git a/test/forcompletefix/c/src/monitor.h b/test/forcompletefix/c/src/monitor.h new file mode 100644 index 000000000..6634c46b9 --- /dev/null +++ b/test/forcompletefix/c/src/monitor.h @@ -0,0 +1,66 @@ +#ifndef MONITOR_H +#define MONITOR_H + +typedef struct Probe { int id; int raw; } Probe; + +typedef struct Sampler +{ + Probe* probes; + int values[ 16 ]; + int count; +} Sampler; + +typedef struct Gauge Gauge; +typedef void ( *GaugeDrawFn )( Gauge* g ); + +typedef struct GaugeStyle +{ + const char* label; + GaugeDrawFn draw; +} GaugeStyle; + +struct Gauge +{ + int value; + int style; + GaugeDrawFn paint; +}; + +typedef struct Widget Widget; +typedef struct WidgetClass +{ + void ( *onKey )( Widget* w, int key ); +} WidgetClass; + +struct Widget +{ + const WidgetClass* klass; + void* state; +}; + +typedef struct Board +{ + Widget* focus; + Gauge* gauges; + int gaugeCount; +} Board; + +typedef struct Monitor +{ + Sampler* sampler; + Board* board; + int stale; + int quit; +} Monitor; + +int Probe_read( Probe* p ); +void Sampler_collectAll( Sampler* s ); +void Board_onKey( Board* b, int key ); +void Board_paint( Board* b ); +void Gauge_setStyle( Gauge* g, int style ); +void Gauge_render( Gauge* g ); +int Input_read( void ); +void Monitor_loop( Monitor* m ); +Monitor* Monitor_new( void ); + +#endif diff --git a/test/forcompletefix/c/src/sampler.c b/test/forcompletefix/c/src/sampler.c new file mode 100644 index 000000000..8fb847d4a --- /dev/null +++ b/test/forcompletefix/c/src/sampler.c @@ -0,0 +1,29 @@ +#include "monitor.h" + +int Probe_read( Probe* p ) +{ + return p->raw * 2 + p->id; +} + +static void Sampler_collectOne( Sampler* s, int i ) +{ + s->values[ i ] = Probe_read( &s->probes[ i ] ); +} + +static void Sampler_prune( Sampler* s ) +{ + while( s->count > 0 && s->values[ s->count - 1 ] < 0 ) + { + s->count--; + } +} + +/* Collect one reading from every probe, then drop trailing invalid readings. */ +void Sampler_collectAll( Sampler* s ) +{ + for( int i = 0; i < s->count; i++ ) + { + Sampler_collectOne( s, i ); + } + Sampler_prune( s ); +} diff --git a/test/forcompletefix/c/src/sink.c b/test/forcompletefix/c/src/sink.c new file mode 100644 index 000000000..e13047a25 --- /dev/null +++ b/test/forcompletefix/c/src/sink.c @@ -0,0 +1,23 @@ +#include "monitor.h" + +typedef struct Sink +{ + void ( *flush )( Sampler* s ); +} Sink; + +/* Write every buffered sample out and reset the buffer. */ +void flush( Sampler* s ) +{ + s->count = 0; +} + +void Sink_drainSamples( Sink* sink, Sampler* s ) +{ + /* a call through the field: nothing proves sink->flush is the free function flush */ + sink->flush( s ); +} + +void Monitor_flushSamples( Sampler* s ) +{ + flush( s ); +} diff --git a/test/forcompletefix/c/src/window.c b/test/forcompletefix/c/src/window.c new file mode 100644 index 000000000..a773814de --- /dev/null +++ b/test/forcompletefix/c/src/window.c @@ -0,0 +1,27 @@ +#include "monitor.h" + +static int Sampler_scale( int v ) { return v * 2; } + +static int +Sampler_average( Sampler* s ) +{ + int sum = 0; + for( int i = 0; i < s->count; i++ ) + { + sum += Sampler_scale( s->values[ i ] ); + } + return s->count > 0 ? sum / s->count : 0; +} + +/* Legal C with the struct defined in the return type: both definitions are extent-suspect (head). */ +struct SampleWindow +{ + int lo; + int hi; +} Sampler_window( Sampler* s ) +{ + struct SampleWindow w; + w.lo = 0; + w.hi = Sampler_average( s ); + return w; +} diff --git a/test/forcompletefix/c/test/sampler_test.c b/test/forcompletefix/c/test/sampler_test.c new file mode 100644 index 000000000..5a71336bb --- /dev/null +++ b/test/forcompletefix/c/test/sampler_test.c @@ -0,0 +1,9 @@ +#include "../src/monitor.h" + +int test_collect_all( void ) +{ + Probe probes[ 2 ] = { { 1, 2 }, { 2, 3 } }; + Sampler s = { probes, { 0 }, 2 }; + Sampler_collectAll( &s ); + return s.values[ 0 ] == 5 ? 0 : 1; +} diff --git a/test/forcompletefix/js/lib/queue.js b/test/forcompletefix/js/lib/queue.js new file mode 100644 index 000000000..41c64a355 --- /dev/null +++ b/test/forcompletefix/js/lib/queue.js @@ -0,0 +1,45 @@ +'use strict'; + +function resizeImage( payload ) { + const w = payload.width / 2; + const h = payload.height / 2; + return { width: w, height: h }; +} + +function logUnknown( payload ) { + return { skipped: true, payload }; +} + +const handlers = { + resize: resizeImage, + fallback: logUnknown, +}; + +function pickHandler( type ) { + return handlers[ type ] || handlers.fallback; +} + +function runJob( job ) { + const handler = pickHandler( job.type ); + return handler( job.payload ); +} + +class JobQueue { + constructor() { + this.jobs = []; + this.stopped = false; + } + + push( job ) { + this.jobs.push( job ); + } + + drain() { + while( this.jobs.length > 0 && !this.stopped ) { + const job = this.jobs.shift(); + runJob( job ); + } + } +} + +module.exports = { JobQueue, runJob }; diff --git a/test/forcompletefix/js/lib/worker.js b/test/forcompletefix/js/lib/worker.js new file mode 100644 index 000000000..46a88b72c --- /dev/null +++ b/test/forcompletefix/js/lib/worker.js @@ -0,0 +1,26 @@ +'use strict'; + +const { JobQueue } = require( './queue' ); + +class Worker { + constructor() { + this.queue = new JobQueue(); + } + + tick() { + this.queue.drain(); + } + + start() { + for( let i = 0; i < 3; i++ ) { + this.tick(); + } + } +} + +// A relay forwards to whatever it was handed: nothing proves `target` is a JobQueue. +function relay( target ) { + target.drain(); +} + +module.exports = { Worker, relay }; diff --git a/test/forcompletefix/js/package.json b/test/forcompletefix/js/package.json new file mode 100644 index 000000000..bb0878868 --- /dev/null +++ b/test/forcompletefix/js/package.json @@ -0,0 +1 @@ +{ "name": "jobs-fixture", "version": "0.0.0", "private": true } diff --git a/test/forcompletefix/py/src/pump/__init__.py b/test/forcompletefix/py/src/pump/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/forcompletefix/py/src/pump/app.py b/test/forcompletefix/py/src/pump/app.py new file mode 100644 index 000000000..c9e32aa93 --- /dev/null +++ b/test/forcompletefix/py/src/pump/app.py @@ -0,0 +1,16 @@ +from pump.loop import MessagePump +from pump.resolver import Resolver + + +class App(MessagePump): + def __init__(self): + super().__init__() + self.resolver = Resolver() + + def on_lookup(self, message): + return self.resolver.resolve(message.name) + + +def main(): + app = App() + app.run_forever() diff --git a/test/forcompletefix/py/src/pump/cache.py b/test/forcompletefix/py/src/pump/cache.py new file mode 100644 index 000000000..0bf39eada --- /dev/null +++ b/test/forcompletefix/py/src/pump/cache.py @@ -0,0 +1,20 @@ +class LookupCache: + def __init__(self): + self._slots = {} + + def fetch_entry(self, key): + """Return the cached entry for key, computing and storing it on a miss.""" + if key not in self._slots: + self._slots[key] = self._compute(key) + return self._slots[key] + + @staticmethod + def _compute(key): + return key.upper() + + def evict_entries( + self, + keys, + ): + for key in keys: + self._slots.pop(key, None) diff --git a/test/forcompletefix/py/src/pump/loop.py b/test/forcompletefix/py/src/pump/loop.py new file mode 100644 index 000000000..161554cc3 --- /dev/null +++ b/test/forcompletefix/py/src/pump/loop.py @@ -0,0 +1,21 @@ +class MessagePump: + def __init__(self): + self._pending = [] + self._running = True + + def post(self, message): + self._pending.append(message) + + def run_forever(self): + while self._running: + self._drain() + + def _drain(self): + while self._pending: + message = self._pending.pop(0) + self.deliver(message) + + def deliver(self, message): + handler = getattr(self, "on_" + message.kind, None) + if handler is not None: + handler(message) diff --git a/test/forcompletefix/py/src/pump/resolver.py b/test/forcompletefix/py/src/pump/resolver.py new file mode 100644 index 000000000..4a3191548 --- /dev/null +++ b/test/forcompletefix/py/src/pump/resolver.py @@ -0,0 +1,15 @@ +from pump.cache import LookupCache + + +class Resolver: + def __init__(self): + self.cache = LookupCache() + + def resolve(self, name): + return self.cache.fetch_entry(name) + + +class Downloader: + def pull(self, source, url): + # `source` is an untyped parameter: nothing proves it is a LookupCache. + return source.fetch_entry(url) diff --git a/test/forcompletefix/ts/docs/context.md b/test/forcompletefix/ts/docs/context.md new file mode 100644 index 000000000..813943f6c --- /dev/null +++ b/test/forcompletefix/ts/docs/context.md @@ -0,0 +1,23 @@ +# Context + +The per-request context object. + +## Creating a context + +Each request creates a context. + +## Per-request context state + +The context state is fresh per request. + +## Context and request + +The context wraps the request. + +## Context lifecycle + +A context is created per request and dropped after the response. + +## Request context helpers + +Helpers read the request through the context. diff --git a/test/forcompletefix/ts/package.json b/test/forcompletefix/ts/package.json new file mode 100644 index 000000000..d2d1f2fb0 --- /dev/null +++ b/test/forcompletefix/ts/package.json @@ -0,0 +1 @@ +{ "name": "ctx-fixture", "version": "0.0.0", "private": true } diff --git a/test/forcompletefix/ts/src/application.ts b/test/forcompletefix/ts/src/application.ts new file mode 100644 index 000000000..b58897fac --- /dev/null +++ b/test/forcompletefix/ts/src/application.ts @@ -0,0 +1,23 @@ +import { Context, IncomingRequest } from './context'; + +function freshState(): Record { + return { startedAt: Date.now() }; +} + +export class Application { + private contexts = 0; + + // Build the per-request context object every middleware receives. + createContext( + req: IncomingRequest, + ): Context { + const ctx = new Context( req ); + ctx.state = freshState(); + this.contexts += 1; + return ctx; + } + + handle( req: IncomingRequest ): Context { + return this.createContext( req ); + } +} diff --git a/test/forcompletefix/ts/src/context.ts b/test/forcompletefix/ts/src/context.ts new file mode 100644 index 000000000..d98906ac1 --- /dev/null +++ b/test/forcompletefix/ts/src/context.ts @@ -0,0 +1,15 @@ +import { RequestView } from './request'; + +export interface IncomingRequest { + url: string; + headers: Record; +} + +export class Context { + request: RequestView; + state: Record = {}; + + constructor( req: IncomingRequest ) { + this.request = new RequestView( req ); + } +} diff --git a/test/forcompletefix/ts/src/registry.ts b/test/forcompletefix/ts/src/registry.ts new file mode 100644 index 000000000..626227bb6 --- /dev/null +++ b/test/forcompletefix/ts/src/registry.ts @@ -0,0 +1,7 @@ +export class Registry { + private entries: Record = {}; + + lookup( key: string ): string | undefined { + return this.entries[ key ]; + } +} diff --git a/test/forcompletefix/ts/src/request.ts b/test/forcompletefix/ts/src/request.ts new file mode 100644 index 000000000..883c12d74 --- /dev/null +++ b/test/forcompletefix/ts/src/request.ts @@ -0,0 +1,15 @@ +import type { IncomingRequest } from './context'; + +export class RequestView { + constructor( private raw: IncomingRequest ) {} + + // The host the request context was created for. + get contextHost(): string { + const bag: any = this.raw; + return bag.lookup( 'host' ) ?? ''; + } + + get path(): string { + return this.raw.url; + } +} diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh new file mode 100755 index 000000000..b5db6e0d3 --- /dev/null +++ b/test/forsigspancheck.sh @@ -0,0 +1,352 @@ +#!/usr/bin/env bash +# forsigspancheck.sh — a --for row says where its definition ENDS (e=), the one-hop slots go to top rows whose +# edges are proven, and the defining code outranks the docs that describe it. +# +# test/forsigspancheck.sh # uses build/ripwire on test/forcompletefix +# RIPWIRE_BIN=asan/ripwire test/forsigspancheck.sh +# test/forsigspancheck.sh build_base/ripwire # the RED run (a pre-change binary) +# +# THE GAPS (each seen in graded answers; the fixture paraphrases them as minimal code): +# (1) SPAN. A row carried only l= (the line of the definition's name). "Which body holds line N" — the question a +# reader asks of every gold item that lives INSIDE a function — was not answerable from the row; a peer that +# printed start-end lines got the credit. +# (2) HOP SLOT. The compact answer hop-expands its ranked head in rank order and the first row with ANY resolved +# callee took the slot: an off-topic getter whose one callee row was a call bound by name alone (`bag.lookup()` +# on an untyped local) spent the slot and printed a false edge. +# (3) DOCS FIRST. Markdown headings that repeat the question's words filled the ranked rows above the code that +# defines the answer (on one graded answer about half of the shown rows were headings). +# +# THE CONTRACT. +# (1) Every row of a --for answer whose definition extent is KNOWN carries e="N" right after l=: the 1-based line +# of the definition's LAST line (body-inclusive; a declaration's own last line; e == l for a one-line definition). +# l= keeps its meaning (the NAME's line), so a definition may start above l= (a return type on the line before, +# a decorator). e= is ABSENT — never 0, never a guess — when the extent is not known: a row flagged +# extent_suspect=, a markdown/doc row (not a code body). Other verbs keep their bytes (no e= outside --for). +# (2) A hop slot ( row) goes only to a top-ranked row with at least ONE proven callee edge; a row whose every +# callee edge was bound by name alone gets no row. Rows that keep a slot keep it in rank order. +# (3) When the question does not name docs, no markdown row ranks above the code rows that define the answer, and +# markdown rows are at most a quarter of the shown rows. A question that names docs still gets a docs row +# in its top five (the twin that must hold). +# +# ARMS (one fixture root per language under test/forcompletefix/; each indexed on its own): +# (E) e= values: C Sampler_average l=6 e=14 (return type on the line above the name), Sampler_scale l=3 e=3 +# (a one-line definition), the header prototype Sampler_collectAll l=57 e=57; TS createContext l=11 e=18 (a +# multi-line signature), handle l=20 e=22; Python _compute l=12 e=13 (a decorator above), evict_entries l=15 +# e=20 (a multi-line signature). Oracle on EVERY row that carries e=: l <= e <= the file's line count, and +# the e= line closes the definition (brace languages, unless e == l: the line holds `}` or ends with `;`/`)`; Python: the next +# non-blank line is indented no deeper than the def, or EOF). e= sits right after l=. +# (U) unknown extent: Sampler_window and SampleWindow (extent_suspect="head") and every docs/*.md row carry NO +# e=; every other row carries one (a present e= is never dropped silently either). +# (O) outside --for: --pack-signatures rows and the default map's rows carry no e=. +# (H) hop slots: TS — contextHost (only callee: a name-only `bag.lookup()`) has no row; createContext keeps +# one. Python — Downloader.pull (only callee: a name-only `source.fetch_entry()`) has no row; fetch_entry +# keeps one. C near miss — Sampler_collectAll keeps its row. rows follow the rows' r= order. +# (R) code over docs: TS "How is a per-request context created?" — createContext and the class Context rank above +# every docs/context.md row, and md rows <= 25% of the shown rows. Twin: "Where do the docs describe the +# context lifecycle?" keeps a docs row in its top five. +# (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). +# (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's +# line, not the definition's first line). +# (K) the predicates can fail: a row with e= < l=, a row with e= on an md row, a missing e= on a code row, and a +# name-only hop row are each caught; a document with no --for root is never a pass. Every run must exit 0. +# +# FLOORS — named, not gated here: +# * e= is the parser's span: a definition spliced by the preprocessor or a recovered parse is covered only by +# extent_suspect= (rows with an unflagged but wrong span are not detected). +# * the code-over-docs rule is checked on one fixture; the threshold for "names docs" is the question's wording. +# +# Exits non-zero on any failure. + +set -u +export PYTHONDONTWRITEBYTECODE=1 +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # a gate that indexes a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary +CORPUS="$ROOT/test/forcompletefix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$CORPUS" ] || { echo "fixture missing: $CORPUS"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +echo "forsigspancheck: BIN=$BIN CORPUS=$CORPUS" + +run(){ local r="$1" f="$2"; shift 2; ( cd "$CORPUS/$r" && "$BIN" . --no-cache "$@" >"$f" 2>"$f.err" ); printf '%s' "$?" >"$f.rc"; } +ran_ok(){ local rc; rc="$( cat "$1.rc" 2>/dev/null )"; [ "$rc" = 0 ] && return 0; no "$2: the binary exited rc=${rc:-missing}"; return 1; } +ask(){ local f="$TMP/$1.$2.xml"; run "$1" "$f" "--for=$3"; printf '%s' "$f"; } + +cat >"$TMP/rows.py" <<'PY' +import re, sys +def attrs( tag ): + return dict( re.findall( r'\s([a-z_]+)="([^"]*)"', tag ) ) +def load( path ): + return open( path, encoding="utf-8", errors="replace" ).read() +def ctx_ok( doc ): + return re.search( r"]*>", doc ) is not None and " row, in document order.""" + return [ ( t, attrs( t ) ) for t in re.findall( r"]*>", doc ) ] +def hrows( doc ): + return [ attrs( t ) for t in re.findall( r"]*>", doc ) ] +def is_doc_row( a ): + return a.get( "p", "" ).endswith( ( ".md", ".markdown", ".rst", ".txt" ) ) +PY + +# echeck ANSWER LABEL ROOTDIR SPEC… — SPEC = name:path:l:e (a row n=name p=path l=l must carry e=e) +echeck(){ + local f="$1" label="$2" rootdir="$3"; shift 3 + python3 - "$TMP/rows.py" "$f" "$label" "$rootdir" "$@" <<'PY' +import os, re, sys +exec( open( sys.argv[ 1 ] ).read() ) +path, label, rootdir = sys.argv[ 2: 5 ] +specs = sys.argv[ 5: ] +doc = load( path ) +if not ctx_ok( doc ): + print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) +rows = drows( doc ) +bad = 0 +for spec in specs: + n, p, l, e = spec.rsplit( ":", 3 ) + hit = [ ( t, a ) for t, a in rows if a.get( "n" ) == n and a.get( "p" ) == p and a.get( "l" ) == l ] + if not hit: + print( " FAIL %s: no row (premise)" % ( label, n, p, l ) ); bad = 1; continue + t, a = hit[ 0 ] + if a.get( "e" ) != e: + print( " FAIL %s: %s l=%s carries e=%r, expected %s" % ( label, n, l, a.get( "e" ), e ) ); bad = 1; continue + if not re.match( r' row of one answer +oracle(){ + python3 - "$TMP/rows.py" "$1" "$2" "$3" <<'PY' +import os, re, sys +exec( open( sys.argv[ 1 ] ).read() ) +path, label, rootdir = sys.argv[ 2: 5 ] +doc = load( path ) +if not ctx_ok( doc ): + print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) +bad = [] +checked = 0 +for t, a in drows( doc ): + n, p = a.get( "n" ), a.get( "p", "" ) + unknown = is_doc_row( a ) or "extent_suspect" in a + if unknown: + if "e" in a: bad.append( "%s (%s) carries e= though its extent is unknown" % ( n, p ) ) + continue + if "e" not in a: + bad.append( "%s (%s:%s) has a known extent and no e=" % ( n, p, a.get( "l" ) ) ); continue + if not ( re.fullmatch( r"[0-9]+", a[ "e" ] ) and re.fullmatch( r"[0-9]+", a.get( "l", "" ) ) ): + bad.append( "%s: l=/e= not numeric" % n ); continue + l, e = int( a[ "l" ] ), int( a[ "e" ] ) + try: + lines = open( os.path.join( rootdir, p ), encoding="utf-8", errors="replace" ).read().split( "\n" ) + except OSError: + bad.append( "%s: cannot read %s (premise)" % ( n, p ) ); continue + if lines and lines[ -1 ] == "": lines = lines[ :-1 ] + if not ( 1 <= l <= e <= len( lines ) ): + bad.append( "%s: l=%d e=%d outside 1..%d or e < l" % ( n, l, e, len( lines ) ) ); continue + last = lines[ e - 1 ].rstrip() + if p.endswith( ".py" ): + ind = len( lines[ l - 1 ] ) - len( lines[ l - 1 ].lstrip() ) + nxt = next( ( x for x in lines[ e: ] if x.strip() ), None ) + if not last.strip() or ( nxt is not None and len( nxt ) - len( nxt.lstrip() ) > ind ): + bad.append( "%s: e=%d does not close the def (line %r)" % ( n, e, last ) ) + elif e != l and not ( "}" in last or last.endswith( ";" ) or last.endswith( ")" ) ): + bad.append( "%s: e=%d line %r does not close the definition" % ( n, e, last ) ) + checked += 1 +if bad: + for b in bad: print( " FAIL %s: %s" % ( label, b ) ) + sys.exit( 1 ) +if checked == 0: + print( " FAIL %s: no row carried e= (nothing checked)" % label ); sys.exit( 1 ) +print( " PASS %s: %d e= rows close their definitions; unknown extents carry none" % ( label, checked ) ) +PY + [ $? -eq 0 ] || { fail=1; return 1; } +} + +echo "(E)/(U) e= on rows" +CE="$( ask c avg 'How does the sampler compute the average reading window?' )" +if ran_ok "$CE" "E/C"; then + echeck "$CE" "E/C" "$CORPUS/c" Sampler_average:src/window.c:6:14 Sampler_scale:src/window.c:3:3 Sampler_collectAll:src/monitor.h:57:57 + oracle "$CE" "E+U/C" "$CORPUS/c" + python3 - "$TMP/rows.py" "$CE" <<'PY' +import sys +exec( open( sys.argv[ 1 ] ).read() ) +rows = [ a for _, a in drows( load( sys.argv[ 2 ] ) ) if a.get( "n" ) in ( "Sampler_window", "SampleWindow" ) ] +if len( rows ) < 2 or any( "extent_suspect" not in a for a in rows ): + print( " FAIL U/C: premise — Sampler_window and SampleWindow are not both shown with extent_suspect=" ); sys.exit( 1 ) +if any( "e" in a for a in rows ): + print( " FAIL U/C: an extent_suspect row carries e=" ); sys.exit( 1 ) +print( " PASS U/C: the two extent_suspect rows carry no e=" ) +PY + [ $? -eq 0 ] || fail=1 +fi +TE="$( ask ts ctx 'How is a per-request context created?' )" +if ran_ok "$TE" "E/TS"; then + echeck "$TE" "E/TS" "$CORPUS/ts" createContext:src/application.ts:11:18 handle:src/application.ts:20:22 + oracle "$TE" "E+U/TS" "$CORPUS/ts" +fi +PE="$( ask py compute 'What does LookupCache._compute return, and how are entries evicted?' )" +if ran_ok "$PE" "E/PY"; then + echeck "$PE" "E/PY" "$CORPUS/py" _compute:src/pump/cache.py:12:13 evict_entries:src/pump/cache.py:15:20 + oracle "$PE" "E+U/PY" "$CORPUS/py" +fi +JE="$( ask js run 'How does a queued job get run?' )" +if ran_ok "$JE" "E/JS"; then + oracle "$JE" "E+U/JS" "$CORPUS/js" +fi + +echo "(O) outside --for: no e= on other verbs' rows" +for spec in "packsig:--pack-signatures" "map:"; do + tag="${spec%%:*}"; arg="${spec#*:}"; f="$TMP/c.out.$tag.xml" + if [ -n "$arg" ]; then run c "$f" "$arg"; else run c "$f"; fi + if ran_ok "$f" "O/$tag"; then + if [ -s "$f" ] && grep -q -e ']* e="[0-9]' "$f"; then + ok "O/$tag: rows present, none carries e=" + else + no "O/$tag: no rows to check, or a row carries e= outside --for" + fi + fi +done + +echo "(H) hop slots go to top rows with a proven callee edge" +hopcheck(){ + python3 - "$TMP/rows.py" "$@" <<'PY' +import sys +exec( open( sys.argv[ 1 ] ).read() ) +path, label, mode, name = sys.argv[ 2: 6 ] +doc = load( path ) +if not ctx_ok( doc ): + print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) +hs = hrows( doc ) +names = [ h.get( "n" ) for h in hs ] +if mode == "nohop": + good = name not in names; why = "no (every callee edge name-only)" % name +elif mode == "hop": + good = name in names; why = " kept (a proven callee edge)" % name +elif mode == "order": + rank = { ( a.get( "n" ), a.get( "p" ), a.get( "l" ) ): int( a[ "r" ] ) for _, a in drows( doc ) if a.get( "r", "" ).isdigit() } + rs = [ rank.get( ( h.get( "n" ), h.get( "p" ), h.get( "l" ) ) ) for h in hs ] + good = bool( rs ) and None not in rs and rs == sorted( rs ); why = " rows in r= order %s" % rs +else: + print( " FAIL %s: unknown mode" % label ); sys.exit( 1 ) +print( " %s %s: %s" % ( "PASS" if good else "FAIL", label, why ) ) +sys.exit( 0 if good else 1 ) +PY + [ $? -eq 0 ] || { fail=1; return 1; } +} +if ran_ok "$TE" "H/TS"; then + hopcheck "$TE" "H/TS getter with a name-only edge" nohop contextHost + hopcheck "$TE" "H/TS near miss" hop createContext + hopcheck "$TE" "H/TS order" order - +fi +PH="$( ask py fetch 'How does the lookup cache fetch or evict an entry?' )" +if ran_ok "$PH" "H/PY"; then + hopcheck "$PH" "H/PY untyped-parameter caller" nohop pull + hopcheck "$PH" "H/PY near miss" hop fetch_entry + hopcheck "$PH" "H/PY order" order - +fi +CH="$( ask c collect 'How does the sampler collect probe readings?' )" +if ran_ok "$CH" "H/C"; then + hopcheck "$CH" "H/C near miss" hop Sampler_collectAll +fi + +echo "(R) the defining code outranks the docs that describe it" +if ran_ok "$TE" "R"; then + python3 - "$TMP/rows.py" "$TE" <<'PY' +import sys +exec( open( sys.argv[ 1 ] ).read() ) +rows = [ a for _, a in drows( load( sys.argv[ 2 ] ) ) if a.get( "r", "" ).isdigit() ] +code = [ int( a[ "r" ] ) for a in rows if ( a.get( "n" ), a.get( "p" ) ) in ( ( "createContext", "src/application.ts" ), ( "Context", "src/context.ts" ) ) ] +docs = [ int( a[ "r" ] ) for a in rows if is_doc_row( a ) ] +bad = [] +if len( code ) != 2: bad.append( "premise: createContext and class Context are not both shown (%s)" % code ) +elif docs and max( code ) > min( docs ): bad.append( "a docs row r=%d ranks above defining code r=%d" % ( min( docs ), max( code ) ) ) +if rows and 4 * len( docs ) > len( rows ): bad.append( "%d of %d shown rows are docs (> 25%%)" % ( len( docs ), len( rows ) ) ) +if bad: + for b in bad: print( " FAIL R: %s" % b ) + sys.exit( 1 ) +print( " PASS R: code r=%s above docs r=%s; docs %d of %d rows" % ( code, docs, len( docs ), len( rows ) ) ) +PY + [ $? -eq 0 ] || fail=1 +fi +TD="$( ask ts docs 'Where do the docs describe the context lifecycle?' )" +if ran_ok "$TD" "R twin"; then + python3 - "$TMP/rows.py" "$TD" <<'PY' +import sys +exec( open( sys.argv[ 1 ] ).read() ) +rows = [ a for _, a in drows( load( sys.argv[ 2 ] ) ) if a.get( "r", "" ).isdigit() ] +top = [ a for a in rows if int( a[ "r" ] ) <= 5 and is_doc_row( a ) ] +if not rows or not top: + print( " FAIL R twin: a question that names docs has no docs row in its top five" ); sys.exit( 1 ) +print( " PASS R twin: docs row %s in the top five" % top[ 0 ].get( "n" ) ) +PY + [ $? -eq 0 ] || fail=1 +fi + +echo "(J) --for --json carries the same e= values" +TJ="$TMP/ts.ctx.json"; run ts "$TJ" "--for=How is a per-request context created?" --json +if ran_ok "$TJ" "J"; then + python3 - "$TMP/rows.py" "$TE" "$TJ" <<'PY' +import json, sys +exec( open( sys.argv[ 1 ] ).read() ) +x = { ( a.get( "n" ), a.get( "p" ), a.get( "l" ) ): a.get( "e" ) for _, a in drows( load( sys.argv[ 2 ] ) ) } +try: + j = json.loads( load( sys.argv[ 3 ] ) ) +except ValueError as err: + print( " FAIL J: not JSON (%s)" % err ); sys.exit( 1 ) +js = { ( r.get( "n" ), r.get( "p" ), str( r.get( "l" ) ) ): ( None if r.get( "e" ) is None else str( r.get( "e" ) ) ) for r in j.get( "sigs", [] ) } +common = set( x ) & set( js ) +if not common or not any( x[ k ] for k in common ): + print( " FAIL J: no shared row carries e= (premise)" ); sys.exit( 1 ) +diff = sorted( k for k in common if x[ k ] != js[ k ] ) +if diff: + print( " FAIL J: XML/JSON e= differ on %s" % diff[ :4 ] ); sys.exit( 1 ) +print( " PASS J: %d shared rows agree on e=" % len( common ) ) +PY + [ $? -eq 0 ] || fail=1 +fi + +echo "(L) legend defines e= and what it does not mean" +LG="$TMP/ts.legend.xml"; run ts "$LG" "--for=How is a per-request context created?" --legend=full +if ran_ok "$LG" "L"; then + python3 - "$LG" <<'PY' +import re, sys +legend = " ".join( re.findall( r"", open( sys.argv[ 1 ], encoding="utf-8", errors="replace" ).read(), re.S ) ) +need = [ ( r"\be=", "defines e=" ), ( r"(?i)absent[^.;]*(unknown|not known)", "says absent = unknown" ), + ( r"(?i)(never 0|not 0|never zero)", "says e= is never 0" ), ( r"(?i)l=[^.;]{0,60}(name's line|line of (the|its) (definition's )?name)", "says l= is the name's line" ) ] +bad = [ w for rx, w in need if not re.search( rx, legend ) ] +if bad: + print( " FAIL L: the legend does not %s" % "; ".join( bad ) ); sys.exit( 1 ) +print( " PASS L: e= defined with its NOT-meaning" ) +PY + [ $? -eq 0 ] || fail=1 +fi + +echo "(K) the predicates can fail" +K="$TMP/k.xml" +printf 'x' >"$K" +if ( oracle "$K" "K1 (expected FAIL)" "$CORPUS/ts" ) >/dev/null 2>&1; then no "K1: e= < l= not caught"; else ok "K1: e= < l= is caught"; fi +printf 'xx' >"$K" +if ( oracle "$K" "K2 (expected FAIL)" "$CORPUS/ts" ) >/dev/null 2>&1; then no "K2: e= on a docs row not caught"; else ok "K2: e= on a docs row is caught"; fi +printf 'x' >"$K" +if ( oracle "$K" "K3 (expected FAIL)" "$CORPUS/ts" ) >/dev/null 2>&1; then no "K3: a code row without e= not caught"; else ok "K3: a code row without e= is caught"; fi +printf 'x' >"$K" +if ( hopcheck "$K" "K4 (expected FAIL)" nohop contextHost ) >/dev/null 2>&1; then no "K4: a name-only hop row not caught"; else ok "K4: a name-only hop row is caught"; fi +printf '' >"$K" +if ( oracle "$K" "K5 (expected FAIL)" "$CORPUS/ts" ) >/dev/null 2>&1; then no "K5: NOROOT passed"; else ok "K5: a document with no --for root is never a pass"; fi + +if [ "$fail" -ne 0 ]; then + echo "forsigspancheck: FAIL" + exit 1 +fi +echo "forsigspancheck: PASS" +exit 0 diff --git a/test/forspinecheck.sh b/test/forspinecheck.sh new file mode 100755 index 000000000..aec8b79fc --- /dev/null +++ b/test/forspinecheck.sh @@ -0,0 +1,387 @@ +#!/usr/bin/env bash +# forspinecheck.sh — a --for answer names who DRIVES its top seeds: the nearest proven callers (the upward spine) and the +# tables a seed is stored in, as names and file:line only, in the answer's tail. +# +# test/forspinecheck.sh # uses build/ripwire on test/forcompletefix +# RIPWIRE_BIN=asan/ripwire test/forspinecheck.sh +# test/forspinecheck.sh build_base/ripwire # the RED run (a pre-change binary) +# +# THE GAP. A --for answer ranks the symbols that MATCH the question and, in , what its top rows CALL. Nothing in +# it says what calls them. On a "how does X happen" question the driver is often the missing half: a scan routine is +# named but not the refresh check that runs it from the main loop; a key action is named but not the dispatcher loop +# that reaches it; draw functions are named but not the struct-array table of function pointers that selects them. +# Measured on graded answers (paraphrased here as minimal code): the gold item was the CALLER of a seed, one or two +# hops up, toward a loop or the program entry; a peer that walked call edges upward from its seeds named it. +# +# THE CONTRACT (one element in the --for answer's tail, after the ranked rows; absent when it has no row): +# with rows : +# * n= p= — a caller's name and its definition's file:line (the same spelling --callers prints). +# * of= — the symbol it calls: a top seed (one of the answer's top 1-3 callable rows) or another row's n=. +# A chain is at most 2 hops from a seed. +# * PROVEN EDGES ONLY. A row with no via= is a call edge --callers lists for of=. A call bound by NAME ALONE (no +# receiver evidence: an untyped parameter, a C function-pointer field spelled like a free function) is never a +# plain row: it is absent, or carries via="name". +# * via="value": n= STORES of= as a value (a table slot, an initializer, a field) — REFVAL's row: n= is the +# 's in_id= (the enclosing definition) when it has one, else the root of its into= (the table), at= its +# bind= file:line. Not a call. A module export (module.exports.X / exports.X) is not a table that runs X and is never such a row. +# * NAMES ONLY: no callee claim, no body, no doc, no signature inside ; no attribute outside n p of via at. +# * a caller in a TEST file is not on the runtime path and is never a row. +# The rows of a seed that has none are simply absent (no row says "none"). +# +# ARMS (one fixture root per language under test/forcompletefix/; each indexed on its own): +# (C) C: (C1) scan chain: Sampler_collectAll <- Monitor_refresh <- Monitor_loop (2 hops, the main loop), and the +# test caller test_collect_all is never a row. (C2) dispatcher loop: Board_onKey <- Monitor_loop. +# (C3) struct-array fn-pointer table: Gauge_drawBar/Text/Dots are stored in Gauge_modes[] -> a via="value" +# row n=Gauge_modes at=src/gauge.c:2x. (C4) key table: actionCycleStyle is stored into keyTable[] by +# Keys_bind -> a via="value" row n=Keys_bind (or n=keyTable) at=src/keys.c:22. +# (C5) name-only near miss: `sink->flush( s )` (a function-pointer field) is never a plain caller row of the +# free function flush; the bare same-file call in Monitor_flushSamples IS one (the near miss that must stay). +# (J) JS: (J1) runJob <- JobQueue.drain (the while loop) <- Worker.tick (a constructor-assigned field: proven). +# (J2) relay's `target.drain()` (an untyped parameter) is never a plain row of drain. +# (J3) resizeImage is stored in the `handlers` object literal -> via="value" n=handlers at=lib/queue.js:14; +# runJob's module.exports binding is never a via="value" row. +# (P) Python: (P1) fetch_entry <- Resolver.resolve (self.cache = LookupCache(): proven) <- App.on_lookup. +# (P2) Downloader.pull's `source.fetch_entry( url )` (an untyped parameter) is never a plain row. +# (P3) dispatcher loop: deliver <- _drain <- run_forever. +# (T) TS: (T1) createContext <- Application.handle. +# (S) shape, on EVERY answer above: every plain row is an edge the same binary's --callers= lists (n= and p=), +# every via="value" row is a of --callers= (at= == bind=); rows carry only n p of via at; no , +# , , CDATA inside ; of= chains reach a non- name within 2 hops; shown <= total; +# capped == (shown < total); shown <= 6; the element sits after . +# (D) dialect parity: --for --json carries the same rows (name, path:line, of, via) as the XML answer of (C1). +# (O) outside --for: --callers, --impact, --pack-signatures and the default map never carry / "spine". +# (L) legend: --legend=full defines , of=, via="value" and says what they do NOT mean (not every caller; a +# value row is not a call). +# (K) the predicates can fail: a synthetic answer with a plain name-only row, an answer with a row --callers does not +# list, and an answer with no root are each caught; every binary run must exit 0. +# +# FLOORS — named, not gated here: +# * loops are not detected: "toward a loop or entry" is the 2-hop bound, not a recognised loop statement. +# * a call inside a nested closure/arrow is attributed as the extractor attributes it today (no arm). +# * value rows exist only where REFVAL extracts a binding (its own floors: JS object literals of IMPORTED names). +# * which caller a fan-in > row cap keeps is not pinned (capped=1 discloses the cut). +# +# Exits non-zero on any failure. + +set -u +export PYTHONDONTWRITEBYTECODE=1 +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" # a gate that indexes a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary +CORPUS="$ROOT/test/forcompletefix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$CORPUS" ] || { echo "fixture missing: $CORPUS"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +echo "forspinecheck: BIN=$BIN CORPUS=$CORPUS" + +# run ROOT OUTFILE ARGS… — one binary run against a fixture root; stdout to OUTFILE, its exit status to OUTFILE.rc +run(){ local r="$1" f="$2"; shift 2; ( cd "$CORPUS/$r" && "$BIN" . --no-cache "$@" >"$f" 2>"$f.err" ); printf '%s' "$?" >"$f.rc"; } +# ran_ok FILE LABEL — the run behind FILE exited 0 (a non-zero exit FAILs the arm, whatever it printed) +ran_ok(){ local rc; rc="$( cat "$1.rc" 2>/dev/null )"; [ "$rc" = 0 ] && return 0; no "$2: the binary exited rc=${rc:-missing}"; return 1; } + +# the predicate library — every check reads ONE answer file and prints PASS/FAIL lines itself +cat >"$TMP/spine.py" <<'PY' +import json, re, sys + +def attrs( tag ): + return dict( re.findall( r'\s([a-z_]+)="([^"]*)"', tag ) ) + +def load( path ): + return open( path, encoding="utf-8", errors="replace" ).read() + +def ctx_ok( doc ): + # an answer is only judged when it is a --for document with ranked rows; anything else is NOROOT + return re.search( r"]*>", doc ) is not None and " element, or None when absent.""" + m = re.search( r"]*)?>(.*?)", doc, re.S ) + if not m: + return None + inner = m.group( 2 ) + rows = [ attrs( t ) for t in re.findall( r"]*/>", inner ) ] + return attrs( "" ), rows, inner + +def callers_rows( path ): + """--callers answer -> ({(n, p)}, [vr attrs]) or None when the answer has no matched root.""" + doc = load( path ) + root = re.search( r"]*>", doc ) + if not root or ' defs="' not in root.group( 0 ): + return None + rows = { ( a.get( "n" ), a.get( "p" ) ) for a in ( attrs( t ) for t in re.findall( r"]*/>", doc ) ) } + vrs = [ attrs( t ) for t in re.findall( r"]*/>", doc ) ] + return rows, vrs +PY + +# spinecheck ANSWER LABEL MODE ARGS… — MODE has one of: +# has N OF [P] a plain row n=N of=OF (and p=P when given) is present +# value N OF ATPFX a via="value" row n=N of=OF whose at= starts with ATPFX is present +# notplain N OF no row n=N of=OF without via="name" (absent or hedged) +# absent N no row with n=N at all +# novalue_into PFX no via="value" row whose at=/n= came from an into= starting with PFX (module.exports, exports) +spinecheck(){ + local f="$1" label="$2"; shift 2 + python3 - "$TMP/spine.py" "$f" "$label" "$@" <<'PY' +import sys +exec( open( sys.argv[ 1 ] ).read() ) +path, label, mode = sys.argv[ 2 ], sys.argv[ 3 ], sys.argv[ 4 ] +args = sys.argv[ 5: ] +doc = load( path ) +if not ctx_ok( doc ): + print( " FAIL %s: NOROOT (no --for / answer)" % label ); sys.exit( 1 ) +sp = spine( doc ) +rows = sp[ 1 ] if sp else [] +def plain( r ): return r.get( "via" ) is None +if mode == "has": + n, of = args[ 0 ], args[ 1 ]; p = args[ 2 ] if len( args ) > 2 else None + hit = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and plain( r ) and ( p is None or r.get( "p" ) == p ) ] + good = bool( hit ) + why = "plain " % ( n, of, ( " p=%s" % p ) if p else "" ) +elif mode == "value": + n, of, at = args + hit = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and r.get( "via" ) == "value" and r.get( "at", "" ).startswith( at ) ] + good = bool( hit ) + why = "" % ( n, of, at ) +elif mode == "notplain": + n, of = args + bad = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and r.get( "via" ) not in ( "name", ) ] + good = not bad + why = "no unhedged (name-only edge)" % ( n, of ) +elif mode == "absent": + n = args[ 0 ] + good = not [ r for r in rows if r.get( "n" ) == n ] + why = "no " % n +elif mode == "novalue_into": + pfx = args[ 0 ] + bad = [ r for r in rows if r.get( "via" ) == "value" and ( r.get( "n", "" ).startswith( pfx ) or r.get( "n" ) in ( "module", "exports" ) ) ] + good = not bad + why = "no via=value row from an %s… binding" % pfx +else: + print( " FAIL %s: unknown mode %s" % ( label, mode ) ); sys.exit( 1 ) +state = "PASS" if good else "FAIL" +print( " %s %s: %s%s" % ( state, label, why, "" if sp or good else " [no element]" ) ) +sys.exit( 0 if good else 1 ) +PY + [ $? -eq 0 ] || { fail=1; return 1; } +} + +# shapecheck ROOT ANSWER LABEL — arm (S): every row is backed by the same binary's --callers answer for its of=; names +# only; bounded; placed after . Runs --callers= for each distinct of= (recorded, rc checked). +shapecheck(){ + local r="$1" f="$2" label="$3" + python3 - "$TMP/spine.py" "$f" "$label" "$CORPUS/$r" "$BIN" "$TMP" <<'PY' +import os, re, subprocess, sys +exec( open( sys.argv[ 1 ] ).read() ) +path, label, root, binp, tmp = sys.argv[ 2: 7 ] +doc = load( path ) +bad = [] +if not ctx_ok( doc ): + print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) +sp = spine( doc ) +if sp is None: + print( " FAIL %s: no element to check the shape of" % label ); sys.exit( 1 ) +head, rows, inner = sp +if not rows: + bad.append( "an empty element (it must be absent when it has no row)" ) +for k in ( "shown", "total", "capped" ): + if not re.fullmatch( r"[0-9]+", head.get( k, "" ) ): + bad.append( "%s= missing or not numeric on " % k ) +if not bad: + shown, total, capped = int( head[ "shown" ] ), int( head[ "total" ] ), int( head[ "capped" ] ) + if shown != len( rows ): bad.append( "shown=%d but %d rows" % ( shown, len( rows ) ) ) + if shown > total: bad.append( "shown=%d > total=%d" % ( shown, total ) ) + if capped != ( 1 if shown < total else 0 ): bad.append( "capped=%d disagrees with shown=%d total=%d" % ( capped, shown, total ) ) + if shown > 6: bad.append( "shown=%d > 6 rows" % shown ) +for forbidden in ( " (names only)" % forbidden ) +for r in rows: + extra = set( r ) - { "n", "p", "of", "via", "at" } + if extra: bad.append( "row %s carries %s" % ( r.get( "n" ), sorted( extra ) ) ) + if not re.fullmatch( r".+:[0-9]+", r.get( "p", "" ) ): bad.append( "row %s p=%r is not file:line" % ( r.get( "n" ), r.get( "p" ) ) ) + if r.get( "via" ) not in ( None, "value", "name" ): bad.append( "row %s via=%r" % ( r.get( "n" ), r.get( "via" ) ) ) + if r.get( "via" ) == "value" and not re.fullmatch( r".+:[0-9]+", r.get( "at", "" ) ): bad.append( "value row %s has no at=file:line" % r.get( "n" ) ) +# depth: follow of= through rows (plain/hedged call rows only) to a name that is not a row — at most 2 hops +callerOf = {} +for r in rows: + if r.get( "via" ) != "value": + callerOf.setdefault( r[ "n" ], [] ).append( r[ "of" ] ) +def depth( name, seen ): + if name not in callerOf or name in seen: return 0 + return 1 + max( depth( o, seen | { name } ) for o in callerOf[ name ] ) +for r in rows: + if r.get( "via" ) != "value" and 1 + depth( r[ "of" ], { r[ "n" ] } ) > 2: + bad.append( "row %s is more than 2 hops from a seed" % r[ "n" ] ) +# placement: in the tail, after the ranked rows +if doc.find( "" ): + bad.append( " precedes (it belongs in the answer's tail)" ) +# backing: every row is an edge (or value ref) the same binary's --callers lists for of= +cache = {} +for r in rows: + of = r[ "of" ] + if of not in cache: + out = os.path.join( tmp, "callers.%s.%s.xml" % ( os.path.basename( root ), re.sub( r"[^A-Za-z0-9_]", "_", of ) ) ) + with open( out, "w" ) as fh: + rc = subprocess.run( [ binp, ".", "--no-cache", "--callers=" + of ], cwd=root, stdout=fh, stderr=subprocess.DEVNULL ).returncode + cache[ of ] = ( rc, callers_rows( out ) ) + rc, cr = cache[ of ] + if rc != 0 or cr is None: + bad.append( "--callers=%s did not answer (rc=%d) — row %s unverifiable" % ( of, rc, r[ "n" ] ) ); continue + plainRows, vrs = cr + if r.get( "via" ) == "value": + if not [ v for v in vrs if v.get( "bind" ) == r.get( "at" ) and ( v.get( "in_id" ) == r[ "n" ] or v.get( "into", "" ).split( "[" )[ 0 ].split( "." )[ 0 ] == r[ "n" ] ) ]: + bad.append( "value row %s of=%s at=%s is not a of --callers=%s" % ( r[ "n" ], of, r.get( "at" ), of ) ) + elif ( r[ "n" ], r.get( "p" ) ) not in plainRows: + bad.append( "row %s p=%s is not a caller --callers=%s lists" % ( r[ "n" ], r.get( "p" ), of ) ) +if bad: + for b in bad: print( " FAIL %s: %s" % ( label, b ) ) + sys.exit( 1 ) +print( " PASS %s: %d rows, every row backed by --callers, names only, <= 2 hops, after " % ( label, len( rows ) ) ) +PY + [ $? -eq 0 ] || { fail=1; return 1; } +} + +# ask ROOT TAG QUESTION — run --for and echo the answer path +ask(){ local f="$TMP/$1.$2.xml"; run "$1" "$f" "--for=$3"; printf '%s' "$f"; } + +echo "(C) C: scan chain, dispatcher loop, fn-pointer tables, a name-only field call" +C1="$( ask c q1 'How does the sampler collect probe readings?' )" +if ran_ok "$C1" "C1"; then + spinecheck "$C1" "C1 first hop" has Monitor_refresh Sampler_collectAll src/monitor.c:4 + spinecheck "$C1" "C1 second hop" has Monitor_loop Monitor_refresh src/monitor.c:14 + spinecheck "$C1" "C1 a test caller is not on the runtime path" absent test_collect_all + shapecheck c "$C1" "S/C1" +fi +C2="$( ask c q2 'How does a key press reach the focused board widget?' )" +if ran_ok "$C2" "C2"; then + spinecheck "$C2" "C2 dispatcher loop" has Monitor_loop Board_onKey src/monitor.c:14 + shapecheck c "$C2" "S/C2" +fi +C3="$( ask c q3 'How does a gauge get drawn in each display style?' )" +if ran_ok "$C3" "C3"; then + spinecheck "$C3" "C3 struct-array table holds the bar draw fn" value Gauge_modes Gauge_drawBar src/gauge.c: + shapecheck c "$C3" "S/C3" +fi +C4="$( ask c q4 'Which key action cycles the gauge style and how is it bound?' )" +if ran_ok "$C4" "C4"; then + spinecheck "$C4" "C4 key table binding" value Keys_bind actionCycleStyle src/keys.c:22 + shapecheck c "$C4" "S/C4" +fi +C5="$( ask c q5 'How does the monitor flush buffered samples?' )" +if ran_ok "$C5" "C5"; then + spinecheck "$C5" "C5 a fn-pointer field call is not a proven caller" notplain Sink_drainSamples flush + spinecheck "$C5" "C5 near miss: the bare same-file call stays" has Monitor_flushSamples flush src/sink.c:20 + shapecheck c "$C5" "S/C5" +fi + +echo "(J) JS: a job loop, a constructed field, an untyped parameter, an object-literal handler table" +J1="$( ask js q6 'How does a queued job get run?' )" +if ran_ok "$J1" "J1"; then + spinecheck "$J1" "J1 the drain loop" has drain runJob lib/queue.js:37 + spinecheck "$J1" "J1 a constructor-assigned field is proven" has tick drain lib/worker.js:10 + spinecheck "$J1" "J2 an untyped parameter is not proven" notplain relay drain + shapecheck js "$J1" "S/J1" +fi +J3="$( ask js q7 'How does a resize job pick its handler?' )" +if ran_ok "$J3" "J3"; then + spinecheck "$J3" "J3 object-literal handler table" value handlers resizeImage lib/queue.js:14 + spinecheck "$J3" "J3 an export is not a dispatch table" novalue_into module.exports + shapecheck js "$J3" "S/J3" +fi + +echo "(P) Python: a constructed field, an untyped parameter, a message pump loop" +P1="$( ask py q8 'How does the lookup cache fetch an entry?' )" +if ran_ok "$P1" "P1"; then + spinecheck "$P1" "P1 first hop (self.cache = LookupCache())" has resolve fetch_entry src/pump/resolver.py:8 + spinecheck "$P1" "P1 second hop" has on_lookup resolve src/pump/app.py:10 + spinecheck "$P1" "P2 an untyped parameter is not proven" notplain pull fetch_entry + shapecheck py "$P1" "S/P1" +fi +P3="$( ask py q9 'How does the message pump deliver queued messages?' )" +if ran_ok "$P3" "P3"; then + spinecheck "$P3" "P3 the pump loop, hop 1" has _drain deliver src/pump/loop.py:13 + spinecheck "$P3" "P3 the pump loop, hop 2" has run_forever _drain src/pump/loop.py:9 + shapecheck py "$P3" "S/P3" +fi + +echo "(T) TS" +T1="$( ask ts q10 'How is a per-request context created?' )" +if ran_ok "$T1" "T1"; then + spinecheck "$T1" "T1 createContext's caller" has handle createContext src/application.ts:20 + shapecheck ts "$T1" "S/T1" +fi + +echo "(D) dialect parity: --for --json carries the rows the XML answer of C1 carries" +DJ="$TMP/c.q1.json"; run c "$DJ" "--for=How does the sampler collect probe readings?" --json +if ran_ok "$DJ" "D"; then + python3 - "$TMP/spine.py" "$C1" "$DJ" <<'PY' +import json, sys +exec( open( sys.argv[ 1 ] ).read() ) +x = spine( load( sys.argv[ 2 ] ) ) +try: + j = json.loads( load( sys.argv[ 3 ] ) ) +except ValueError as e: + print( " FAIL D: --for --json is not JSON (%s)" % e ); sys.exit( 1 ) +js = j.get( "spine" ) +xr = sorted( ( r.get( "n" ), r.get( "p" ), r.get( "of" ), r.get( "via" ) ) for r in ( x[ 1 ] if x else [] ) ) +jr = sorted( ( r.get( "n" ), r.get( "p" ), r.get( "of" ), r.get( "via" ) ) for r in ( ( js or {} ).get( "rows" ) or [] ) ) +if not xr: + print( " FAIL D: the XML answer has no spine rows to compare" ); sys.exit( 1 ) +if xr != jr: + print( " FAIL D: JSON spine rows %s != XML rows %s" % ( jr, xr ) ); sys.exit( 1 ) +print( " PASS D: JSON \"spine\" rows equal the XML rows (%d)" % len( xr ) ) +PY + [ $? -eq 0 ] || fail=1 +fi + +echo "(O) outside --for nothing changes shape: no on other verbs" +for spec in "callers:--callers=Sampler_collectAll" "impact:--impact=Sampler_collectAll" "packsig:--pack-signatures" "map:"; do + tag="${spec%%:*}"; arg="${spec#*:}"; f="$TMP/c.out.$tag.xml" + if [ -n "$arg" ]; then run c "$f" "$arg"; else run c "$f"; fi + if ran_ok "$f" "O/$tag"; then + if [ -s "$f" ] && ! grep -q -e '", doc, re.S ) ) +need = [ ( r"" ), ( r"\bof=", "defines of=" ), ( r'via="?value', "defines via=value" ), + ( r"(?i)not (every|all|a complete)[^.;]*caller", "says the rows are not every caller" ), + ( r"(?i)(not a (proven )?call|never a call)", "says a value row is not a call" ) ] +bad = [ what for rx, what in need if not re.search( rx, legend ) ] +if bad: + print( " FAIL L: the legend does not %s" % "; ".join( bad ) ); sys.exit( 1 ) +print( " PASS L: legend defines /of=/via=value with its NOT-meaning" ) +PY + [ $? -eq 0 ] || fail=1 +fi + +echo "(K) the predicates can fail" +K1="$TMP/k1.xml" +printf '' >"$K1" +if ( spinecheck "$K1" "K1 (expected FAIL)" notplain relay drain ) >/dev/null 2>&1; then no "K1: a plain name-only row was not caught"; else ok "K1: a plain name-only row is caught"; fi +K2="$TMP/k2.xml" +printf '' >"$K2" +if ( shapecheck c "$K2" "K2 (expected FAIL)" ) >/dev/null 2>&1; then no "K2: a row --callers does not list was not caught"; else ok "K2: a row --callers does not list is caught"; fi +K3="$TMP/k3.xml"; printf '' >"$K3" +if ( spinecheck "$K3" "K3 (expected FAIL)" absent anything ) >/dev/null 2>&1; then no "K3: a NOROOT document passed"; else ok "K3: a document with no --for root is never a pass"; fi +K4="$TMP/k4.xml" +printf '' >"$K4" +if ( shapecheck c "$K4" "K4 (expected FAIL)" ) >/dev/null 2>&1; then no "K4: a callee claim inside was not caught"; else ok "K4: a callee claim inside is caught"; fi + +if [ "$fail" -ne 0 ]; then + echo "forspinecheck: FAIL" + exit 1 +fi +echo "forspinecheck: PASS" +exit 0 diff --git a/test/regression.sh b/test/regression.sh index 6ebb7fd68..2f4ab2b80 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forspinecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From eefa76310263885bea91171481173438fb083f94 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:40:58 -0400 Subject: [PATCH 016/229] fix(go): a named type's t= comes from its written form, never a blanket struct queries/go/tags.scm captured every type_spec as @definition.type, which defKind maps to SymKind::Struct (the typedef/alias/enum bucket). So `type TestName string` and `type F func(...)` read t="struct", a false claim on every surface that prints t=, and `type A = B` (a type_alias node) was not indexed at all. Three kinds are appended to SymKind: NamedType (t="type": a defined type over a non-struct form, or over another named type), FuncType (t="functype") and Alias (t="alias"). struct_type / interface_type keep t="struct" / t="iface" through their own patterns, which win the dedup on the same name byte. Generic and grouped specs follow the same rule. The new kinds behave as Struct did wherever a predicate reads the kind (model.h isStructOrNamedType): a conversion call keeps its edge, owner and type-like rules are unchanged; only the label moves. The map's --legend=full gains a clause for them only when the corpus has one, so every other corpus keeps its legend bytes. callhierarchy.h's cross_kind bound read ModuleScope+1 and now reads kSymKindCount. Only Go is split: other grammars' @definition.type captures still map to Struct, and the legend clause says so. kParserVer 143 -> 145 (143's full-use file tag is 144), quality.h's mirror and the qschemetrip pin move with it; a lane-local number, the train renumbers. Gate: test/gokindcheck.sh (red on base, 7 arms). Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- queries/go/tags.scm | 14 +- src/abicheck.h | 2 +- src/arch.h | 2 +- src/callhierarchy.h | 6 +- src/didyoumean.h | 2 +- src/extentsuspect.h | 3 +- src/fieldaffinity.h | 2 +- src/graph.h | 22 +-- src/htmlexport.h | 3 + src/ingest_cache.h | 6 +- src/ingest_crawl.h | 15 +- src/ingest_model.h | 6 + src/ingest_sidecap.h | 2 +- src/layout.h | 2 +- src/lsp.h | 6 + src/model.h | 28 +++- src/quality.h | 4 +- src/query.h | 7 +- src/resolve.h | 8 +- src/serialize.h | 17 +++ src/valuerefindex.h | 2 +- test/gokindcheck.sh | 222 ++++++++++++++++++++++++++++ test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 4 + test/regression.sh | 2 +- 29 files changed, 359 insertions(+), 47 deletions(-) create mode 100755 test/gokindcheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 46492fcb1..14a449bf6 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -209,6 +209,7 @@ "gitstampcheck.sh": 84.3, "goinstcheck.sh": 0.2, "gointerfacecheck.sh": 0.4, +"gokindcheck.sh": 0.6, "graphlegendbudgetcheck.sh": 2.5, "graphqueryrefusecheck.sh": 1.9, "grepandcheck.sh": 8.8, diff --git a/README.md b/README.md index 26dd08715..1e1af4329 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-664 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +665 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **664 gate scripts** and is the authoritative list; +`test/regression.sh` names **665 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..30956ac0c 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` | 664 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **664 gate scripts**, all of which exist on disk. +naming **665 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 664. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 665. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..3868b0a5c 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 = [ - ["664 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "664", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/queries/go/tags.scm b/queries/go/tags.scm index 3ecd51013..36a26ac9f 100644 --- a/queries/go/tags.scm +++ b/queries/go/tags.scm @@ -19,8 +19,20 @@ (method_elem name: (field_identifier) @name) @definition.method +;; honesty-small-068: a named type's KIND comes from its `type:` field — the form the grammar parsed — never a blanket +;; "struct". `type TestName string`, `type L[T any] []T`, `type W Other` are defined types (t="type"); `type F func(...)` +;; is a func type (t="functype"); struct_type / interface_type keep the two type_declaration patterns below (their +;; higher dedup specificity wins over this generic one on the same name byte). `type A = B` parses as `type_alias`, a +;; different node, which the generic pattern never matched — so aliases were not indexed at all before; they are t="alias". (type_spec - name: (type_identifier) @name) @definition.type + name: (type_identifier) @name) @definition.definedtype + +(type_spec + name: (type_identifier) @name + type: (function_type)) @definition.functype + +(type_alias + name: (type_identifier) @name) @definition.alias (type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @definition.interface diff --git a/src/abicheck.h b/src/abicheck.h index 5b63d474f..716e9ca9d 100644 --- a/src/abicheck.h +++ b/src/abicheck.h @@ -307,7 +307,7 @@ inline gtl::btree_map> buildCandidates( layo for( const Symbol& s : ctx.ing.symbols ) { - if( s.kind != SymKind::Struct && s.kind != SymKind::Class && s.kind != SymKind::Interface ) + if( !isStructOrNamedType( s.kind ) && s.kind != SymKind::Class && s.kind != SymKind::Interface ) { continue; } diff --git a/src/arch.h b/src/arch.h index 6f587d6a8..a804223cd 100644 --- a/src/arch.h +++ b/src/arch.h @@ -1097,7 +1097,7 @@ inline std::vector computeModuleMetrics( const IngestResult& ing, std::vector totalTypes( M, 0 ), abstractTypes( M, 0 ); for( const Symbol& s : ing.symbols ) { - const bool isType = ( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ); + const bool isType = ( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface ); if( !isType ) { continue; diff --git a/src/callhierarchy.h b/src/callhierarchy.h index 900d43b37..c0892403a 100644 --- a/src/callhierarchy.h +++ b/src/callhierarchy.h @@ -97,7 +97,7 @@ struct CallHierarchyRows // absent) whenever every definition shares one kind, so a same-kind overload set keeps its bytes. inline std::string crossKindValue( const IngestResult& ing, const std::vector& matches ) { - constexpr std::size_t kKinds = std::size_t( SymKind::ModuleScope ) + 1; + constexpr std::size_t kKinds = kSymKindCount; // model.h's compile-time-proven count (was ModuleScope+1: an appended kind tripped the EXPECTS) std::size_t perKind[kKinds] = {}; std::size_t distinct = 0; for( const NodeId m : matches ) @@ -107,7 +107,7 @@ inline std::string crossKindValue( const IngestResult& ing, const std::vector tsContractSignatures( const IngestResult& ing ) { methods.push_back( id ); } - else if( s.kind == SymKind::Class || s.kind == SymKind::Interface || s.kind == SymKind::Struct ) + else if( s.kind == SymKind::Class || s.kind == SymKind::Interface || isStructOrNamedType( s.kind ) ) { owners.push_back( Span{ s.fileId, s.sigStartByte, s.endByte, id } ); } diff --git a/src/didyoumean.h b/src/didyoumean.h index 3794492d8..ec143c4fc 100644 --- a/src/didyoumean.h +++ b/src/didyoumean.h @@ -216,7 +216,7 @@ inline std::string nearestAggregateName( const IngestResult& ing, std::string_vi { const std::string_view best = nearestSymbolNameWhere( ing, typed, []( const Symbol& s ) { - return s.kind == SymKind::Struct || s.kind == SymKind::Class + return isStructOrNamedType( s.kind ) || s.kind == SymKind::Class || s.kind == SymKind::Interface; } ); return best.empty() || best == typed ? std::string() : ( " (did you mean '" + std::string( best ) + "'?)" ); diff --git a/src/extentsuspect.h b/src/extentsuspect.h index bb3a2c9fe..c8550ce65 100644 --- a/src/extentsuspect.h +++ b/src/extentsuspect.h @@ -83,7 +83,8 @@ inline std::string_view extentSuspectReasons( std::uint8_t bits ) noexcept } // ── the two membership tables the rules key on (declarative, one lookup shape) ────────────────────────────── -inline constexpr std::array kExtentClassKinds = { SymKind::Class, SymKind::Struct, SymKind::Interface }; +inline constexpr std::array kExtentClassKinds = { SymKind::Class, SymKind::Struct, SymKind::Interface, + SymKind::NamedType, SymKind::Alias, SymKind::FuncType }; // the Struct split (model.h isStructOrNamedType) inline constexpr std::array kHeadRuleLangs = { Lang::C, Lang::Cpp, Lang::ObjC }; // R3's C family template diff --git a/src/fieldaffinity.h b/src/fieldaffinity.h index 928991214..ab895ab8d 100644 --- a/src/fieldaffinity.h +++ b/src/fieldaffinity.h @@ -325,7 +325,7 @@ inline std::vector modelAllAggregates( layout::ModelCtx& ctx, std::s for( const Symbol& s : ctx.ing.symbols ) { - if( s.kind != SymKind::Struct && s.kind != SymKind::Class && s.kind != SymKind::Interface ) + if( !isStructOrNamedType( s.kind ) && s.kind != SymKind::Class && s.kind != SymKind::Interface ) { continue; } diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..d34ed1e12 100644 --- a/src/graph.h +++ b/src/graph.h @@ -262,7 +262,7 @@ inline bool namespaceCompatible( RefRole role, SymKind kind ) noexcept case RefRole::Type: case RefRole::Extends: { - return kind == SymKind::Class || kind == SymKind::Struct || kind == SymKind::Interface; + return kind == SymKind::Class || isStructOrNamedType( kind ) || kind == SymKind::Interface; } case RefRole::Macro: { @@ -1252,7 +1252,7 @@ inline JavaFieldOwnerGroups buildJavaFieldOwnerGroups( const IngestResult& ing ) const Symbol& owner = ing.symbols[ b.fromSymbol ]; if( owner.lang != Lang::Java || ( owner.kind != SymKind::Class && owner.kind != SymKind::Interface - && owner.kind != SymKind::Struct ) ) + && !isStructOrNamedType( owner.kind ) ) ) { continue; } @@ -1482,7 +1482,7 @@ inline HashMap> buildJavaTypeMembers( const PROFILE_SCOPE_DESCRIBE( "buildGraph/2e: Java Class::method members (issue #74)" ); HashMap> members; const auto isJavaType = []( const Symbol& s ) { - return s.lang == Lang::Java && ( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ); + return s.lang == Lang::Java && ( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface ); }; if( std::ranges::none_of( ing.symbols, isJavaType ) ) { @@ -2537,7 +2537,7 @@ struct BuiltinMethodGate static bool isClassLike( const Symbol& s ) noexcept { - return s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface + return s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface || ( s.lang == Lang::Ruby && s.kind == SymKind::Other ); // a Ruby module, as classNameSet counts it } @@ -3005,14 +3005,14 @@ struct FalseEdgeRules // the functions defined earlier in that body. A METHOD's bare call does not reach its siblings (that needs `self.`). bool callerOwnClass( const Symbol& caller, const Symbol& cand ) const { - const bool callerIsClass = caller.kind == SymKind::Class || caller.kind == SymKind::Struct || caller.kind == SymKind::Interface; + const bool callerIsClass = caller.kind == SymKind::Class || isStructOrNamedType( caller.kind ) || caller.kind == SymKind::Interface; return callerIsClass && !cand.scope.empty() && cand.scope == caller.name; } bool cannotReach( const Reference& r, const Symbol& caller, const Symbol& cand, std::string_view callerDir ) const { if( r.lang == Lang::C ) { - return cand.kind == SymKind::Class || cand.kind == SymKind::Struct || cand.kind == SymKind::Interface; + return cand.kind == SymKind::Class || isStructOrNamedType( cand.kind ) || cand.kind == SymKind::Interface; } if( memberLike( cand ) && !callerOwnClass( caller, cand ) ) { @@ -3959,7 +3959,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.rubyBasesUnscoped = rubyBases.disclosure.unscoped; // ruby_bases_unscoped= (absent at zero) { const auto isClassLikeK = []( SymKind k ) noexcept - { return k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface; }; + { return k == SymKind::Class || isStructOrNamedType( k ) || k == SymKind::Interface; }; PROFILE_SCOPE_DESCRIBE( "buildGraph/2h: CHA-lite inheritance name graph" ); for( const Reference& ir : ing.references ) { @@ -5345,7 +5345,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null for( NodeId typeId : it->second ) { const SymKind k = ing.symbols[ typeId ].kind; - if( k != SymKind::Class && k != SymKind::Struct ) + if( k != SymKind::Class && !isStructOrNamedType( k ) ) { continue; } @@ -6995,7 +6995,7 @@ inline FieldUseAnswer collectFieldUseSites( const IngestResult& ing, FieldId fie return {}; } const Symbol& e = ing.symbols[ encl ]; - const bool classLike = e.kind == SymKind::Class || e.kind == SymKind::Struct || e.kind == SymKind::Interface; + const bool classLike = e.kind == SymKind::Class || isStructOrNamedType( e.kind ) || e.kind == SymKind::Interface; return classLike ? std::string_view( e.name ) : std::string_view( e.scope ); }; const auto fieldTypeOf = [ & ]( std::string_view owner, std::string_view member ) -> std::string_view @@ -7198,7 +7198,7 @@ inline std::string memberSelectorUnservedRefusal( const IngestResult& ing, std:: const std::string_view owner = spec.substr( 0, dot ); for( const Symbol& s : ing.symbols ) { - const bool classLike = s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface; + const bool classLike = s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface; const bool fieldsLang = s.lang == Lang::Cpp || s.lang == Lang::C || s.lang == Lang::Python; // queries/{c,cpp,python}/tags.scm carry @definition.field if( classLike && s.name == owner && !fieldsLang ) { @@ -7503,7 +7503,7 @@ inline QMetrics computeQMetrics( const IngestResult& ing, const Graph& g ) std::string key; for( const Symbol& s : ing.symbols ) { - const bool isClassKind = ( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ); + const bool isClassKind = ( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface ); if( !isClassKind ) { continue; diff --git a/src/htmlexport.h b/src/htmlexport.h index d7e3d2ced..9ae5ed294 100644 --- a/src/htmlexport.h +++ b/src/htmlexport.h @@ -2208,6 +2208,9 @@ inline constexpr const char* kSymShapes[] = { "cross", // Other "bar", // ModuleScope — a file's module scope holds statements, it is not callable: it shares // Section's bar rather than borrowing the function circle it is not. + "square", // NamedType — a type, like class/struct/interface + "square", // Alias + "square", // FuncType }; inline constexpr std::size_t kSymShapeCount = sizeof( kSymShapes ) / sizeof( kSymShapes[0] ); static_assert( kSymShapeCount == kSymKindCount, diff --git a/src/ingest_cache.h b/src/ingest_cache.h index e13079f71..c8be8e7ab 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -302,7 +302,11 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 143; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change + // 145 = lane honesty-small-068 (a lane-local number; the train renumbers): + // Go named types get their kind from the type_spec's form (t="type" / + // "functype"), and `type A = B` is now indexed (t="alias") — new defs and + // new kind bytes. 145, not 144: 143's RICH file tag is 144. // 143 = 2026-10-04 (train 25 review fixes): two extraction changes — // a declaration named like the global object (`var self = this`, a // parameter `window`) is now a JsShadow binding, and a value-reference diff --git a/src/ingest_crawl.h b/src/ingest_crawl.h index 6a5c10a32..04b698462 100644 --- a/src/ingest_crawl.h +++ b/src/ingest_crawl.h @@ -391,7 +391,20 @@ SymKind defKind( std::string_view tail ) noexcept } if( tail == "type" ) { - return SymKind::Struct; // typedef/alias/enum bucket + return SymKind::Struct; // typedef/alias/enum bucket (every grammar but Go's; model.h NamedType note) + } + // honesty-small-068: the Go type_spec split — the grammar's `type:` field PROVES the form, so the kind says it. + if( tail == "definedtype" ) // `type N string`, `type L[T any] []T`, `type W Other` — a defined type, not a struct + { + return SymKind::NamedType; + } + if( tail == "functype" ) // `type F func(...)` + { + return SymKind::FuncType; + } + if( tail == "alias" ) // `type A = B` + { + return SymKind::Alias; } if( tail == "section" ) { diff --git a/src/ingest_model.h b/src/ingest_model.h index 13ad31101..406897861 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -44,6 +44,12 @@ inline void dedupRawDefs( std::vector& rawDefs ) case SymKind::Section: return 0; // each comes from its own single capture (a #define, a heading or data key), so case SymKind::Other: return 0; // neither collides with a code kind on one name byte case SymKind::ModuleScope: return 0; // minted after this pass runs; it can never reach the dedup + // Go's type_spec fires the generic @definition.definedtype AND, for `type F func(...)`, @definition.functype on the + // same name byte (struct/interface specs fire their own pattern too): the form-specific kind must win, and a + // struct/interface must beat the generic defined type. An alias is its own node (type_alias) and never collides. + case SymKind::FuncType: return 2; + case SymKind::NamedType: return 1; + case SymKind::Alias: return 1; } return 0; }; diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 0b3ebb9a7..35809f1b7 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -2254,7 +2254,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // holds an error (fileHasError, one O(1) flag test per file) — see parseRecoveredBits. d.recovered = fileHasError ? parseRecoveredBits( defNode, kind, le.lang, d.scope.empty() ) : std::uint8_t( 0 ); defs.push_back( std::move( d ) ); - if( kind == SymKind::Class || kind == SymKind::Struct || kind == SymKind::Interface ) + if( kind == SymKind::Class || isStructOrNamedType( kind ) || kind == SymKind::Interface ) { captureBases( defNode, fileId, le.lang, src, refs ); // IS-A: inheritance edges (derived → base) captureFields( defNode, fileId, le.lang, src, refs ); // HAS-A: member-variable type edges (S5-E) diff --git a/src/layout.h b/src/layout.h index b3972493d..5aec3715c 100644 --- a/src/layout.h +++ b/src/layout.h @@ -1476,7 +1476,7 @@ inline AggIndex buildAggIndex( const IngestResult& ing ) AggIndex byName; for( const Symbol& s : ing.symbols ) { - if( s.kind == SymKind::Struct || s.kind == SymKind::Class || s.kind == SymKind::Interface ) + if( isStructOrNamedType( s.kind ) || s.kind == SymKind::Class || s.kind == SymKind::Interface ) { byName[ s.name ].push_back( s.id ); } diff --git a/src/lsp.h b/src/lsp.h index 1d657799f..114738178 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -358,6 +358,9 @@ inline int lspKindOf( rw::SymKind k ) noexcept case rw::SymKind::Macro: return 14; // Constant case rw::SymKind::Field: return 8; // Field case rw::SymKind::ModuleScope: return 2; // Module — LSP's own word for a file's top-level scope + case rw::SymKind::NamedType: return 5; // Class — LSP has no defined-type/alias kind; gopls files a non-struct named type here too + case rw::SymKind::Alias: return 5; // Class (as above) + case rw::SymKind::FuncType: return 5; // Class (as above) case rw::SymKind::Other: break; } return 13; // Variable — the neutral bucket @@ -377,6 +380,9 @@ inline const char* lspKindWord( rw::SymKind k ) noexcept case rw::SymKind::Macro: return "macro"; case rw::SymKind::Field: return "field"; case rw::SymKind::ModuleScope: return "module scope"; + case rw::SymKind::NamedType: return "type"; + case rw::SymKind::Alias: return "type alias"; + case rw::SymKind::FuncType: return "function type"; case rw::SymKind::Other: break; } return "symbol"; diff --git a/src/model.h b/src/model.h index cb58e3082..693ee7fb0 100644 --- a/src/model.h +++ b/src/model.h @@ -42,7 +42,7 @@ template using HashMap = ankerl::unordered_dense::map; using NodeId = std::uint32_t; inline constexpr NodeId kNoNode = 0xFFFFFFFFu; -// symbol kind → the terse XML attribute (t="fn|method|cls|struct|iface|var|sec|macro|modscope"). +// symbol kind → the terse XML attribute (t="fn|method|cls|struct|iface|var|sec|macro|modscope|type|alias|functype"). // Macro (the macro-edges round) is APPENDED before Other so no existing kind renumbers: a preprocessor // `#define` definition (@definition.macro — C/C++ preproc_def/preproc_function_def, Rust macro_definition). // Previously the C/Rust captures mapped to Function, which read as a lie on every t= surface; the kind now @@ -64,12 +64,20 @@ inline constexpr NodeId kNoNode = 0xFFFFFFFFu; // mintModuleScopeOwners() appends one per file that has a file-scope call, AFTER the parse cache is released, // so it is the owner a top-level statement or an anonymous callback body attributes to. It is a CALLER, never a // callee: nothing in any language can name ``, so it takes no in-edge and cannot become a rank hub. -enum class SymKind : std::uint8_t { Function, Method, Class, Struct, Interface, Var, Section, Macro, Field, Other, ModuleScope }; +// NamedType / Alias / FuncType (honesty-small-068) are APPENDED after ModuleScope so no existing kind renumbers. They +// split the old `@definition.type` → Struct bucket where a grammar's syntax PROVES the type's form, which today is Go +// alone: `type N string` / `type L[T any] []T` (a defined type over a non-struct, non-interface, non-func form, or over +// another named type) is t="type", `type F func(...)` is t="functype", `type A = B` is t="alias". A Go struct or +// interface keeps t="struct"/t="iface". Every other language's @definition.type capture still maps to Struct (C/C++ +// typedef and enum, TS/Swift/Dart aliases, Java/C#/PHP/GDScript enums, Elixir @type) — a disclosed floor, not a claim +// that those are structs. The three kinds BEHAVE as Struct did everywhere a predicate reads the kind +// (isStructOrNamedType below): only the label changes, so callable/owner/type-like semantics are byte-identical. +enum class SymKind : std::uint8_t { Function, Method, Class, Struct, Interface, Var, Section, Macro, Field, Other, ModuleScope, NamedType, Alias, FuncType }; // The number of SymKind enumerators, and the bound a cached def's kind byte is VALIDATED against on the way // back in (ingest_cache.h ByteR::enumU8). The static_assert is not a restatement: enumCountIsExact asks the // compiler whether the last enumerator is the last NAMED value, so appending a kind without moving this is a // build error rather than a validator that silently refuses the new kind's every cached record. -inline constexpr std::size_t kSymKindCount = static_cast( SymKind::ModuleScope ) + 1; +inline constexpr std::size_t kSymKindCount = static_cast( SymKind::FuncType ) + 1; static_assert( enumCountIsExact(), "kSymKindCount must name the LAST SymKind enumerator — move it with the append" ); inline const char* symTag( SymKind k ) noexcept @@ -87,10 +95,22 @@ inline const char* symTag( SymKind k ) noexcept case SymKind::Field: return "field"; // member variable (id=path::Owner::field; use-sites via --uses=Owner.field) case SymKind::Other: return "other"; case SymKind::ModuleScope: return "modscope"; // the file's module scope: n="", no body to expand + case SymKind::NamedType: return "type"; // Go `type N string`: a defined type, NOT a struct + case SymKind::Alias: return "alias"; // Go `type A = B`: another name for B + case SymKind::FuncType: return "functype"; // Go `type F func(...)`: a named function type } return "other"; // a byte past the enum; a NEW SymKind is a -Werror=switch error above, never a silent "other" } +// The kinds the `@definition.type` bucket mapped to Struct before honesty-small-068 split Go's: every predicate that +// admitted Struct as "a named type" (callable as a conversion, a method owner, a type-like row) admits these too, so +// the split changes the t= LABEL and nothing else. Read this, not `== SymKind::Struct`, wherever the question is +// "is this a struct-or-named-type definition". +inline bool isStructOrNamedType( SymKind k ) noexcept +{ + return k == SymKind::Struct || k == SymKind::NamedType || k == SymKind::Alias || k == SymKind::FuncType; +} + // #324: a ModuleScope owner is SYNTHETIC (comment above the enum) — a CALLER minted after the tags.scm pass, // never something a test (or anything else) can name or invoke, so it can never be discharged as a test // obligation. It is legitimate wherever a CALLER is shown (--callers/--impact/--for already carry the @@ -642,7 +662,7 @@ struct FnLocalScope // (the PR's bodyless interface) and §13. inline bool isDefinitionNotDeclaration( const Symbol& s ) noexcept { - const bool kotlinType = s.lang == Lang::Kotlin && ( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ); + const bool kotlinType = s.lang == Lang::Kotlin && ( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface ); return s.endByte > s.sigEndByte || kotlinType; } diff --git a/src/quality.h b/src/quality.h index 5961d1b00..1ecc478c8 100644 --- a/src/quality.h +++ b/src/quality.h @@ -692,7 +692,7 @@ inline std::vector topLevelCalleeNameHashes( const IngestResult& // which is the direction a deletion candidate must err in. inline bool languageInvokedSymbol( const Symbol& s ) noexcept { - if( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ) + if( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface ) { return true; // a type is declared, never called } @@ -2180,7 +2180,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 143; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/query.h b/src/query.h index 88e43be7e..00ab09a94 100644 --- a/src/query.h +++ b/src/query.h @@ -11,7 +11,7 @@ // EXPR := SOURCE | FILTER | CLOSURE | JOIN // SOURCE := name( "STR" ) symbols whose name == STR (unions same-name defs, like --callers) // | all every symbol (parens optional: `all` or `all()`) -// FILTER := kind( EXPR , KIND ) keep nodes of KIND (fn|method|cls|struct|iface|var|sec|macro|modscope) +// FILTER := kind( EXPR , KIND ) keep nodes of KIND (fn|method|cls|struct|iface|var|sec|macro|modscope|type|alias|functype) // | cx( EXPR , INT ) keep nodes with cyclomatic complexity >= INT // | fanin(EXPR , INT ) keep nodes with in-degree (caller count) >= INT // | file( EXPR , "RE" ) keep nodes whose ROOT-RELATIVE file path (the p= the verb prints) matches the @@ -63,6 +63,9 @@ inline bool kindOfWord( std::string_view w, SymKind& out ) noexcept if( w == "sec" ) { out = SymKind::Section; return true; } if( w == "macro" ) { out = SymKind::Macro; return true; } // macro-edges round: t="macro" is queryable like every other kind if( w == "modscope" ) { out = SymKind::ModuleScope; return true; } // #60: so a query can select the file-scope owners, or not(kind(all,modscope)) them out + if( w == "type" ) { out = SymKind::NamedType; return true; } // honesty-small-068: every t= the map emits is queryable + if( w == "alias" ) { out = SymKind::Alias; return true; } + if( w == "functype" ) { out = SymKind::FuncType; return true; } return false; } @@ -514,7 +517,7 @@ struct Eval SymKind k = SymKind::Other; if( !kindOfWord( kw, k ) ) { - fail( "unknown kind '" + kw + "' (use fn|method|cls|struct|iface|var|sec|macro|modscope)" ); + fail( "unknown kind '" + kw + "' (use fn|method|cls|struct|iface|var|sec|macro|modscope|type|alias|functype)" ); } result = filterKind( std::move( set ), k ); } diff --git a/src/resolve.h b/src/resolve.h index 653e31787..45fcfecb3 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -1494,7 +1494,7 @@ inline HashMap buildElixirModuleIndex( const IngestR for( const Symbol& s : ing.symbols ) { if( s.lang != Lang::Elixir || !s.scope.empty() || s.name.empty() || s.fileId >= F - || ( s.kind != SymKind::Other && s.kind != SymKind::Class && s.kind != SymKind::Struct && s.kind != SymKind::Interface ) ) + || ( s.kind != SymKind::Other && s.kind != SymKind::Class && !isStructOrNamedType( s.kind ) && s.kind != SymKind::Interface ) ) { continue; // only module/protocol/struct containers, never attributes or type declarations } @@ -4668,7 +4668,7 @@ inline void addTypeAliasBases( const IngestResult& ing, HashMap realClassNames; // alias names some class-like symbol at a non-alias site also carries for( const Symbol& s : ing.symbols ) { - const bool classLike = s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface; + const bool classLike = s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface; if( classLike && aliasNames.find( s.name ) != aliasNames.end() ) { siteKey( key, s.fileId, s.line, s.name ); @@ -4701,7 +4701,7 @@ inline HashMap classNameSet( const IngestResult& ing ) for( const Symbol& s : ing.symbols ) { const bool rubyModule = ( s.lang == Lang::Ruby && s.kind == SymKind::Other ); - if( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface || rubyModule ) + if( s.kind == SymKind::Class || isStructOrNamedType( s.kind ) || s.kind == SymKind::Interface || rubyModule ) { classNames.try_emplace( s.name, '\0' ); } @@ -4995,7 +4995,7 @@ inline HashMap> buildUsingReexports( const } const Symbol& cls = ing.symbols[ ur.fromSymbol ]; const std::string_view base = namesplit::stripTemplateArgs( ur.qualifier ); - if( ( cls.kind != SymKind::Class && cls.kind != SymKind::Struct ) || base.empty() || base == ur.calleeName || base == cls.name ) + if( ( cls.kind != SymKind::Class && !isStructOrNamedType( cls.kind ) ) || base.empty() || base == ur.calleeName || base == cls.name ) { continue; // not at class scope, an inheriting constructor, or a class naming itself } diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..90182aa9a 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2365,6 +2365,15 @@ inline constexpr std::string_view kModScopeMapLegend = "-and-an-anonymous-callback-body's-calls-live;a-CALLER-never-a-callee(nothing-in-the-source-can-name-it)-with-no-body-to-expand;" "a-file-with-no-such-call-has-no-such-row -->"; +// honesty-small-068: the three Go named-type kinds' reading, appended ONLY when the corpus has one (the modscope clause's +// byte-identity rule: a corpus without such a row keeps every byte of this legend). What it does NOT mean: t=type is not +// "a struct" (its underlying form is not a struct/interface/func, or is another named type the parse cannot see into), +// and only Go is split — another language's typedef/alias/enum still reads t=struct. +inline constexpr std::string_view kNamedTypeMapLegend = + ""; + // EXTENT HONESTY (src/extentsuspect.h, gate test/extentcheck.sh) — the ONE reading of extent_suspect= on every ROW // surface: the map's , a bundle's and . Written only into a document that carries the attribute, right // where the reader meets it, so a corpus with nothing flagged stays byte-identical. The map adds the header count @@ -2784,6 +2793,12 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto { legend += kModScopeMapLegend; } + // honesty-small-068: the Go named-type kinds' clause, under the same absent-at-zero rule as modscope's above. + if( std::any_of( ing.symbols.begin(), ing.symbols.end(), []( const Symbol& sym ) + { return sym.kind == SymKind::NamedType || sym.kind == SymKind::Alias || sym.kind == SymKind::FuncType; } ) ) + { + legend += kNamedTypeMapLegend; + } std::size_t extentSuspectTotal = 0; for( const Symbol& sym : ing.symbols ) @@ -5639,6 +5654,8 @@ inline std::vector calleeWalkOrder( NodeId id, const std::vector t="struct"; interface_type -> t="iface"; function_type -> t="functype"; +# `type A = B` -> t="alias"; every other form (string, []T, map, chan, *T, [N]T, another named or qualified type, +# a generic over a non-struct form) -> t="type". Generic and grouped `type ( … )` specs follow the same rule. +# The three new kinds BEHAVE as Struct did (model.h isStructOrNamedType): a conversion `TestName( s )` keeps its +# caller edge. Only Go is split; another language's typedef keeps t="struct" (a disclosed floor, arm F). +# +# ARMS +# (A) positive: each non-struct form gets its own kind (type / functype / alias), standalone, generic and grouped. +# (B) near-miss negatives: struct, generic struct, grouped struct, embedded-field struct, interface, grouped +# interface keep t="struct"/t="iface"; a func declaration stays fn, a method on a defined type stays method, +# a package var of func type stays var. +# (C) behaviour kept: the conversion call is still an edge (--callers=TestName lists Convert). +# (D) the cache round-trips the new kind bytes: a warm run equals the --no-cache run byte for byte. +# (E) the legend defines the three kinds under --legend=full on a Go corpus; the graph query can select them. +# (F) scope floor: a C typedef keeps t="struct" and its map carries NO named-type clause (byte-identity rule). +# (G) cross_kind= on --callers counts the new kinds (a name that is a func in one file and a defined type in another) +# without tripping callhierarchy.h's kind bound. +# +# Usage: test/gokindcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } + +mkdir -p "$TMP/g" "$TMP/g2" "$TMP/c" +cat > "$TMP/g/go.mod" <<'EOF' +module example.com/kinds + +go 1.22 +EOF +cat > "$TMP/g/kinds.go" <<'EOF' +package kinds + +type TestName string + +type eventFormatterFunc func(event string) error + +type Pair struct { + A int + B int +} + +type Reader interface { + Read(p []byte) (int, error) +} + +type Alias = Pair + +type List[T any] []T + +type Set[K comparable] map[K]struct{} + +type Box[T any] struct { + v T +} + +type ( + Count int + Handler func() + Lookup map[string]int + Events chan string + PairPtr *Pair + Names []string + Grid [4]int + Inner struct{ X int } + Closer interface{ Close() error } + OldName = Count + Wrapped Pair + Qualified ext.Thing +) + +type Outer struct { + Pair + *Inner + n TestName +} + +var OnEvent func(string) + +func (t TestName) Split() []string { return nil } + +func (f eventFormatterFunc) Format(e string) error { return f(e) } + +func Run() {} + +func Convert(s string) TestName { return TestName(s) } +EOF + +# The map rows, one "t n" pair per line, read off a run that must SUCCEED and produce rows (checklist 14). +MAP="$TMP/map.xml" +"$BIN" "$TMP/g" --no-cache > "$MAP" 2>"$TMP/map.err"; rc=$? +rows="$( grep -o ' "$TMP/callers.xml" 2>&1; rc=$? +if [ "$rc" -eq 0 ] && grep -q 'count="1"' "$TMP/callers.xml" && grep -q ' "$TMP/cold.xml" 2>/dev/null; c1=$? +"$BIN" "$TMP/g" --cache="$CACHE" > "$TMP/warm.xml" 2>/dev/null; c2=$? +if [ "$c1" -eq 0 ] && [ "$c2" -eq 0 ] && [ -s "$CACHE" ] && cmp -s "$MAP" "$TMP/warm.xml" && grep -q 't="functype"' "$TMP/warm.xml"; then + ok "(D) cold and warm runs equal the --no-cache map byte for byte (t=\"functype\" survives the cache)" +else + no "(D) cold rc=$c1 warm rc=$c2; warm differs from --no-cache: $( diff <( tr '>' '\n' < "$MAP" ) <( tr '>' '\n' < "$TMP/warm.xml" ) | head -5 )" +fi + +echo "(E) the legend defines them; the graph query selects them" +"$BIN" "$TMP/g" --no-cache --legend=full > "$TMP/full.xml" 2>&1 +if grep -q '", - ex( reqNote ).c_str(), kindNote.c_str(), symTag( pick.targetKind ), rw::kExemplarSelectionRule ); + "list is complete). Copy its shape, not its text.{} -->", + ex( reqNote ).c_str(), kindNote.c_str(), symTag( pick.targetKind ), rw::kExemplarSelectionRule, + exemplarVia ? rw::forViaNameClause() : std::string() ); // R-E fix (2026-08-19): root= — same reason as --lego above. p= went root-relative in the first R-E // landing with no attribute naming the root, on the one verb whose whole job is "open this file". const std::string exemplarRootAttr = tvSingleRoot ? ( " root=\"" + ex( tvRootArg ) + "\"" ) : std::string(); @@ -3669,7 +3690,7 @@ std::optional runTargetedViews( const MainDispatch& d ) pick.overCcxBar ? " over_ccx_bar=\"1\"" : "" ); packBodies( stdout, ing, { pick.winner }, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, - /*withFileContext=*/false, tvRootArg ); + /*withFileContext=*/false, tvRootArg, /*calleeRank=*/nullptr, exemplarVia ); rw::emitRaw( stdout, "" ); reportRedactions( stderr, redactCounts ); return 0; diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 31bc84c78..21edc8357 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,12 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-04 (lane FE-B, receiver evidence): docdemotegolden_for.xml 5809 -> 6302 B (est_tokens "2328" -> "2521"). +# CAUSE: pagecache.py's `evict_one( self )` names a function its file never imports — Python resolves it nowhere — so +# FE-B binds it by NAME ALONE: the hop's row carries via="name", and the hops block carries the +# via="name" reading as its own comment (graphlegend.h kViaNameLegend). Verified before re-pinning, against the pre-change +# golden: those three lines (the root's est_tokens=, the comment, the row's attribute) are the whole diff — no ranking, +# demotion or route byte moved, and arm (f)'s own assertions still hold. # RE-PIN 2026-09-13 (merge of lane/sc-legend and lane/for-widen): docdemotegolden_for.xml RE-MEASURED on the # MERGED tree at 5,809 B (est_tokens "2328"), from 5,887 on for-widen's tree and 5,425 on sc-legend's. Neither # lane's own number is the merged one, so this is measured, not summed. Three identified changes, and the golden diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 06e898765..d7f19d8a7 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/impactpartitioncheck.sh b/test/impactpartitioncheck.sh index 0c30b38e6..4cb485ed1 100755 --- a/test/impactpartitioncheck.sh +++ b/test/impactpartitioncheck.sh @@ -143,7 +143,9 @@ for name in seed_names: # scope included, so len(rows) == reaches stays the row-count invariance it always was. # 0.6.5 (depth-labelled --impact): a row may carry d= (its hop depth, run-length) between p= and tested=; the # row COUNT this invariance reads is unchanged by it (test/impactdepthcheck.sh gates the depths themselves). - rows = re.findall(r'', doc) + # FE-B: a row reached only through a by-name edge carries via="name" after tested= — it DISCLOSES how the row was + # reached, never whether it is one, so the row count this invariance reads must take it like d= above. + rows = re.findall(r'', doc) reaches = int(attrs.get("reaches", "-1")) if len(rows) != reaches: row_count_ok = False From 975ca8b556a532ad191f2d93d252f9f4b63ed76e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 13:22:34 -0400 Subject: [PATCH 021/229] falseedgecheck: a quoted replace into a sibling root keeps its cross-root edge The sibling of the quoted go.mod replace: resolve.h's cross-root replace aliases read the same goModToken. Root pair goworkspace/{a,b}, indexed together; a's go.mod replaces example.com/wb by "../b" (quoted). Red on the previous binary (no row), green now. Co-Authored-By: Claude Opus 5.5 --- test/falseedgecheck.sh | 8 ++++++++ test/falseedgefix/goworkspace/a/app/app.go | 6 ++++++ test/falseedgefix/goworkspace/a/go.mod | 6 ++++++ test/falseedgefix/goworkspace/b/go.mod | 3 +++ test/falseedgefix/goworkspace/b/lib/lib.go | 4 ++++ 5 files changed, 27 insertions(+) create mode 100644 test/falseedgefix/goworkspace/a/app/app.go create mode 100644 test/falseedgefix/goworkspace/a/go.mod create mode 100644 test/falseedgefix/goworkspace/b/go.mod create mode 100644 test/falseedgefix/goworkspace/b/lib/lib.go diff --git a/test/falseedgecheck.sh b/test/falseedgecheck.sh index aa770b3c3..05a672d61 100755 --- a/test/falseedgecheck.sh +++ b/test/falseedgecheck.sh @@ -261,6 +261,14 @@ exactly goreplace callees app/app.go:UseLegacy "fn Shape legacy/lib/lib.go" exactly goreplace callees quoted/app/app.go:UseQuoted "fn Quote quoted/qlegacy/lib/lib.go" exactly goreplace callees quoted/app/raw.go:UseRaw "fn Raw quoted/qraw/lib/lib.go" lacks goreplace callees quoted/app/remote.go:UseRemote "Quote quoted/qlegacy/lib/lib.go" +# goworkspace/: two roots indexed together; a's go.mod replaces example.com/wb by the SIBLING root ../b, quoted. The +# cross-root alias (resolve.h parseGoModReplaces) reads the path with the same goModToken, so the call keeps its edge. +wsf="$TMP/goworkspace.callees.xml" +( cd "$CORPUS/goworkspace" && "$BIN" a b --no-cache --callees=app/app.go:Use >"$wsf" 2>/dev/null ); wsrc=$? +wsgot="$( rows "$wsf" | tr '\n' ';' | sed 's/;$//' )" +if [ "$wsrc" -ne 0 ]; then no "(goworkspace) --callees=app/app.go:Use over roots a b exited rc=$wsrc" +elif [ "$wsgot" = "fn Far b/lib/lib.go" ]; then ok "(goworkspace) a quoted replace into a sibling root keeps the cross-root edge [fn Far b/lib/lib.go]" +else no "(goworkspace) --callees=app/app.go:Use over roots a b: rows [${wsgot:-none}], want [fn Far b/lib/lib.go]"; fi echo "=== (B) JS: globals, required packages, accessors ===" lacks js callees lib/response.js:length "stringify lib/query.js" diff --git a/test/falseedgefix/goworkspace/a/app/app.go b/test/falseedgefix/goworkspace/a/app/app.go new file mode 100644 index 000000000..92aea7b5e --- /dev/null +++ b/test/falseedgefix/goworkspace/a/app/app.go @@ -0,0 +1,6 @@ +package app + +import "example.com/wb/lib" + +// Use calls into the sibling root a quoted replace maps: a true edge. +func Use(n int) int { return lib.Far(n) } diff --git a/test/falseedgefix/goworkspace/a/go.mod b/test/falseedgefix/goworkspace/a/go.mod new file mode 100644 index 000000000..5b70cc33e --- /dev/null +++ b/test/falseedgefix/goworkspace/a/go.mod @@ -0,0 +1,6 @@ +module example.com/wa + +go 1.22 + +// A quoted replace of a module by a SIBLING root (indexed beside this one): the cross-root alias. +replace "example.com/wb" => "../b" diff --git a/test/falseedgefix/goworkspace/b/go.mod b/test/falseedgefix/goworkspace/b/go.mod new file mode 100644 index 000000000..49301ae03 --- /dev/null +++ b/test/falseedgefix/goworkspace/b/go.mod @@ -0,0 +1,3 @@ +module example.com/wb + +go 1.22 diff --git a/test/falseedgefix/goworkspace/b/lib/lib.go b/test/falseedgefix/goworkspace/b/lib/lib.go new file mode 100644 index 000000000..989b90698 --- /dev/null +++ b/test/falseedgefix/goworkspace/b/lib/lib.go @@ -0,0 +1,4 @@ +package lib + +// Far lives in the sibling root. +func Far(n int) int { return n } From 00b94b5e7124eb34f0aafdd8308356ace8ee637d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 13:36:00 -0400 Subject: [PATCH 022/229] test: qschemetrip re-pinned for kParserVer 145 / kCacheVersion 29 (logged) Co-Authored-By: Claude Opus 5.5 --- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index dc466e816..451f4b12d 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -1d723069b171c4c05a635896aa97b8d2002e0c413ad6fa3a61a13e1b7a695877 +e7817db464abaea1493ac8eb822c8223b200ffa3ba49e4f32086772471d8fc2f diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index d7c381b0e..ee35d04e4 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-04, lane/fe-b-receiver-evidence: RE-DERIVED with UPDATE_GOLDEN=1 (hash e7817db464…71d8fc2f). kParserVer 141 -> 145 +# (above train 25's 143/144; the ref record gains the receiver chain memberPath/memberCtor and the receiver-evidence +# binding kinds), kCacheVersion 28 -> 29; quality.h's mirrors move with them. kQSnapCacheScheme stays 17: a snapshot's meaning is unchanged, and the +# edges it reads are re-derived under the new kParserVer. The train renumbers and re-derives. # 2026-10-04, train 25, extractor fix: RE-DERIVED with UPDATE_GOLDEN=1 (hash d1a2878ce0…44e6c8). No source or scheme # change: extract_fn now reads a candidate's whole signature before deciding it is a prototype, so computeSnapshot's # two-line forward declaration is skipped and its REAL definition is hashed for the first time since the import From 523e0cc7eccbf150fe41fc9fa4e7715e885e766b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:40:05 -0400 Subject: [PATCH 023/229] buildGraph: memoize Rule 3's include narrow per (caller file, name's candidate list) C2 of the self-profile: Narrower::rule3IncludeFile walks a name's WHOLE candidate list with a binary search per candidate over the caller's transitive include set, once per call reference. It is a pure function of (caller file, candidate list, minCands), so a file that calls one name N times paid that walk N times for one answer. On a 1.9k-file C repo the resolve loop made 1.70M such asks over ~150-candidate lists; 98% repeated an earlier (file, list) pair (686k candidates walked instead of 257M). Rule3FileMemo keeps the answers of the current caller file, keyed by the byName entry's address (the resolve loop never inserts into byName), and drops them when a reference of another file arrives; references come file by file, so memory is one file's names. A hit returns the ids the first computation wrote, in the same order. Lists a call builds for itself (reachableByName survivors, a FalseEdgeRules-narrowed list, minCands 1) have no stable address and still call rule3IncludeFile directly. Measured (profile build, 3 runs each, base and head interleaved, machine loaded, so wall is noisy; the scope is the signal): buildGraph "resolve loop" on that 1.9k-file C repo, warm CLI grep: 1.14 s median (0.92-1.18) -> 0.52 s (0.47-0.64); cold map 1.34 s (1.00-1.98) -> 0.52 s (0.52-0.95). Small repos: no change beyond the spread (a 400-file C repo 4.8 -> 6.5 ms, a 538-file TS repo 8.6 -> 7.2 ms, a 2.5k-file C++ tree 141 -> 177 ms). Co-Authored-By: Claude Sonnet 5.5 --- src/graph.h | 10 ++++++++-- src/resolve.h | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..e12194777 100644 --- a/src/graph.h +++ b/src/graph.h @@ -3938,6 +3938,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // whose per-reference tier work dominates the growth cascade by orders of // magnitude. A guessed reserve would be a made-up number in a hot struct. std::vector rule3Out; // reused Rule-3 output buffer (candidates from the single included file) + Rule3FileMemo rule3Memo; // Rule 3's answer per (caller file, byName entry): one walk per name per file, not per call std::string qkey; // reused "qualifier::name" buffer for the E#4 canonical lookup (no per-ref alloc) std::vector locShare; // reused per-candidate localityRank memo (computed once per tier, below) @@ -4502,8 +4503,13 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const rw::SmallVec* nameIds = localsYield ? &reachScratch : baseIds; if( !scipPinned && !canonical && !narrowed && it != byName.end() ) { - if( narrower.rule3IncludeFile( *nameIds, r.fileId, rule3Out, localsYield ? 1u : 2u ) - || ( localsYield && narrower.rule3IncludeFile( *baseIds, r.fileId, rule3Out ) ) ) + // memoized when the list asked about IS the byName entry (no per-call survivor list in play): same answer, asked once + const auto rule3 = [ & ]( const rw::SmallVec& ids, std::size_t minCands ) + { + return ( &ids == &it->second && minCands == 2 ) ? rule3Memo.narrow( narrower, ids, r.fileId, rule3Out ) + : narrower.rule3IncludeFile( ids, r.fileId, rule3Out, minCands ); + }; + if( rule3( *nameIds, localsYield ? 1u : 2u ) || ( localsYield && rule3( *baseIds, 2u ) ) ) { for( NodeId c : rule3Out ) { diff --git a/src/resolve.h b/src/resolve.h index 653e31787..1a0941aac 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -6931,6 +6931,58 @@ struct Narrower } }; +// Rule 3 asked once per (caller file, name's candidate list) instead of once per call. rule3IncludeFile is a pure +// function of (caller file, candidate list, minCands) and walks the WHOLE list with a binary search per candidate over +// the caller's transitive include set, so a file that calls one name N times paid that walk N times for one answer +// (C: thousands of include ids, a name defined in dozens of files). The key is the candidate list's own address — a +// byName entry, which buildGraph's resolve loop never inserts into or reorders — plus the file, held until the next +// reference of ANOTHER file replaces it (references arrive file by file, so one file's answers are all that is ever +// resident; out of order only costs a recompute, never a wrong answer). minCands is fixed at its default (2): the +// lists a call computes per reference (a reachableByName survivor set, a FalseEdgeRules-narrowed list) have no stable +// address and are never memoized. A hit hands back exactly the ids the first computation wrote, in the same order. +struct Rule3FileMemo +{ + struct Answer + { + std::uint32_t begin = 0; // slice of `pool` + std::uint32_t count = 0; + bool fired = false; // rule3IncludeFile's return + }; + std::uint32_t fileId = kNoFile; // the caller file the answers below are for + HashMap*, Answer> answers; + std::vector pool; // every fired answer's survivor ids, back to back + + // == narrower.rule3IncludeFile( cands, callerFileId, out ): true iff it narrowed (then `out` holds the survivors); + // false leaves `out` untouched. + bool narrow( const Narrower& narrower, const rw::SmallVec& cands, std::uint32_t callerFileId, std::vector& out ) + { + if( callerFileId != fileId ) + { + fileId = callerFileId; + answers.clear(); + pool.clear(); + } + const auto [ it, fresh ] = answers.try_emplace( &cands ); + Answer& ans = it->second; + if( fresh ) + { + ans.fired = narrower.rule3IncludeFile( cands, callerFileId, out ); + if( ans.fired ) + { + ans.begin = static_cast( pool.size() ); + ans.count = static_cast( out.size() ); + pool.insert( pool.end(), out.begin(), out.end() ); + } + return ans.fired; + } + if( ans.fired ) + { + out.assign( pool.begin() + ans.begin, pool.begin() + ans.begin + ans.count ); + } + return ans.fired; + } +}; + // ── C++ template families: the canonical tier's fallback when a template-id qualifier keys no definition ────────── // Ingest keys a primary template's out-of-line member by the bare template name and a specialization by its canonical // template-id (ingest_names.h); a specialization header's base clause arrives as an inherit ref whose derived name is From cd02c90312193f3df47e1afc17cc2885c904a82b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:47:37 -0400 Subject: [PATCH 024/229] --for/--metrics/--exemplar: run the 18-month history walk beside the ingest C4 of the self-profile: the amp=/churn= block after the graph build ran the history walk (gitHeadSha, the window-boundary probe, the HEAD-epoch probe and, on a cold qchurn blob, one `git log --name-only` over 18 months) strictly after the ingest, although none of it reads anything the ingest produces. Only the final step, resolving paths to file ids, needs `ing`. quality::gitCoChangeAndChurnCached is split in two: gitRawCommitStreamCached (the git probes, blob read and cold walk; no ingest) and gitCoChangeAndChurnFromRaw (the HEAD-anchored churn cutoff and the resolve against `ing`); the Cached form is now their composition, same signature and result. main.cpp starts one std::async walk per root just before the ingest when one of the three verbs will mine history (not --help-task, which answers in between) and hands each future's stream to the resolve step. The thread only touches mutex-guarded memos and an atomically renamed blob; a run that stops before the block still joins its walks. Output is unchanged: the same raw stream is resolved against the same ingest. Measured (profile build, 3 runs each, previous commit vs this, interleaved, machine loaded): main-thread "computeQMetrics + change-amplification" scope, cold blob: a 538-file TS repo 422 ms -> 85 ms, ripwire's own 2.5k-file tree 3.87 s -> 0.80 s, a 400-file C repo 3.5 s -> 3.0 s (git walk longer than its ingest there); warm blob: 343 -> 173 ms (C repo), 350 -> 116 ms (own tree). Whole-process wall for --for, cold: TS repo 1.27 s -> 1.02 s, own tree 7.2 s -> 4.4 s; warm C repo 0.83 s -> 0.65 s. Peak RSS unchanged. Co-Authored-By: Claude Sonnet 5.5 --- src/gitmine.h | 8 +++--- src/gitstamp.h | 2 +- src/main.cpp | 28 ++++++++++++++++++-- src/quality.h | 62 ++++++++++++++++++++++++++++++++------------- src/verbs_for.h | 2 +- test/qchurncheck.sh | 4 +-- 6 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/gitmine.h b/src/gitmine.h index 44352c90d..cee6a5b88 100644 --- a/src/gitmine.h +++ b/src/gitmine.h @@ -450,7 +450,7 @@ inline std::string sinceLogArgs( const SinceScope& scope, const char* fallbackSi // repo 43 of 1028 paths moved, ALL upward, none downward, and 2 appeared for the first time. // // Cost: 0.103 s → 0.148 s for the whole-history walk on this repo (1133 commits, 133 merges) — one popen, -// and the expensive walk is memoized (quality::gitCoChangeAndChurnCached; its cache SCHEME is bumped with +// and the expensive walk is memoized (quality::gitRawCommitStreamCached; its cache SCHEME is bumped with // this change, since a warm blob written by a merge-blind binary would otherwise keep serving the old stream). // // `-c` after `log` is git log's combined-diff option; it is NOT the `git -c core.quotepath=false` config @@ -1550,7 +1550,7 @@ inline std::int64_t approxMonthsAgoEpoch( unsigned months ) // The RAW, per-commit (epoch, changed-paths) stream from ONE `git log --name-only` walk // over `coSince`: the parsed-but-UNRESOLVED form gitCoChangeAndChurn needs. Split out of that function so -// it can be MEMOIZED (quality::gitCoChangeAndChurnCached) independent of any particular ingest's fileId +// it can be MEMOIZED (quality::gitRawCommitStreamCached) independent of any particular ingest's fileId // space — paths stay raw repo-relative strings here; resolveCommitStream (below) resolves them against a // CALLER-supplied `ing` fresh on every call. Committed-history-only by construction (git log never sees // working-tree state), which is exactly what makes this cacheable: two calls with the SAME (repo, coSince) @@ -1629,7 +1629,7 @@ inline std::string gitWindowBoundarySha( const std::string& root, const std::str // Resolve a RawCommitStream against the CURRENT `ing` (+ `onlyRoot`) into the same (sets, churn) shape // gitCoChangeAndChurn has always returned — pure in-memory work (basename/suffix matching + a churn tally), -// no subprocess. Split out (Y2) so the cached path (quality::gitCoChangeAndChurnCached) and the uncached +// no subprocess. Split out (Y2) so the cached path (quality::resolveHistoryWalk) and the uncached // path (gitCoChangeAndChurn, below) share ONE resolver instead of two copies that could drift. inline std::vector> resolveCommitStream( const RawCommitStream& raw, const IngestResult& ing, std::size_t maxFiles, @@ -1704,7 +1704,7 @@ inline std::vector> resolveCommitStream( // // This UNCACHED form is now a thin raw-walk + resolve composition (below) — kept as-is for // any caller that doesn't want the memoized path. The two rich-verb call sites (main.cpp's --metrics/--for -// amp=/churn= computation) go through quality::gitCoChangeAndChurnCached instead, which memoizes exactly +// amp=/churn= computation) go through quality::gitRawCommitStreamCached + resolveHistoryWalk instead, which memoize exactly // the expensive part (gitLogNameOnlyRaw) this function also calls. inline std::vector> gitCoChangeAndChurn( const std::string& root, const IngestResult& ing, const char* coSince, std::size_t maxFiles, diff --git a/src/gitstamp.h b/src/gitstamp.h index 1dd34b9c6..8a8f0f41f 100644 --- a/src/gitstamp.h +++ b/src/gitstamp.h @@ -111,7 +111,7 @@ inline std::string stampAt( const std::string& root ) // retrieval path is contractually git-free on a git-less corpus (test/nongitqmetricscheck.sh: "rich // retrieval on a non-git root must not spawn git merely to degrade"), and stampAt then paid TWO popens to // learn there was no repo. hasEnclosingGitRepo is the same filesystem probe (stat for .git, walking up) - // that quality::gitCoChangeAndChurnCached already gates its own walk on: it costs no subprocess, changes + // that quality::gitRawCommitStreamCached already gates its own walk on: it costs no subprocess, changes // no answer — on a non-git root the result was always "" — and makes the property hold for EVERY stamped // verb rather than for whichever one a gate happened to be watching. if( !rw::hasEnclosingGitRepo( root ) ) diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..86e14feb8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "infra/emit.h" // rw::emitTo / emitRaw / formatTo — THE emitter and its siblings #include "gitcmd.h" // rw::gitCmd — every git child starts with --no-optional-locks -c core.fsmonitor=false #include // %.*s (precision, pointer) collapses to one view +#include // std::async — the history walk that runs beside the ingest // main.cpp — ripwire entry point: parse args → ingest → graph → PageRank → minified XML, // plus --map-diff (teleport toward git-changed files) and --pack-top-n (append source). @@ -3696,6 +3697,26 @@ static std::string_view scipIndexUnreadableReason( const std::string& scipPath ) return {}; } +// --for / --metrics / --exemplar mine 18 months of history after the graph build (the amp=/churn= block in +// dispatchMain). That walk is `git` subprocesses plus a blob read and touches nothing the ingest produces, so it +// starts BEFORE the ingest, one thread per root, and runs beside it; the amp=/churn= block only resolves each walk's +// paths against `ing`. Empty when no such verb runs — and for --help-task, the one verb that answers between the +// two, because a run that stops before the block still waits for its walks (a std::async future joins). +static std::vector> startHistoryWalks( const rw::Config& cfg, bool multiRoot, + const std::vector& ws, const std::string& root ) +{ + std::vector> walks; + if( ( !cfg.metrics && cfg.forTask.empty() && cfg.exemplar.empty() ) || !cfg.helpTask.empty() ) + { + return walks; + } + for( std::size_t r = 0; r < ( multiRoot ? ws.size() : 1u ); ++r ) + { + walks.push_back( std::async( std::launch::async, [ rootArg = multiRoot ? ws[r].arg : root ]() { return rw::quality::gitHistoryWalk( rootArg ); } ) ); + } + return walks; +} + static int dispatchMain( const rw::Config& cfg, char** argv ); // The key for a SHARED root (`r` = the map family, `ctx` = the bundle family) is read off the ANSWER, never off a flag order. @@ -5034,6 +5055,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) // confident, wrong zero; --quality-panel builds two of its six families out of exactly those two lenses. const bool needsValueUses = rw::needsValueUses( cfg ); // cli.h — ONE definition, and --doctor's // rich_verbs= roster is derived from it + std::vector> historyWalks = startHistoryWalks( cfg, multiRoot, ws, root ); IngestResult ing; if( multiRoot ) { @@ -5224,6 +5246,8 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) // churn work, sets byte-identical to the old gitCommitFileSets("18 months ago", 30). // Multi-root (§5): one popen PER root, each resolved only against its own files; the per-commit // sets concatenate (commits are disjoint across repos) and churn accumulates per file. + ASSUME( historyWalks.size() == ( multiRoot ? ws.size() : 1u ), + "this block runs under startHistoryWalks' condition (--help-task has already answered), so every root's walk was started" ); std::vector> commits; if( multiRoot ) { @@ -5233,7 +5257,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) // Y2: the memoized form — skips the 431 ms `git log --name-only` walk on a // warm (repo, HEAD sha, window, boundary-sha) hit; see quality.h's qchurn family. std::vector> part = - quality::gitCoChangeAndChurnCached( ws[r].arg, ing, "18 months ago", 30, + quality::resolveHistoryWalk( historyWalks[ r ].get(), ing, cfg.forTask.empty() ? 0u : 12u, cfg.forTask.empty() ? nullptr : &rootChurn, r ); for( std::vector& c : part ) @@ -5256,7 +5280,7 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) else { // Y2: memoized — see the multi-root branch above. - commits = quality::gitCoChangeAndChurnCached( root, ing, "18 months ago", 30, + commits = quality::resolveHistoryWalk( historyWalks[ 0 ].get(), ing, cfg.forTask.empty() ? 0u : 12u, cfg.forTask.empty() ? nullptr : &forChurn ); } diff --git a/src/quality.h b/src/quality.h index 5961d1b00..8404319b6 100644 --- a/src/quality.h +++ b/src/quality.h @@ -4406,29 +4406,25 @@ inline bool deserializeRawCommitStream( const std::string& blob, const std::stri return true; } -// The memoized drop-in for gitCoChangeAndChurn: same signature, same return contract, but the expensive -// `git log --name-only` walk is skipped on a warm hit (blob self-validates against `keyMat`; any mismatch — -// wrong headSha, wrong coSince, drifted boundary, or a corrupt/foreign blob — is a clean miss that falls -// through to a full recompute, never a wrong answer). No git repo / no resolvable HEAD degrades straight to -// the uncached walk (which itself degrades to an empty stream — gitLogNameOnlyRaw's own contract). -inline std::vector> gitCoChangeAndChurnCached( - const std::string& root, const IngestResult& ing, const char* coSince, std::size_t maxFiles, - unsigned churnMonths = 0, std::vector* outChurn = nullptr, - std::uint32_t onlyRoot = UINT32_MAX ) +// The memoized raw stream, WITHOUT any ingest: the git probes, the qchurn blob and, on a miss, the walk — everything +// the amp=/churn= computation needs before it resolves paths against an `ing`. It reads nothing but the repo's committed +// history and the blob, so a caller that wants the walk's wall time hidden behind its own ingest starts it first +// (main.cpp does, for --for/--metrics/--exemplar) and resolves the result once `ing` exists. A cache hit skips the walk +// (blob self-validates against `keyMat`; any mismatch — wrong headSha, wrong coSince, drifted boundary, or a +// corrupt/foreign blob — is a clean miss that falls through to a full recompute, never a wrong answer). No git repo +// degrades to an empty stream; no resolvable HEAD to the uncached walk (which itself degrades to an empty stream — +// gitLogNameOnlyRaw's contract). Thread-safe: every memo it reaches (the HEAD anchor, the toplevel) takes its own +// mutex, and the blob write is an atomic rename. +inline RawCommitStream gitRawCommitStreamCached( const std::string& root, const char* coSince ) { if( !hasEnclosingGitRepo( root ) ) { - return resolveCommitStream( RawCommitStream{}, ing, maxFiles, /*churnCutoff=*/0, outChurn, onlyRoot ); + return RawCommitStream{}; } - // F1: the churn sub-window's cutoff, resolved ONCE from HEAD's own committer epoch — the same anchor the - // co-change window (inside gitLogNameOnlyRaw) uses, so the two horizons this one walk yields agree. - // AFTER the no-repo check, so a non-git root never reaches the anchor read at all. - const std::int64_t churnCutoff = rw::defaultWindowCutoffEpoch( root, churnMonths ); - const std::string headSha = gitHeadSha( root ); if( headSha.empty() ) { - return resolveCommitStream( gitLogNameOnlyRaw( root, coSince ), ing, maxFiles, churnCutoff, outChurn, onlyRoot ); + return gitLogNameOnlyRaw( root, coSince ); } const std::string repoHex = cacheRootKeyHex( root ); @@ -4443,13 +4439,43 @@ inline std::vector> gitCoChangeAndChurnCached( const std::optional blob = readQSnapBlob( cachePath ); if( blob && deserializeRawCommitStream( *blob, keyMat, raw ) ) { - return resolveCommitStream( raw, ing, maxFiles, churnCutoff, outChurn, onlyRoot ); // warm hit — no walk + return raw; // warm hit — no walk } raw = gitLogNameOnlyRaw( root, coSince ); // cold — the 431 ms walk atomicWriteFile( cachePath, serializeRawCommitStream( raw, keyMat ) ); // best-effort; a failed // write just recomputes next time - return resolveCommitStream( raw, ing, maxFiles, churnCutoff, outChurn, onlyRoot ); + return raw; +} + +// One root's amp=/churn= history, fetched without an ingest: the root it was walked for and its raw 18-month stream. +// main.cpp starts one per root beside the ingest and resolves each against `ing` afterwards. +struct HistoryWalk +{ + std::string root; + RawCommitStream raw; +}; + +inline HistoryWalk gitHistoryWalk( std::string root ) +{ + RawCommitStream raw = gitRawCommitStreamCached( root, "18 months ago" ); + return HistoryWalk{ std::move( root ), std::move( raw ) }; +} + +// The resolve half, the memoized drop-in for gitCoChangeAndChurn on the 18-month window main.cpp's amp=/churn= block +// reads (commits with more than 30 files dropped from the sets): `walk` resolved against the CALLER's current `ing`. +// Pure in-memory work except the HEAD anchor's memo (one `git log -1` the first time a root asks); a root with no +// enclosing repo never reaches that anchor at all. +inline std::vector> resolveHistoryWalk( + const HistoryWalk& walk, const IngestResult& ing, unsigned churnMonths = 0, + std::vector* outChurn = nullptr, std::uint32_t onlyRoot = UINT32_MAX ) +{ + constexpr std::size_t kMaxFilesPerCommit = 30; + // F1: the churn sub-window's cutoff, resolved ONCE from HEAD's own committer epoch — the same anchor the + // co-change window (inside gitLogNameOnlyRaw) uses, so the two horizons this one walk yields agree. + // AFTER the no-repo check, so a non-git root never reaches the anchor read at all. + const std::int64_t churnCutoff = hasEnclosingGitRepo( walk.root ) ? rw::defaultWindowCutoffEpoch( walk.root, churnMonths ) : 0; + return resolveCommitStream( walk.raw, ing, kMaxFilesPerCommit, churnCutoff, outChurn, onlyRoot ); } // `root` = the ingest root exactly as invoked (cfg.rootPath). It is folded into every baseline key via diff --git a/src/verbs_for.h b/src/verbs_for.h index dfe6a6967..6186eb856 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2508,7 +2508,7 @@ std::optional runForLens( const MainDispatch& d ) // where budget_bytes= goes and for its reason (see there). // M10: --for reads git for the per-file churn= column (folded onto the bundle below, mined once in - // main.cpp's gitCoChangeAndChurnCached pass) and, before this fix, carried no anchor — an agent + // main.cpp's resolveHistoryWalk pass) and, before this fix, carried no anchor — an agent // quoting churn= into a handoff had nothing checkable to pin it to. Single-root only (same gate // flRootArg itself uses): "" on multi-root or a non-git root, same silence-means-omitted convention // every other stamped verb follows. ALWAYS present when non-empty, like confidence= — a root fact, diff --git a/test/qchurncheck.sh b/test/qchurncheck.sh index 3800e0ae9..481ba8264 100755 --- a/test/qchurncheck.sh +++ b/test/qchurncheck.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# qchurncheck.sh — gate for Y2 (P2): quality::gitCoChangeAndChurnCached memoizes gitmine's +# qchurncheck.sh — gate for Y2 (P2): quality::gitRawCommitStreamCached memoizes gitmine's # `git log --name-only` walk (431 ms on a large private C++ corpus; every rich verb — --for, --metrics, # --exemplar — pays it once per invocation, main.cpp:5392/5404 call it via gitmine::gitCoChangeAndChurn). # @@ -43,7 +43,7 @@ echo "qchurncheck: BIN=$BIN" qchurnfiles(){ find "$QCACHE" -maxdepth 2 -type f -name 'ripwire-qchurn-*.bin' 2>/dev/null; } nqchurn(){ qchurnfiles | wc -l | tr -d ' '; } # name_only_count TRACEFILE — how many git child processes in this run's trace invoked --name-only (the -# expensive walk gitCoChangeAndChurnCached guards). Any OTHER git call this run makes (rev-parse, HEAD +# expensive walk gitRawCommitStreamCached guards). Any OTHER git call this run makes (rev-parse, HEAD # resolution, etc.) never contains "name-only", so this is a clean, argv-based signal — not a timing guess. name_only_count(){ grep -c "name-only" "$1" 2>/dev/null || true; } # run TRACEFILE ARGS... — invokes ripwire against $REPO with our private TMPDIR + a fresh GIT_TRACE file. From 6be99a5b76262a85406fd2dd86bca4acd89ac074 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:08:17 -0400 Subject: [PATCH 025/229] =?UTF-8?q?test(gate):=20countfloorcheck=20?= =?UTF-8?q?=E2=80=94=20a=20count=20the=20index=20cannot=20vouch=20for=20is?= =?UTF-8?q?=20a=20floor=20beside=20the=20number=20(red=20on=20base)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A zero printed as a total reads as "nothing uses this". Graded answers called eight such numbers false: a grep callers="0" on a method whose only call the resolver declined to bind, the same on a module variable another file reads, --safe-delete callers/uses="0" risk="none-found" on a C struct whose type mentions are never indexed, and the --for bundle's implementors= counting the rows it LISTED after narrowing (one of six). --safe-delete caller rows also carried the caller's definition line where an edit needs the call line. Arms (temp corpora, no git): - A/E: a C struct and a Python module variable floor callers= and uses=, risk="unmodelled", and the next= / floor_next= clue reaches the use. - F/G/H: a declined call, an unbound same-name call and a value use each floor a grep callers= count. - B/I/J (near-miss negatives): a static function nobody calls keeps callers="0" risk="none-found" dead_code_candidate="1"; a fully bound count and a markdown heading's zero stay plain. - C/D: safe-delete caller rows carry sites_l= call lines beside p=; a caller with three calls lists three, a single-definition callee called once still lists its line. - K/L: the bundle's implementors= is the lego verb's count, a narrowed list adds implementors_shown=/implementors_next=; an extends clause bound nowhere adds implementors_floor= on the bundle and the targeted verb. - M: MCP grep enclosing rows and the MCP for block say the same. - N: every new attribute is defined in its own answer, compact and full. - O: determinism and well-formed XML. Gate count regenerated by docs/gatecount_build.py (665). Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/countfloorcheck.sh | 338 ++++++++++++++++++++++++++++ test/regression.sh | 2 +- 6 files changed, 348 insertions(+), 9 deletions(-) create mode 100644 test/countfloorcheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 46492fcb1..bb5c1d180 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -93,6 +93,7 @@ "constcheck.sh": 2.7, "contextratiocheck.sh": 2.8, "coplintcheck.sh": 0.9, +"countfloorcheck.sh": 14.0, "cppbenchcheck.sh": 170.6, "cppoperatorcheck.sh": 5.5, "cppqualcheck.sh": 36.1, diff --git a/README.md b/README.md index 26dd08715..1e1af4329 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-664 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +665 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **664 gate scripts** and is the authoritative list; +`test/regression.sh` names **665 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..30956ac0c 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` | 664 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **664 gate scripts**, all of which exist on disk. +naming **665 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 664. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 665. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..3868b0a5c 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 = [ - ["664 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "664", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh new file mode 100644 index 000000000..5a352d74d --- /dev/null +++ b/test/countfloorcheck.sh @@ -0,0 +1,338 @@ +#!/usr/bin/env bash +# countfloorcheck.sh — a ZERO (or any count) the index cannot know is complete is printed as a FLOOR beside the +# number, with the call that finds the rest; a count the index CAN vouch for stays a plain total. Plus the +# call-site line on a --safe-delete caller row. +# +# WHY. Graded answers called eight of this tool's printed counts false claims, and every one of them was a +# number the graph really held, read as a total: +# * `` (grep) while six calls the resolver DECLINED to bind named it; +# * `` (grep) about a module variable that another file READS — a variable is +# never called, so a call count is blind to its every use; +# * `--safe-delete=` answering callers="0" uses="0" risk="none-found" about a C struct whose type +# mentions the index does not capture at all (thirty files use it); +# * `` (the --for bundle) on a tree with six — the bundle printed the +# number of rows it LISTED after narrowing to its own files, not how many implementors there are; +# * and --safe-delete's caller rows carried the caller's DEFINITION line where the edit needs the CALL line. +# A root's counts_floor="1" and a legend's "a zero means none found" are blanket sentences far from the number; +# the reader takes the number. The marker now sits beside it, only where the index holds evidence of a miss. +# +# Arms (temp corpora, no git; every positive has a near-miss negative the new code could plausibly mishandle): +# A C struct, safe-delete: callers_floor + uses_floor + risk="unmodelled" + next= the literal grep; +# and the clue LEADS to the uses (the grep names the file that declares a `struct cell`). +# B (negative) a static C function nobody calls: a provably complete zero stays callers="0" risk="none-found", +# dead_code_candidate="1", no floor marker, no next=. +# C CALLSITE-LINE: --safe-delete=width_of rows carry sites_l= (the call lines) beside p= (the definition line): +# a caller with three calls on three lines lists all three; one call split over two lines lists its line. +# D (near-miss) a single-definition symbol called once still prints its call site (sites_l= present, one line). +# E grep on a Python module VARIABLE: callers_floor="1" floor_next="--uses=BACKEND", and --uses=BACKEND +# (the clue) lists the reading file. +# F grep on a method whose only call the resolver DECLINED (two same-named defs in other dirs, an +# untyped receiver): callers="0" callers_floor="1". +# G grep on a TS method whose only call is spelled like it and bound NOWHERE (unresolved, no decline). +# H grep on a C function stored in a table and never called directly (a value use). +# I (negative) grep on a C function with only direct, bound calls: callers="2", no marker. +# J (negative) grep on a markdown heading: callers="0" stays plain (nothing calls or reads a heading). +# K --for bundle : implementors= is the lego verb's own count (6) even when fewer rows are listed, with +# implementors_shown= + implementors_next=; (negative) all rows listed => no shown/next attribute. +# L implementors_floor: an inherit clause naming the interface that bound nowhere (a cross-language base) +# floors the count on the bundle AND the targeted verb; (negative) without it, no marker. +# M MCP twins: grep's `enclosing` JSON carries callers_floor/floor_next exactly where the CLI does; the MCP +# `for` lego block equals the CLI's. +# N legend: every new attribute is defined in the answer that carries it (compact and full dialects). +# O determinism (x2) + xmllint. +# +# Usage: bash test/countfloorcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/countfloorcheck.sh + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "countfloorcheck: SKIP — python3 not found (needed for the MCP JSON arm)"; exit 0; } +command -v perl >/dev/null 2>&1 || { echo "countfloorcheck: SKIP — perl not found (needed to read past legend comments)"; exit 0; } + +W="$( mktemp -d )"; trap 'rm -rf "$W"' EXIT +mkdir -p "$W/c" "$W/py/pkg/css" "$W/py/pkg/other" "$W/ts" "$W/md" + +# ── corpora ────────────────────────────────────────────────────────────────────────────────────────────── +cat > "$W/c/a.h" <<'EOF' +struct cell { int ch; int attr; }; +int width_of(const char *s); +EOF +cat > "$W/c/a.c" <<'EOF' +#include "a.h" +static int helper_dead(int x) { return x + 1; } +int width_of(const char *s) +{ + int n = 0; + while (*s) { n++; s++; } + return n; +} +int pad(const char *s) +{ + int w = width_of(s); + w += width_of("x"); + w += width_of("yz"); + return w; +} +static int twice(int v) { return v * 2; } +int user(void) +{ + struct cell c; + c.ch = twice(1); + return pad("a") + + width_of("yy"); +} +EOF +cat > "$W/c/ops.c" <<'EOF' +struct ops { int (*run)(int); }; +static int run_fn(int v) { return v + 41; } +static const struct ops table = { .run = run_fn }; +int dispatch(int v) { return table.run(v); } +EOF +cat > "$W/py/pkg/constants.py" <<'EOF' +import os +BACKEND = os.environ.get("APP_BACKEND_ENV", None) +EOF +cat > "$W/py/pkg/app.py" <<'EOF' +from . import constants + +def get_backend(): + d = constants.BACKEND + return d +EOF +cat > "$W/py/pkg/css/query.py" <<'EOF' +class Selection: + def tail(self): + raise ValueError("no items match last") +EOF +cat > "$W/py/pkg/other/cache.py" <<'EOF' +class LRU: + def tail(self): + return "lru tail" +EOF +cat > "$W/py/pkg/use.py" <<'EOF' +def use_param(q): + return q.tail() +EOF +cat > "$W/ts/router.ts" <<'EOF' +export interface Router { add(m: string): void; match(p: string): T | null } +EOF +for n in Alpha Beta Gamma Delta Epsilon Zeta; do +cat > "$W/ts/$n.ts" < implements Router { + add(m: string): void {} + match(p: string): T | null { return null } +} +EOF +done +cat > "$W/ts/lonely.ts" <<'EOF' +export class Lonely { lonelyMatch(p: string): string { return "lonely marker " + p } } +export function onlyName() { return lonelyMatch('y') } +EOF +cat > "$W/md/notes.md" <<'EOF' +# Notes + +## Added + +- Added HEADING_MARKER environment variable +EOF + +run(){ ( cd "$W/$1" && shift && "$BIN" . --no-cache "$@" 2>/dev/null ); } +root_tag(){ nocomment "$1" | grep -oE "<$2 [^>]*>" | head -1; } +attr(){ printf '%s' "$1" | grep -oE " $2=\"[^\"]*\"" | head -1 | sed -e "s/^ $2=\"//" -e 's/"$//'; } +nocomment(){ printf '%s' "$1" | perl -0pe 's///gs'; } +comments(){ printf '%s' "$1" | perl -0ne 'print "$&\n" while //gs'; } +first_iface(){ nocomment "$1" | grep -oE ']*>' | head -1; } +enc_row(){ printf '%s' "$1" | grep -oE "]*/>" | head -1; } + +echo "countfloorcheck: BIN=$BIN" + +# ── A: a C struct ────────────────────────────────────────────────────────────────────────────────── +echo "=== A: --safe-delete on a C struct: the zero is a floor, risk is no reading, next= finds its uses ===" +OUT_A="$( run c --safe-delete=cell )"; R_A="$( root_tag "$OUT_A" safe-delete )" +[ -n "$R_A" ] || no "(A) premise: no root for cell" +[ "$( attr "$R_A" callers )" = 0 ] && [ "$( attr "$R_A" uses )" = 0 ] || no "(A) premise: callers/uses are not 0: $R_A" +if [ "$( attr "$R_A" callers_floor )" = 1 ]; then ok "(A) callers_floor=\"1\" beside callers=\"0\""; else no "(A) callers=\"0\" printed as a total: $R_A"; fi +if [ "$( attr "$R_A" uses_floor )" = 1 ]; then ok "(A) uses_floor=\"1\" beside uses=\"0\""; else no "(A) uses=\"0\" printed as a total: $R_A"; fi +if [ "$( attr "$R_A" risk )" = unmodelled ]; then ok "(A) risk=\"unmodelled\", never none-found"; else no "(A) risk=\"$( attr "$R_A" risk )\", expected unmodelled"; fi +NEXT_A="$( attr "$R_A" next )" +if [ "$NEXT_A" = "--grep=cell" ]; then ok "(A) next=\"--grep=cell\" (C reads are never indexed: the literal scan)"; else no "(A) next=\"$NEXT_A\", expected --grep=cell"; fi +FOLLOW_A="$( run c "$NEXT_A" )" +printf '%s' "$FOLLOW_A" | grep -q '' && ok "(A) the clue leads to the use: --grep=cell names a.c (struct cell c;)" \ + || no "(A) following next= did not reach a.c" + +# ── B: a provably complete zero ────────────────────────────────────────────────────────────────────────── +echo "=== B: a static C function with no call anywhere keeps a plain zero ===" +R_B="$( root_tag "$( run c --safe-delete=helper_dead )" safe-delete )" +[ "$( attr "$R_B" callers )" = 0 ] && [ "$( attr "$R_B" risk )" = none-found ] && [ "$( attr "$R_B" dead_code_candidate )" = 1 ] \ + && ok "(B) callers=\"0\" risk=\"none-found\" dead_code_candidate=\"1\" (unchanged)" || no "(B) the complete zero changed: $R_B" +case "$R_B" in *callers_floor=*|*uses_floor=*|*' next='*) no "(B) a provably complete zero carries a floor marker: $R_B" ;; *) ok "(B) no floor marker, no next= on a complete zero" ;; esac + +# ── C/D: the call-site line ────────────────────────────────────────────────────────────────────────────── +echo "=== C/D: --safe-delete caller rows carry the CALL lines beside the definition line ===" +OUT_C="$( run c --safe-delete=width_of )" +ROW_PAD="$( printf '%s' "$OUT_C" | grep -oE ']*/>' | head -1 )" +ROW_USER="$( printf '%s' "$OUT_C" | grep -oE ']*/>' | head -1 )" +if [ "$( attr "$ROW_PAD" p )" = "a.c:9" ]; then ok "(C) p= still names the caller's definition line (a.c:9)"; else no "(C) p= changed on the pad row: $ROW_PAD"; fi +if [ "$( attr "$ROW_PAD" sites_l )" = "11,12,13" ]; then ok "(C) three calls on three lines: sites_l=\"11,12,13\""; else no "(C) pad row: $ROW_PAD"; fi +if [ "$( attr "$ROW_USER" sites_l )" = "22" ]; then ok "(C) a call continued onto its own line: sites_l=\"22\""; else no "(C) user row: $ROW_USER"; fi +ROW_TW="$( printf '%s' "$( run c --safe-delete=twice )" | grep -oE ']*/>' | head -1 )" +if [ "$( attr "$ROW_TW" sites_l )" = "20" ]; then ok "(D) a single-definition callee called once still prints its call site (20)"; else no "(D) twice row: $ROW_TW"; fi +R_C="$( root_tag "$OUT_C" safe-delete )" +case "$R_C" in *callers_floor=*) no "(C) bound direct calls only, yet callers is floored: $R_C" ;; *) ok "(C) callers=\"2\" with no floor (every call bound)" ;; esac + +# ── E: a Python variable ───────────────────────────────────────────────────────────────────────────────── +echo "=== E: grep on a module variable ===" +OUT_E="$( run py --grep=APP_BACKEND_ENV )"; ROW_E="$( enc_row "$OUT_E" BACKEND )" +[ "$( attr "$ROW_E" callers )" = 0 ] || no "(E) premise: BACKEND row: $ROW_E" +if [ "$( attr "$ROW_E" callers_floor )" = 1 ]; then ok "(E) callers_floor=\"1\" on a variable's call count"; else no "(E) printed callers=\"0\" as a total: $ROW_E"; fi +NEXT_E="$( attr "$ROW_E" floor_next )" +if [ "$NEXT_E" = "--uses=BACKEND" ]; then ok "(E) floor_next=\"--uses=BACKEND\""; else no "(E) floor_next=\"$NEXT_E\""; fi +if run py "$NEXT_E" | grep -q 'pkg/app.py:4'; then ok "(E) the clue leads to the reader: --uses=BACKEND lists pkg/app.py:4"; else no "(E) --uses=BACKEND did not list the reading line"; fi +R_ED="$( root_tag "$( run py --safe-delete=BACKEND )" safe-delete )" +[ "$( attr "$R_ED" uses_floor )" = 1 ] && [ "$( attr "$R_ED" risk )" = unmodelled ] && [ "$( attr "$R_ED" next )" = "--uses=BACKEND" ] \ + && ok "(E) --safe-delete=BACKEND: uses_floor=\"1\" risk=\"unmodelled\" next=\"--uses=BACKEND\" (Python reads ARE indexed by the uses verb)" \ + || no "(E) --safe-delete=BACKEND: $R_ED" + +# ── F: a declined call ─────────────────────────────────────────────────────────────────────────────────── +echo "=== F: grep on a method whose one call was declined ===" +OUT_F="$( run py --grep='no items match' )"; ROW_F="$( enc_row "$OUT_F" 'Selection::tail' )" +CALLERS_F="$( root_tag "$( run py --callers=Selection.tail )" callers )" +[ -n "$( attr "$CALLERS_F" declined_calls )" ] || no "(F) premise: the q.tail() call is not declined here: $CALLERS_F" +[ "$( attr "$ROW_F" callers )" = 0 ] && [ "$( attr "$ROW_F" callers_floor )" = 1 ] && [ "$( attr "$ROW_F" floor_next )" = "--uses=tail" ] \ + && ok "(F) callers=\"0\" callers_floor=\"1\" floor_next=\"--uses=tail\"" || no "(F) declined row: $ROW_F" + +# ── G: an unbound call, no decline ─────────────────────────────────────────────────────────────────────── +echo "=== G: grep on a TS method whose one call bound nowhere ===" +ROW_G="$( enc_row "$( run ts --grep='lonely marker' )" lonelyMatch )" +CALLERS_G="$( root_tag "$( run ts --callers=lonelyMatch )" callers )" +[ -z "$( attr "$CALLERS_G" declined_calls )" ] || no "(G) premise: this arm wants NO decline: $CALLERS_G" +if [ "$( attr "$ROW_G" callers )" = 0 ] && [ "$( attr "$ROW_G" callers_floor )" = 1 ]; then ok "(G) unbound same-name call: callers_floor=\"1\""; else no "(G) row: $ROW_G"; fi + +# ── H: a value use ─────────────────────────────────────────────────────────────────────────────────────── +echo "=== H: grep on a C function only a table holds ===" +ROW_H="$( enc_row "$( run c --grep='v + 41' )" run_fn )" +if [ "$( attr "$ROW_H" callers )" = 0 ] && [ "$( attr "$ROW_H" callers_floor )" = 1 ]; then ok "(H) value use: callers_floor=\"1\""; else no "(H) row: $ROW_H"; fi + +# ── I/J: complete counts stay plain ────────────────────────────────────────────────────────────────────── +echo "=== I/J: counts the index can vouch for stay plain totals ===" +ROW_I="$( enc_row "$( run c --grep='n++' )" width_of )" +[ "$( attr "$ROW_I" callers )" = 2 ] || no "(I) premise: width_of row: $ROW_I" +case "$ROW_I" in *callers_floor=*|*floor_next=*) no "(I) a fully bound count carries a floor: $ROW_I" ;; *) ok "(I) callers=\"2\" stays a plain total" ;; esac +ROW_J="$( printf '%s' "$( run md --grep=HEADING_MARKER )" | grep -oE ']*/>' | head -1 )" +[ -n "$ROW_J" ] || no "(J) premise: no row for the heading" +case "$ROW_J" in *callers_floor=*) no "(J) a markdown heading's zero is floored: $ROW_J" ;; *) ok "(J) a heading's callers=\"0\" stays plain" ;; esac + +# ── K: the --for lego count ────────────────────────────────────────────────────────────────────────────── +echo "=== K: the --for bundle's implementors= is the lego verb's count, a short list says so ===" +TOTAL_K="$( attr "$( first_iface "$( run ts --lego=Router )" )" implementors )" +[ "$TOTAL_K" = 6 ] || no "(K) premise: --lego=Router implementors=\"$TOTAL_K\", expected 6" +LEGO_ALL="$( run ts '--for=which router classes implement the Router interface' | grep -oE ']*>' | head -1 )" +if [ "$( attr "$LEGO_ALL" implementors )" = 6 ]; then ok "(K) all six listed: implementors=\"6\""; else no "(K) $LEGO_ALL"; fi +case "$LEGO_ALL" in *implementors_shown=*|*implementors_next=*) no "(K) every row listed, yet a short-list marker: $LEGO_ALL" ;; *) ok "(K) no implementors_shown= when every row is listed" ;; esac +# the narrowed shape, built by construction: a task that reaches ONE implementor's file and the interface's +# file, so the bundle's rows are narrowed to the files its own sigs render (legobundlecheck rule 2) — one row of six +mkdir -p "$W/tsn"; cp "$W/ts/"[A-Z]*.ts "$W/ts/router.ts" "$W/tsn/" +printf 'export function routerDeltaDispatch(): number { return 1 }\n' >> "$W/tsn/router.ts" +cat > "$W/tsn/Delta.ts" <<'EOF' +import { Router } from './router' +export class DeltaRouter implements Router { + add(m: string): void {} + match(p: string): T | null { return null } + deltaDispatch(p: string): number { return p.length } +} +EOF +LEGO_N="$( run tsn '--for=delta dispatch' | grep -oE ']*>' | head -1 )" +if [ -z "$LEGO_N" ]; then + no "(K) premise: no Router iface row in the narrowed bundle" +else + IMPL_N="$( attr "$LEGO_N" implementors )"; SHOWN_N="$( attr "$LEGO_N" implementors_shown )" + if [ "$IMPL_N" = 6 ]; then ok "(K) narrowed bundle still says implementors=\"6\""; else no "(K) narrowed bundle: $LEGO_N"; fi + if [ -n "$SHOWN_N" ]; then + [ "$SHOWN_N" -lt 6 ] && [ "$( attr "$LEGO_N" implementors_next )" = "--lego=router.ts:Router" ] \ + && ok "(K) implementors_shown=\"$SHOWN_N\" implementors_next=\"--lego=router.ts:Router\"" || no "(K) short-list marker: $LEGO_N" + if run tsn --lego=router.ts:Router | grep -q 'implementors="6"'; then ok "(K) the clue lists all six"; else no "(K) implementors_next did not list six"; fi + else + no "(K) premise: the bundle was not narrowed (no implementors_shown=): $LEGO_N" + fi +fi + +# ── L: implementors floor ──────────────────────────────────────────────────────────────────────────────── +echo "=== L: an inherit clause naming the interface that bound nowhere floors implementors= ===" +mkdir -p "$W/tsl"; cp "$W/ts/"*.ts "$W/tsl/" +printf 'class PyRouter(Router):\n def add(self, m):\n return m\n' > "$W/tsl/py_router.py" +IF_L="$( first_iface "$( run tsl --lego=Router )" )" +[ "$( attr "$IF_L" implementors )" = 6 ] || no "(L) premise: the Python class must not be counted: $IF_L" +[ "$( attr "$IF_L" implementors_floor )" = 1 ] && [ "$( attr "$IF_L" floor_next )" = "--uses=Router" ] \ + && ok "(L) --lego: implementors_floor=\"1\" floor_next=\"--uses=Router\"" || no "(L) --lego: $IF_L" +IF_LB="$( run tsl '--for=which router classes implement the Router interface' | grep -oE ']*>' | head -1 )" +if [ "$( attr "$IF_LB" implementors_floor )" = 1 ]; then ok "(L) the --for bundle carries the same floor"; else no "(L) bundle: $IF_LB"; fi +case "$( first_iface "$( run ts --lego=Router )" )" in *implementors_floor=*) no "(L) a fully bound interface carries implementors_floor" ;; *) ok "(L) no floor when every inherit clause bound" ;; esac + +# ── M: MCP twins ───────────────────────────────────────────────────────────────────────────────────────── +echo "=== M: the MCP grep and for twins say the same thing ===" +cat > "$W/mcptext.py" <<'PY' +import sys, json +for line in sys.stdin: + line = line.strip() + if not line: continue + try: d = json.loads( line ) + except Exception: continue + c = d.get( "result", {} ).get( "content" ) + if c: print( c[0].get( "text", "" ) ) +PY +mcp_call(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"%s","arguments":%s}}\n' "$1" "$2" \ + | "$BIN" --mcp 2>/dev/null | python3 "$W/mcptext.py"; } +MCP_G="$( mcp_call grep "{\"path\":\"$W/py\",\"pattern\":\"APP_BACKEND_ENV\"}" )" +printf '%s' "$MCP_G" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +rows = { r["n"]: r for r in d.get( "enclosing", [] ) } +r = rows.get( "BACKEND", {} ) +sys.exit( 0 if r.get( "callers" ) == 0 and r.get( "callers_floor" ) == 1 and r.get( "floor_next" ) == "--uses=BACKEND" else 1 )' \ + && ok "(M) MCP grep enclosing BACKEND: callers 0, callers_floor 1, floor_next --uses=BACKEND" || no "(M) MCP grep: $MCP_G" +MCP_GI="$( mcp_call grep "{\"path\":\"$W/c\",\"pattern\":\"n++\"}" )" +printf '%s' "$MCP_GI" | python3 -c ' +import json, sys +d = json.loads( sys.stdin.read() ) +r = { x["n"]: x for x in d.get( "enclosing", [] ) }.get( "width_of", {} ) +sys.exit( 0 if r.get( "callers" ) == 2 and "callers_floor" not in r and "floor_next" not in r else 1 )' \ + && ok "(M) MCP grep: a complete count carries no floor key (as on the CLI)" || no "(M) MCP grep negative: $MCP_GI" +MCP_F="$( mcp_call for "{\"path\":\"$W/tsl\",\"task\":\"which router classes implement the Router interface\",\"sections\":\"lego\"}" | grep -oE '.*' | head -1 )" +CLI_F="$( run tsl '--for=which router classes implement the Router interface' --sections=lego | grep -oE '.*' | head -1 )" +if [ -n "$CLI_F" ] && [ "$MCP_F" = "$CLI_F" ]; then ok "(M) MCP for == CLI --for (implementors_floor included)"; else no "(M) lego twin differs: cli='$CLI_F' mcp='$MCP_F'"; fi + +# ── N: legend ──────────────────────────────────────────────────────────────────────────────────────────── +echo "=== N: every new attribute is defined where it rides ===" +for dialect in compact full; do + L_A="$( run c --safe-delete=cell --legend=$dialect )"; L_C="$( run c --safe-delete=width_of --legend=$dialect )" + L_E="$( run py --grep=APP_BACKEND_ENV --legend=$dialect )" + L_K="$( run tsl '--for=which router classes implement the Router interface' --legend=$dialect )" + for pair in "L_A:callers_floor=" "L_A:uses_floor=" "L_A:unmodelled" "L_C:sites_l=" "L_E:callers_floor=" "L_E:floor_next=" \ + "L_K:implementors_floor=" "L_K:implementors_shown=" "L_K:floor_next="; do + var="${pair%%:*}"; term="${pair#*:}" + comments "${!var}" | grep -qF -- "$term" \ + && ok "(N/$dialect) $term defined in the legend of the $var answer" || no "(N/$dialect) $term is not defined in the $var legend" + done +done +case "$( run c --safe-delete=helper_dead --legend=compact )" in *callers_floor*|*uses_floor*|*sites_l*) no "(N) a legend defines a floor/sites term its answer does not carry" ;; *) ok "(N) present-only: no floor/sites term on an answer without them" ;; esac + +# ── O: determinism + xmllint ───────────────────────────────────────────────────────────────────────────── +echo "=== O: determinism and well-formed XML ===" +for spec in "c --safe-delete=cell" "c --safe-delete=width_of" "py --grep=APP_BACKEND_ENV" "tsl --lego=Router"; do + set -- $spec; a="$( run "$@" )"; b="$( run "$@" )" + if [ -n "$a" ] && [ "$a" = "$b" ]; then ok "(O) deterministic: $spec"; else no "(O) not deterministic (or empty): $spec"; fi + if command -v xmllint >/dev/null 2>&1; then + if printf '%s' "$a" | xmllint --noout - 2>/dev/null; then ok "(O) well-formed: $spec"; else no "(O) xmllint rejects: $spec"; fi + fi +done + +if [ "$fail" -ne 0 ]; then echo "countfloorcheck: FAIL"; exit 1; fi +echo "countfloorcheck: PASS" +exit 0 diff --git a/test/regression.sh b/test/regression.sh index 6ebb7fd68..fbb44c631 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From a45f9b5031dcca519b7fe78d41ba943edd799cd4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:08:28 -0400 Subject: [PATCH 026/229] fix(honesty): mark a count the index cannot vouch for as a floor; safe-delete rows carry the call line COUNT-FLOOR. A row's count stays a plain total only when the index holds no evidence of a miss for that definition. Otherwise it carries _floor="1" beside the number and the call that lists the rest (src/countfloor.h, one function for the CLI and the MCP twin): - evidence: a declined call that named it; a call or macro site spelled like it that bound to no definition of that name; a use as a value; or a kind used by reading or naming it (variable, class, struct, interface, field). - grep : callers_floor="1" floor_next=...; MCP grep enclosing rows carry callers_floor/floor_next from the same rows. - --safe-delete: callers_floor="1"; uses_floor="1" when the definition's reads and type mentions are not indexed in this run; risk="unmodelled" (new value) when nothing was found for such a kind; next= the uses verb, or the literal grep where the language's reads are never indexed (C, and a .h definition). - --for : implementors= is the lego verb's own count, never the rows listed; a shorter list adds implementors_shown= and implementors_next= (--lego=FILE:NAME). implementors_floor= / floor_next= when an extends clause spelled like the interface bound nowhere (bundle and --lego, CLI and MCP). - The MCP for twin narrows with the root prefix the CLI passes; it compared root-relative rows against absolute paths and served no . CALLSITE-LINE. --safe-delete caller rows carry sites_l=, the call-site lines spelled like the symbol (edit-check's own editCheckCallSites pass), beside p=, which stays the caller's definition line. Every new attribute is defined present-only in the answer carrying it (compact table rows, the full safe-delete and grep prose, a post-render --for splice like the section-stub clause). Answers without one are byte-identical. --help, docs/COMMANDS.md (regenerated), the change-check skill and CHANGELOG name risk=unmodelled and the new attributes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 ++ docs/COMMANDS.md | 5 +- skills/ripwire-change-check/SKILL.md | 2 +- src/cli.h | 8 +- src/compactlegend.h | 11 + src/countfloor.h | 334 +++++++++++++++++++++++++++ src/main.cpp | 1 + src/mcpverbs.h | 55 ++++- src/serialize.h | 59 ++++- src/verbs_for.h | 17 +- src/verbs_grep.h | 23 +- src/verbs_navigate.h | 91 +++++++- 12 files changed, 593 insertions(+), 38 deletions(-) create mode 100644 src/countfloor.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..94f867267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,31 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — a count the index cannot vouch for is marked a floor beside the number; safe-delete rows carry the call line + +A zero printed as a total read as "nothing uses this" where the index itself held evidence of a miss: +`--grep`'s `` on a method whose only call the resolver declined to bind, or on a module variable that +other files read (a call count cannot see a read); `--safe-delete` answering `callers="0" uses="0" risk="none-found"` +on a C struct whose type mentions are not indexed; and the `--for` bundle's ``, which counted the +implementor rows it LISTED after narrowing to its own files (`implementors="1"` on a tree with six). + +- **Row floors.** `callers_floor="1"` (grep `` and the MCP `grep` `enclosing` rows, `--safe-delete`) and + `implementors_floor="1"` (`` rows, bundle and `--lego`) ride beside the count exactly when the index holds + evidence of a miss for that definition: a declined call that named it, a call or extends clause spelled like it that + bound to no definition, a use as a value, or a kind used by reading or naming it (a variable, class, struct, + interface). `floor_next=` (rows) / `next=` (`--safe-delete`) is the call that lists the rest — the uses verb, or the + literal grep where the language's reads are never indexed. A count with no such evidence stays a plain total: a + static function nobody calls still reads `callers="0" risk="none-found" dead_code_candidate="1"`. +- **`--safe-delete`.** `uses_floor="1"` when the definition's uses are reads or type mentions this run did not index, + and `risk="unmodelled"` (new value) when nothing was found for such a kind — never `none-found`. Each caller row + carries `sites_l=`, the ascending call-site lines spelled like the symbol (the edit-check verb's own pass), beside + `p=`, which stays the line where the caller is defined. +- **`--for` ``.** `implementors=` is now the lego verb's own count; a shorter list adds `implementors_shown=` and + `implementors_next="--lego=FILE:NAME"`. The MCP `for` twin now narrows its `` block with the root prefix the + CLI passes; before, it compared root-relative rows against absolute paths and served no `` at all. +- Every new attribute is defined present-only in the answer that carries it (compact and full dialects). Answers + without one are byte-identical (34 argv across three corpora). Gate: `countfloorcheck`. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..22a001ec8 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -3228,7 +3228,7 @@ $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply **Answers:** "can I delete this?" — one call joining callers, blast radius, tests and history "can I delete this?" — ONE call composing signals the tool already computes for one already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=), every --uses read/write/import/call/extends site (uses=), how much of the blast radius the tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own zero-caller/internal-linkage shape at defs=1 (dead_code_candidate=). -ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict. +ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; unmodelled when nothing was found for a kind used by reading or naming it (a variable, class, struct) whose uses those counts cannot see. callers_floor=/uses_floor= mark a count the index holds evidence of a miss for, and next= is the call that lists the rest. A caller row's sites_l= is its call-site LINES (p= stays the line where the caller is defined). **Try it** @@ -3244,7 +3244,8 @@ $ ./build/ripwire . --safe-delete=DoesNotExist **Caveats (stated by the binary):** - ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. -- FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict. +- FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; +- unmodelled when nothing was found for a kind used by reading or naming it (a variable, class, struct) whose uses those counts cannot see. ### `--slice=SYM[:VAR]` diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index bc2a23fb1..b02e87711 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -189,7 +189,7 @@ emits a flat `` top-K — identity + score + signatu 1-hop `callers=`, the transitive `--impact` blast radius (`impact_reaches=`), every `--uses` read/write/import/call/extends site (`uses=`), how much of that radius the `tested=` lens covers (`radius_tested=`/`radius_untested=`), and `--dead-code`'s own zero-caller/internal-linkage shape at - `defs=1` (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `uses-exist` / + `defs=1` (`dead_code_candidate=`), into ONE call. `risk=` NAMES what was found — `none-found` / `uses-exist` / `unmodelled` / `untested-radius` — never a go/no-go verdict; `radius_untested=` equal to `impact_reaches=` is the strongest signal ("nothing downstream is test-covered"). `ambiguous_callers=`/per-row `amb="1"` disclose the same call-graph resolution limit `--edit-check`'s `incompatible=` and `--for`'s `amb=` already carry. diff --git a/src/cli.h b/src/cli.h index 373fd9ed9..a34da352e 100644 --- a/src/cli.h +++ b/src/cli.h @@ -335,7 +335,7 @@ struct Config // computes — 1-hop callers, the transitive --impact blast radius, every --uses // read/write/import/call/extends site, how much of the radius the tested= lens // covers, and --dead-code's own zero-caller/internal-linkage shape at defs=1. FACTS only: - // risk= names what was found (none-found/uses-exist/untested-radius), never a + // risk= names what was found (none-found/uses-exist/untested-radius/unmodelled), never a // go/no-go verdict. file:name disambiguates like --around/--lego. std::string_view sliceSpec; // --slice=SYM[:VAR] (lane/paper-slice): NAME-BASED intra-procedural def-use // slice of VAR inside the ONE uniquely-resolved definition SYM (multiple defs @@ -1889,7 +1889,11 @@ inline constexpr char kHelpHead[] = " whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of\n" " proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND\n" " zero uses), untested-radius (a radius exists and none of it is test-covered), or\n" - " uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict.\n" + " uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict; unmodelled\n" + " when nothing was found for a kind used by reading or naming it (a variable, class, struct)\n" + " whose uses those counts cannot see. callers_floor=/uses_floor= mark a count the index holds\n" + " evidence of a miss for, and next= is the call that lists the rest. A caller row's sites_l= is\n" + " its call-site LINES (p= stays the line where the caller is defined).\n" " --slice=SYM[:VAR] trace one variable's definitions and uses inside one function\n" " NAME-BASED intra-procedural def-use slice of variable VAR inside the ONE uniquely-resolved\n" " definition SYM (statement-level def-use edges as a queryable primitive — the ARISE result,\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..d331d43aa 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -833,6 +833,17 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "unindexed_hits", "unindexed_hits=N: hits in off-index files, NOT in hits=/total=; listed in the trailing unindexed element", false, "grep", MapHeaderRead::No, {}, "grep" }, { "callers", "callers=N: 1-hop distinct callers of this name, all same-named defs (a FLOOR)", true, "enc", MapHeaderRead::No, {}, "grep" }, { "cx", "cx=N: cyclomatic complexity, max over same-named defs; absent when 0", true, "enc", MapHeaderRead::No, {}, "grep" }, + // count-floor (src/countfloor.h): the row's own floor marker, emitted only beside evidence of a miss for THAT row. + { "callers_floor", "callers_floor=1: callers= may be short for this row: a declined or unbound call spelled like it, a value use, or a kind used by reading/naming it (var, class, struct, interface); not proof more callers exist; absent = no such evidence", true, "enc", MapHeaderRead::No, {}, "grep" }, + { "floor_next", "floor_next=: the call that lists what that count could not see (the uses verb; the literal scan where reads are not indexed)", true, "enc", MapHeaderRead::No, {}, "grep" }, + // count-floor + CALLSITE-LINE on safe-delete (src/verbs_navigate.h runSafeDelete): root markers beside the counts they qualify. + { "callers_floor", "callers_floor=1: callers= and impact_reaches= may be short: a declined or unbound call spelled like sym=, a value use, or a kind used by reading/naming it; not proof more exist", false, {}, MapHeaderRead::No, {}, "safe-delete" }, + { "uses_floor", "uses_floor=1: uses= cannot see this kind's reads or type mentions in this run; a zero there is not a count of its uses", false, {}, MapHeaderRead::No, {}, "safe-delete" }, + { "risk", "risk=unmodelled: nothing found, but the counts are blind to this kind's uses; no reading either way, next= lists them", false, "safe-delete", MapHeaderRead::No, "unmodelled", "safe-delete" }, + { "sites_l", "c sites_l=: that caller's call-site LINES spelled like sym= (p= is where the caller is defined); not proof each binds here", true, "c", MapHeaderRead::No, {}, "safe-delete" }, + // count-floor on the targeted lego verb (serialize.h packLego, countfloor.h implementorFloors): present-only. + { "implementors_floor", "implementors_floor=1: an extends clause spelled like this interface bound nowhere; implementors= may be short (not proof of another)", true, "iface", MapHeaderRead::No, {}, "lego" }, + { "floor_next", "floor_next=: lists every extends site of the name, bound or not", true, "iface", MapHeaderRead::No, {}, "lego" }, // layout: src/layout.h writeLayout / writeLayoutDef { "sym", "sym=: the aggregate name asked for", false, "layout", MapHeaderRead::No, {}, "layout" }, { "found", "found=1: a C-family struct/class/union body was located for sym=", false, "layout", MapHeaderRead::No, {}, "layout" }, diff --git a/src/countfloor.h b/src/countfloor.h new file mode 100644 index 000000000..2f0bc78fb --- /dev/null +++ b/src/countfloor.h @@ -0,0 +1,334 @@ +#pragma once + +// countfloor.h — when a per-row count is a FLOOR, and which call finds the rest. +// +// WHY. A root's counts_floor="1" and a verb legend's "a zero means none found" are blanket statements; the reader +// meets them once, far from the number. A graded comparison showed what that costs: of the false claims scored +// against this tool, eight were a ZERO or "none" read as a TOTAL — a method's `` while six calls the +// resolver declined to bind could have meant it, a module variable's `` while another file READS it +// (it is never called), a C struct's `--safe-delete` answering callers/uses="0" risk="none-found" although its type +// mentions — which the index does not capture at all — sat in thirty files. Each number was what the graph +// held; none of them was the answer the reader took from it. +// +// THE RULE. A row's count stays a plain total only when nothing this index knows could add to it. It is a floor — +// spelled `_floor="1"` beside the number (the locals="N" locals_floor="1" precedent), with a pasteable +// follow-up that finds the rest — when the index itself holds EVIDENCE of a miss for THAT definition: +// * declined — a call the resolver refused to bind named it among its candidates (graph.h declinedCallsNaming); +// * unbound — a call or macro site SPELLED like it bound to no definition of that name at all (a receiver the +// resolver could not type, a name whose every candidate was filtered), from a symbol that is not +// already one of its callers; +// * value — the function is stored or passed as a value (valuerefindex.h), so it is called by another name; +// * unmodelled — its KIND is used by reading or naming it (a variable, a struct, an interface, a field), and a +// call count cannot see a read or a type mention at all. +// None of these says the missing callers EXIST: an unbound `x.last()` may be a list's own method. The marker is a +// statement about what was NOT read, which is exactly what a total would have claimed was read. +// +// CLI AND MCP. Every input here is identical in the lean (CLI nav verbs) and rich (MCP) ingest families: call and +// macro references, inherit references, the graph's edges and declines, value references (both families capture +// them, ingest_sidecap.h) and symbol kinds. Read/write/type references — the rich family's extra rows — are never +// consulted, so the two surfaces cannot disagree about a floor. + +#include "graph.h" // Graph, declinedCallsNaming — the declined evidence and the in-edge CSR +#include "model.h" +#include "nextverb.h" // nextFlag — the follow-up's shell-safe spelling +#include "valuerefindex.h" // ValueRefIndex, valueRefCallerRows — the value evidence +#include "infra/Diagnostics.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +// How a definition is USED, for the one question a caller count can answer. +// Calls — a call reaches it: functions, methods, function-like macros. +// NotCalls — it is read or named more than it is called: a variable, a class or struct (a constructor call is one use, a +// type mention, a base clause or an isinstance are the others), an interface, a field, anything unclassified. +// callers= is structurally blind to most of its uses. +// NotCode — a markdown heading or a file's module scope: no code calls or reads it, so a zero there is simply true. +enum class UseForm : std::uint8_t { Calls, NotCalls, NotCode }; + +inline UseForm useFormOf( const Symbol& s ) noexcept +{ + switch( s.kind ) + { + case SymKind::Function: + case SymKind::Method: + case SymKind::Macro: + return UseForm::Calls; + case SymKind::Section: + case SymKind::ModuleScope: + return UseForm::NotCode; + case SymKind::Class: // a constructor call is one use; a type mention, a base clause, an isinstance are the rest + case SymKind::Struct: + case SymKind::Interface: + case SymKind::Var: + case SymKind::Field: + case SymKind::Other: + return UseForm::NotCalls; + } + return UseForm::NotCalls; // a byte past the enum: the conservative answer (a floor), never a silent total +} + +// Whether a read/write/type use of a symbol in `lang` reaches the use-site table in an ingest that captured value +// uses (ingest_sidecap.h arms that pass for C++, ObjC and Python only). Outside those, `--uses` lists calls and value +// references but never a read, so the honest follow-up for a NotCalls symbol is the literal scan. +inline bool valueUsesArmedFor( Lang lang ) noexcept +{ + return lang == Lang::Cpp || lang == Lang::ObjC || lang == Lang::Python; +} + +// The follow-up that finds what a floored count could not: the uses verb lists every call, value and (where armed) +// read site spelled like the name, bound or not; a NotCalls symbol in a language whose reads are never captured gets +// the literal scan instead, which is the only verb that can see `struct cell c;`. +// +// A `.h` definition is the exception: the grammar reads it as C++, but its users are as often `.c` files, whose reads the +// uses verb never sees — so a header's NotCalls symbol gets the literal scan too (`struct cell` in a.h used in a.c). +inline std::string countFloorNext( const IngestResult& ing, const Symbol& s ) +{ + const std::string_view path = s.fileId < ing.files.size() ? std::string_view( ing.files[ s.fileId ] ) : std::string_view(); + const bool cHeader = path.ends_with( ".h" ); + const bool literal = useFormOf( s ) == UseForm::NotCalls && ( !valueUsesArmedFor( s.lang ) || cHeader ); + return nextFlag( literal ? "--grep=" : "--uses=", s.name ); +} + +// One row's verdict. `next` is a CLI flag spelling (MCP prints the same text), empty exactly when !isFloor. +struct CallerFloor +{ + bool isFloor = false; + bool unmodelled = false; // the NotCalls reason (no call form): the safe-delete verdict reads it + std::string next; +}; + +// callers= floors for a batch of rows: sets[i] is one row's definitions (all one NAME — a grep row's ids, a +// safe-delete selector's defs). ONE pass over the symbols and ONE over the references, whatever the row count — +// never a per-row rescan of the reference table (editcheck.h's rule). `vri` may be null: then it is built here, once, +// and only when a Calls row is still undecided after the cheaper evidence. +inline std::vector callerFloors( const IngestResult& ing, const Graph& g, std::span> sets, + const ValueRefIndex* vri = nullptr ) +{ + std::vector out( sets.size() ); + if( sets.empty() ) + { + return out; + } + const std::size_t nodeCount = g.wOutDeg.size(); + const auto* inRo = g.inEdges.rowOffsets(); + const auto* inCi = g.inEdges.colIndices(); + + // Row → its name; a name may head several rows (two chains `A::f` and `B::f`) — each row asks of the NAME. + HashMap> rowsOfName; + std::vector pending; // Calls rows not yet decided by the cheap evidence + for( std::uint32_t i = 0; i < sets.size(); ++i ) + { + const std::vector& ids = sets[i]; + bool anyNotCalls = false; + bool anyCalls = false; + for( const NodeId id : ids ) + { + if( id >= ing.symbols.size() ) + { + continue; + } + const UseForm form = useFormOf( ing.symbols[id] ); + anyNotCalls = anyNotCalls || form == UseForm::NotCalls; + anyCalls = anyCalls || form == UseForm::Calls; + } + if( ids.empty() || ids[0] >= ing.symbols.size() ) + { + continue; + } + if( anyNotCalls ) + { + out[i].isFloor = true; + out[i].unmodelled = true; + continue; + } + if( !anyCalls ) + { + continue; // NotCode only: nothing calls or reads a heading, the zero is true + } + if( declinedCallsNaming( g, ids ) > 0 ) + { + out[i].isFloor = true; + continue; + } + rowsOfName[ std::string_view( ing.symbols[ ids[0] ].name ) ].push_back( i ); + pending.push_back( i ); + } + + if( !pending.empty() ) + { + // Per pending NAME: every symbol with an edge into ANY definition of that name. A call from one of them was + // bound somewhere — to this row or to a same-named sibling — so it is not evidence of a miss. + HashMap> boundCallers; + for( NodeId id = 0; id < ing.symbols.size() && id < nodeCount; ++id ) + { + const auto it = rowsOfName.find( std::string_view( ing.symbols[id].name ) ); + if( it == rowsOfName.end() ) + { + continue; + } + std::vector& bound = boundCallers[ it->first ]; + for( std::uint32_t k = inRo[id]; k < inRo[id + 1]; ++k ) + { + bound.push_back( inCi[k] ); + } + } + for( auto& [ name, bound ] : boundCallers ) + { + std::sort( bound.begin(), bound.end() ); + bound.erase( std::unique( bound.begin(), bound.end() ), bound.end() ); + } + + // ONE pass: a call or macro site spelled like a pending name, from a symbol no edge of that name leaves. + for( const Reference& r : ing.references ) + { + if( ( r.role != RefRole::Call && r.role != RefRole::Macro ) || r.isInherit || r.isDocLink || r.isCompose || r.lang == Lang::Markdown ) + { + continue; + } + const auto it = rowsOfName.find( std::string_view( r.calleeName ) ); + if( it == rowsOfName.end() ) + { + continue; + } + const auto boundIt = boundCallers.find( it->first ); + const std::vector* bound = boundIt == boundCallers.end() ? nullptr : &boundIt->second; + const bool isBound = r.fromSymbol != kNoNode && bound != nullptr && std::binary_search( bound->begin(), bound->end(), r.fromSymbol ); + if( isBound ) + { + continue; + } + for( const std::uint32_t row : it->second ) + { + out[row].isFloor = true; + } + } + + // value evidence, last and only where still undecided: a ValueRefIndex is one more pass over the table. + std::vector stillOpen; + for( const std::uint32_t row : pending ) + { + if( !out[row].isFloor ) + { + stillOpen.push_back( row ); + } + } + if( !stillOpen.empty() ) + { + std::optional own; + const ValueRefIndex* index = vri; + if( index == nullptr ) + { + own.emplace( ing ); + index = &*own; + } + for( const std::uint32_t row : stillOpen ) + { + for( const ValueRefRow& vr : valueRefCallerRows( ing, *index, sets[row] ).rows ) + { + // a decorator row (into="@name") is a fact about the definition, not a path that calls it + if( vr.ref < ing.references.size() && !ing.references[ vr.ref ].fieldName.starts_with( '@' ) ) + { + out[row].isFloor = true; + break; + } + } + } + } + } + + for( std::uint32_t i = 0; i < sets.size(); ++i ) + { + if( out[i].isFloor ) + { + out[i].next = countFloorNext( ing, ing.symbols[ sets[i][0] ] ); + } + } + return out; +} + +// implementors= floors for the rows: an inherit reference spelled like the interface's name from a type that +// is in NO implementor list of any definition of that name — an `extends`/`implements` clause the graph did not +// bind (a cross-language filter, a qualified base it could not place). ONE pass over the references for the batch. +// The structural-typing limit (a TS class or Go type that satisfies an interface without naming it) leaves no +// reference at all, and is stated in the legend rather than guessed at here. +inline std::vector implementorFloors( const IngestResult& ing, const std::vector>& graphImplementors, + std::span ifaces ) +{ + std::vector out( ifaces.size(), 0 ); + HashMap> rowsOfName; + for( std::uint32_t i = 0; i < ifaces.size(); ++i ) + { + if( ifaces[i] < ing.symbols.size() ) + { + rowsOfName[ std::string_view( ing.symbols[ ifaces[i] ].name ) ].push_back( i ); + } + } + if( rowsOfName.empty() ) + { + return out; + } + HashMap> boundDerived; // every implementor of any def of the name + for( NodeId id = 0; id < ing.symbols.size() && id < graphImplementors.size(); ++id ) + { + const auto it = rowsOfName.find( std::string_view( ing.symbols[id].name ) ); + if( it == rowsOfName.end() ) + { + continue; + } + std::vector& bound = boundDerived[ it->first ]; + bound.insert( bound.end(), graphImplementors[id].begin(), graphImplementors[id].end() ); + } + for( auto& [ name, bound ] : boundDerived ) + { + std::sort( bound.begin(), bound.end() ); + bound.erase( std::unique( bound.begin(), bound.end() ), bound.end() ); + } + for( const Reference& r : ing.references ) + { + if( !r.isInherit && r.role != RefRole::Extends ) + { + continue; + } + const auto it = rowsOfName.find( std::string_view( r.calleeName ) ); + if( it == rowsOfName.end() ) + { + continue; + } + const auto boundIt = boundDerived.find( it->first ); + const bool isBound = r.fromSymbol != kNoNode && boundIt != boundDerived.end() + && std::binary_search( boundIt->second.begin(), boundIt->second.end(), r.fromSymbol ); + if( !isBound ) + { + for( const std::uint32_t row : it->second ) + { + out[row] = 1; + } + } + } + return out; +} + +// The targeted lego verb's clause for implementors_floor=/floor_next=, emitted as its own comment exactly when the root's +// interface carries them (kLegoLegend is one closed literal — the graphUnindexedLegendComment route). CLI and MCP share it. +inline constexpr const char* kImplementorsFloorLegendComment = + ""; + +inline const char* implementorsFloorLegendComment( const IngestResult& ing, const std::vector>& graphImplementors, NodeId focus ) +{ + const NodeId one[1] = { focus }; + return implementorFloors( ing, graphImplementors, std::span( one ) )[0] ? kImplementorsFloorLegendComment : ""; +} + +} // namespace rw diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..321e27467 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,6 +40,7 @@ // BEFORE mcp.h so mcpverbs.h's explore verb can reach packTaskPartitionText (same rule packtask.h follows). #include "tracelocus.h" // L4: the shared --from-trace / MCP from_trace bundle assembler (fromTraceBundleText) #include "callhierarchy.h" // H14/M13: the shared --callers/--callees / MCP find_* 1-hop computation (CLI == MCP rows) +#include "countfloor.h" // count-floor: a row's callers=/implementors= floor marker and its follow-up (CLI and MCP share it) #include "editcheck.h" // L4: the shared --edit-check / MCP edit_check contract-comparison core (editCheckBundleText) #include "slice.h" // lane/paper-slice: --slice=SYM[:VAR] — the ARISE-motivated def-use slice core (sliceBundleText) #include "editpreview.h" // card A1: the PRE-APPLY contract preview (editpreview::run) — BEFORE mcp.h, which diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..d4f4e0fa1 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -23,6 +23,7 @@ #include "packtask.h" // L4: the shared --pack-task / MCP explore+pack_task bundle assembler (packTaskBundleText) #include "partition.h" // the explore verb's `partition` argument (packTaskPartitionText) #include "tracelocus.h" // L4: the shared --from-trace / MCP from_trace bundle assembler (fromTraceBundleText) +#include "countfloor.h" // count-floor: a row's callers=/implementors= floor marker and its follow-up (CLI and MCP share it) #include "editcheck.h" // L4: the shared --edit-check / MCP edit_check contract-comparison core (editCheckBundleText) #include "callhierarchy.h" // H14/M13: the shared 1-hop call-hierarchy computation — find_symbol / find_referencing_symbols #include "graphlegend.h" // §H4 §3.4: the ONE counts_floor= marker + shared graph-count legend wording (CLI ≡ MCP) @@ -864,16 +865,28 @@ inline std::string grepEnclosingJson( const McpIndex& ix, std::span row's callers_floor=/floor_next=, by the same function over the same rows (countfloor.h). + std::vector> rowIds; + rowIds.reserve( encRows.size() ); for( const GrepEncRow& row : encRows ) { - if( !first ) + rowIds.push_back( row.ids ); + } + const std::vector floors = callerFloors( ing, g, rowIds, &valueRefIndexOf( ix ) ); + ENSURES( floors.size() == encRows.size() ); + std::string out = ",\"enclosing\":["; + for( std::size_t r = 0; r < encRows.size(); ++r ) + { + const GrepEncRow& row = encRows[r]; + if( r > 0 ) { out += ","; } - first = false; out += "{\"n\":\"" + mcpdetail::jsonEscape( row.chain ) + "\",\"callers\":" + std::to_string( row.callerCount ); + if( floors[r].isFloor ) + { + out += ",\"callers_floor\":1,\"floor_next\":\"" + mcpdetail::jsonEscape( floors[r].next ) + "\""; + } if( row.defCount > 1 ) { out += ",\"defs\":" + std::to_string( row.defCount ); @@ -1781,6 +1794,22 @@ inline void spliceForSectionStubLegend( std::string& doc, bool stubbed ) "spliceForSectionStubLegend: a section stubbed but the legend clause was not spliced into the returned document" ); } +// count-floor: the MCP twin of the CLI's lego COUNT clause (verbs_for.h, appended to sectionsStubNote) — inserted at the +// same header boundary, AFTER spliceForSectionStubLegend so the two clauses land in the CLI's order, and before the price. +inline void spliceForLegoCountLegend( std::string& doc, bool carries ) +{ + if( !carries ) + { + return; + } + const std::size_t legendAt = doc.find( " -->" ); + ASSUME( legendAt != std::string::npos, "spliceForLegoCountLegend: a lego count attribute rode but the header's closing \"-->\" was not found" ); + if( legendAt != std::string::npos ) + { + doc.insert( legendAt, rw::kForLegoCountLegend ); + } +} + inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) { if( doc.empty() ) @@ -2153,7 +2182,7 @@ inline std::optional forTaskText( const std::string& root, const st // L2 (round-1 lever B1): each renderer's OWN pre-cap row count, captured by the SAME call that renders // the real body — see verbs_for.h's identical CLI-twin comment for the full rationale. std::size_t legoPreCapCount = 0, composePreCapCount = 0; - std::string legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount ); } ); + std::string legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &ix.g.implementors ); } ); std::string composeStr, routeStr; if( !ix.g.composeEdges.empty() ) { @@ -2307,9 +2336,14 @@ inline std::optional forTaskText( const std::string& root, const st // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr // directly now rather than re-slicing it back out of the flushed memstream buffer. - if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr ) ) + // count-floor (CLI/MCP parity): the root prefix the CLI twin passes. sigsStr's rows are root-relative (flRootArg) and + // without the prefix this scan compared them against ABSOLUTE ing.files spellings, matched nothing and narrowed the + // whole block away (serialize.h narrowLegoToRenderedSigs' own R-E note): MCP `for` served no at all where the + // CLI served six rows. + const std::string mcpLegoRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); + if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr, mcpLegoRootPrefix ) ) { - legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount ); } ); // R-R: the re-render dropped the root its first render (above) passed + legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &ix.g.implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed } // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= @@ -2362,6 +2396,7 @@ inline std::optional forTaskText( const std::string& root, const st mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; } } + const bool mcpLegoCountAttrs = rw::legoCarriesCountAttrs( legoStr ); // false on a stub (its own attributes only) std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); @@ -2387,6 +2422,7 @@ inline std::optional forTaskText( const std::string& root, const st // path) returns immediately and costs this function nothing but the call. Done BEFORE priceForTaskRoot // so the clause's bytes are part of the est_tokens price exactly as the CLI's own version is. spliceForSectionStubLegend( out, mcpSectionsWillStub ); + spliceForLegoCountLegend( out, mcpLegoCountAttrs ); // count-floor: present-only, the CLI twin's clause // F5 (terminality round A 2026-09-05): PRICE the bundle instead of declaring it unpriced. The document is // complete here, so this is the same measurement the CLI twin makes over its own (deliberately different) // bytes: pricedRootAttr's ≤4-pass fixpoint at kBytesPerTokenDefault, spliced onto the root by the @@ -2432,8 +2468,9 @@ inline std::string legoText( const std::string& root, const std::string& type, R // H5: the same legend the CLI --lego prints, and (issue #66) the same adjacent clause defining the // graph_unindexed= the root below carries — CLI and MCP are one wording by construction. H1: the unproven_defs= // clause rides as its own comment beside the closed literal, exactly as on the CLI. - rw::emitTo( mem, "{}{}{}", kLegoLegend, graphUnindexedLegendComment( rw::graphGaugeClauses( ix.g ) ).c_str(), - unprovenDefsVerbComment( UnprovenDefsVerb::Lego, unprovenDefs > 0, "", doc, re.S ) ) print( "DEFINED" if DEF.search( text ) else "UNDEFINED" ) @@ -623,6 +639,41 @@ legend_arm "--connect" "$( answer js connect lib/reply.js:respondWith,lib/reply. legend_arm "--expand" "$( answer js expand lib/reply.js:respondWith --top-k=0 --legend=full )" legend_arm "--for" "$( answer js for "how does respondWith send the reply" --legend=full )" +echo "--- (H) the MAP's rows carry the same bit (the orient map is a surface too)" +# map_c FILE CALLER FILE CALLEE — the hedge column of that map row ("" when absent; NOROOT FAILs the arm) +map_c(){ + P map "$1" || return 1 + if [ "$PARSED" = "NOROOT" ]; then no "(H) map: no map document to read"; return 1; fi + MAPC="$( printf '%s\n' "$PARSED" | awk -v n="$2" -v f="$3" -v c="$4" '$1 == n && $2 == f && $3 == c { print $4 }' | sort -u | tr '\n' ' ' | sed 's/ $//' )" +} +map_arms(){ + local f="$TMP/js.map.xml" + rw js --top-k=500 --legend=full >"$f"; printf '%s' "$?" >"$f.rc" + ran_ok "(js) the default map" "$f" || return + map_c "$f" respondWith lib/reply.js send || return + if [ "$MAPC" = "name" ]; then ok "(H) map: respondWith's is marked via=\"name\"" + else no "(H) map: respondWith's is [${MAPC:-absent}], want via=\"name\""; fi + map_c "$f" direct lib/reply.js send || return + if [ "$MAPC" = "-" ]; then ok "(H) map: near miss — direct's (new Reply().send) stays plain" + else no "(H) map: direct's is [${MAPC:-absent}], want plain (a constructed receiver proves it)"; fi + map_c "$f" respond lib/application.js onerror || return + case "$MAPC" in *-*) no "(H) map: respond's is PROVEN (the false witness ctx.onerror())";; + *) ok "(H) map: respond's is absent or marked";; esac + legend_arm "map" "$f" + # near miss for the legend: a map whose rows carry no name-only pays nothing for it — a bare call to a function + # its own file defines is proven by scope, so its is plain and the comment must not ride + local g="$TMP/plainmap" gf="$TMP/plainmap.xml" + mkdir -p "$g" && printf 'function a() { return b(); }\nfunction b() { return 1; }\nmodule.exports = { a };\n' >"$g/m.js" + ( cd "$g" && "$BIN" . --no-cache --top-k=500 --legend=full >"$gf" 2>/dev/null ); printf '%s' "$?" >"$gf.rc" + ran_ok "(H) the plain-call map" "$gf" || return + map_c "$gf" a m.js b || return + if [ "$MAPC" != "-" ]; then no "(H) map near miss: a's is [${MAPC:-absent}], want a plain row (the premise of this arm)"; return; fi + P legend "$gf" || return + if [ "$PARSED" = "UNDEFINED" ]; then ok "(H) map near miss: a scope-proven stays plain and the via legend does not ride" + else no "(H) map near miss: the via legend rides a map whose rows carry no via=\"name\""; fi +} +map_arms + echo "--- (H) MCP twins (fresh TMPDIR: the MCP cache lives there)" mkdir -p "$TMP/mcp" # mcp ROOT TOOL SELECTOR OUT — one tools/call (then tools/list); the raw stream into OUT, the exit status into OUT.rc @@ -670,6 +721,27 @@ mcp_legend(){ } mcp_legend find_symbol lib/reply.js:respondWith mcp_legend find_referencing_symbols lib/reply.js:send +# the MAP's MCP twin (analyze): the same rows, the same bit, the same legend +mcp_map(){ + local out="$TMP/mcp.js.analyze" c d + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + "$( printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"analyze","arguments":{"path":"%s"}}}' "$CORPUS/js" )" \ + | TMPDIR="$TMP/mcp" "$BIN" --mcp >"$out" 2>/dev/null + printf '%s' "$?" >"$out.rc" + ran_ok "(H) MCP analyze" "$out" || return + P mcpmap "$out" || return + if [ "$PARSED" = "NOROOT" ]; then no "(H) MCP analyze: no map in the answer"; return; fi + c="$( printf '%s\n' "$PARSED" | awk '$1 == "respondWith" && $2 == "lib/reply.js" && $3 == "send" { print $4 }' | sort -u )" + d="$( printf '%s\n' "$PARSED" | awk '$1 == "direct" && $2 == "lib/reply.js" && $3 == "send" { print $4 }' | sort -u )" + if [ "$c" = "name" ]; then ok "(H) MCP analyze: respondWith's is marked via=\"name\" (the CLI map's bit)" + else no "(H) MCP analyze: respondWith's is [${c:-absent}], want via=\"name\""; fi + if [ "$d" = "-" ]; then ok "(H) MCP analyze: near miss — direct's stays plain" + else no "(H) MCP analyze: direct's is [${d:-absent}], want plain"; fi + P mcplegend "$out" analyze || return + if [ "$PARSED" = "DEFINED" ]; then ok "(H) MCP analyze: the answer defines via=\"name\" with a NOT sentence" + else no "(H) MCP analyze: the answer does not define via=\"name\" (\"name alone\" + \"does NOT mean\")"; fi +} +mcp_map echo "--- (H) parity: the hedge bit is value-equal across CLI and MCP, and across --callees and --expand" # cli_set ROOT VERB SEL — "name@file#H" lines of the CLI answer; mcp_set ROOT TOOL SEL FIELD — the same from MCP From d8c402b31a18f57c9217a66faeb3731629730b53 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:27:36 -0400 Subject: [PATCH 028/229] whereis: list the definitions, count the references (lean default) --whereis=SYM now lists every kind="def" row and counts the kind="ref" rows in one trailing element; the next= lists exactly those rows. --whereis-listing=defs|refs|all picks the rows (MCP: listing); all is the whole hit list. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. shown=/capped=/limit/offset and window the listed rows; hits= still counts every row; complete= reads the listing. Lossless: a row on HEAD's commit omits tip= and date= (tip is at= without +dirty, date is the new root head_date=). ref= stays on every row. Both legends (full and compact) define the new vocabulary. Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 27 +++++++ src/compactlegend.h | 6 +- src/crossref.h | 167 +++++++++++++++++++++++++++++++++++++++----- src/mcp.h | 9 ++- src/mcprefusal.h | 4 +- src/mcpverbs.h | 17 ++++- src/verbs_change.h | 3 +- 7 files changed, 207 insertions(+), 26 deletions(-) diff --git a/src/cli.h b/src/cli.h index 373fd9ed9..19c4868db 100644 --- a/src/cli.h +++ b/src/cli.h @@ -384,6 +384,7 @@ struct Config std::string_view laneBrief; // --brief=FILE: one non-blank line per lane, each ranked on its own bool whereisFlag = false; // --whereis was given at all (a bare/empty value still routes to the // handler and refuses loudly rather than falling through to the map) + std::string_view whereisListing; // --whereis-listing=defs|refs|all (default defs; crossref.h WhereisListing) std::string_view whereis; // --whereis=SYM: every ref whose TREE contains SYM, // HEAD first, with on-head= saying whether the live line has it at all. // Scans each ref's FULL tree; each distinct blob is read once (content- @@ -2092,6 +2093,15 @@ inline constexpr char kHelpTail[] = " is invisible. Add --with-history: a row then says v=\"never\" or v=\"removed\"\n" " with the commit, date and file that removed it. Remote-tracking refs are excluded\n" " (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR.\n" + " LISTING: by default only the kind=\"def\" rows are listed; the kind=\"ref\" rows are\n" + " counted in one element (see --whereis-listing).\n" + " --whereis-listing=WHICH with --whereis: which rows to list, defs (the default), refs or all\n" + " defs lists every kind=\"def\" row and COUNTS the kind=\"ref\" rows in one\n" + " element whose next= lists exactly them (refs). With no ref\n" + " row, or no def row (the mentions are then the answer), every hit is listed and the\n" + " root carries no listing=. all lists every row, the whole hit list. shown=/capped=\n" + " and --limit/--offset window the LISTED rows; hits= counts every row. Refused\n" + " without --whereis, and on an unknown value.\n" " --flags[=SUBSTR] the dark-content dashboard: what is built but switched OFF in this repo\n" " the dark-content dashboard: what is BUILT but OFF in this repo. Harvests all three gate\n" " patterns — #ifndef/#define header gates, CMake option(), and getenv() reads — and reports\n" @@ -4645,6 +4655,11 @@ inline void validateModifierGuards( Config& c ) noexcept rw::emitRaw( stderr, "ripwire: --and=/--not=/--grep-scope= modify --grep=STR — pass it too (e.g. ripwire --grep=stale --and=mcp)\n" ); c.ok = false; } + if( !c.whereisListing.empty() && !c.whereisFlag ) + { + rw::emitRaw( stderr, "ripwire: --whereis-listing=defs|refs|all modifies --whereis=SYM — pass it too (e.g. ripwire --whereis=parseArgs --whereis-listing=all)\n" ); + c.ok = false; + } // R-H: --grep-in= is the one grep modifier that ALSO applies to --regex (a regex hit lands in a span // exactly like a literal one), so its refusal tests both spellings rather than --grep= alone. if( !c.grepIn.empty() && c.grep.empty() ) @@ -5321,6 +5336,18 @@ inline Config parseArgs( int argc, char** argv ) noexcept } c.grepScope = v; } + else if( startsWith( a, "--whereis-listing=" ) ) + { + // A closed value set, refused on an unknown value (the --grep-in= rule): a typo must not quietly read + // as the default listing and hide the rows the caller asked for. + const std::string_view v = a.substr( 18 ); + if( v != "defs" && v != "refs" && v != "all" ) + { + rw::emitTo( stderr, "ripwire: --whereis-listing={} — unknown value (supported: defs|refs|all), e.g. --whereis-listing=all\n", std::string_view( v.data(), v.size() ) ); + c.ok = false; return c; + } + c.whereisListing = v; + } else if( startsWith( a, "--grep-in=" ) ) { // R-H span tiers. Same shape as --grep-scope= above (closed value set, refuse an unknown diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..45b0b186b 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -929,6 +929,10 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // 2026-10-01 freshness fix (crossref.h scanWorktree): present only on a checkout that differs from HEAD. { "worktree", "worktree=read|partial|unlisted: the checkout differs from HEAD (at= +dirty); each changed path is read from disk as ref=\"worktree\" rows replacing HEAD's, and on-head=/hits=/head_labels= count them; on-head= then reads the checkout, not HEAD's commit; partial = some changed path unreadable or a directory (nested repo, submodule), its HEAD rows may be stale; unlisted = git could not list the changes; either withholds complete=", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "hits", "more hits=N: rows after this page; page on with offset=next_offset", true, "more", MapHeaderRead::No, {}, "whereis" }, + // lean-answers lane (crossref.h WhereisListing / the tip-date hoist): each rides only the answer that carries it. + { "listing", "listing=defs: kind=def rows only, the kind=ref rows counted by refs count= (next= lists them); refs: kind=ref rows only; the window counts listed rows, hits= all", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "head_date", "head_date=: a hit without tip= date= is on HEAD's commit: tip= is at=, date= is this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "count", "refs count=N: the kind=ref rows not listed", true, "refs", MapHeaderRead::No, {}, "whereis" }, // the GREY ZONE of the same sweep: attributes the compact prose named in passing ("in/out, cx/ccx", " groups") but never // DEFINED as name= — legendcoveragecheck's default rows hold the definitional predicate, so each gets its reading here. // affected: src/verbs_change.h runAffected @@ -985,7 +989,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // doc-drift: src/docdrift.h { "filter", "filter=: the path filter this run was narrowed to; docs outside it were not checked", false, "doc-drift", MapHeaderRead::No, {}, "doc-drift" }, // whereis: src/crossref.h (the exhaustiveness claim) - { "complete", "complete=1: the scan read every ref AND this page lists every hit (absent: one of the two is a floor)", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "complete", "complete=1: the scan read every ref AND this page lists every hit of its listing (absent: one of the two is a floor)", false, "whereis", MapHeaderRead::No, {}, "whereis" }, // plan-lint: src/planlint.h { "file", "file=/dialect=: the plan read and whether the PLAN dialect was detected (dialect=0: nothing to lint)", false, "plan-lint", MapHeaderRead::No, {}, "plan-lint" }, // also defines dialect= { "cards", "cards=/ledger=: card rows found / whether the doc carries a ledger (ledger_line= names its line)", false, "plan-lint", MapHeaderRead::No, {}, "plan-lint" }, // also defines ledger= diff --git a/src/crossref.h b/src/crossref.h index 05e88df39..731787729 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -97,6 +97,7 @@ #include "infra/jsonesc.h" // shSingleQuote #include "serialize.h" // escapeXml #include "pageview.h" // §P8: pageWindow / pageDisclosure — the shared --limit/--offset contract +#include "nextverb.h" // nextAttrXml / nextFlag — the follow-up of the default listing #include "workspace.h" // wsdetail::segmentsOf #include "filter.h" // §P11.5: rw::pathTierOf — the shared source/test/doc ORDERING tier #include "infra/Diagnostics.h" // ASSUME / DISCLOSE @@ -141,6 +142,17 @@ constexpr std::uint32_t kMaxRefs = 512; // refusal bound — a swee constexpr std::size_t kStrayFilesPerRef = 12; constexpr std::size_t kWhereisHits = 60; +// WHICH ROWS a --whereis answer lists (--whereis-listing=, MCP `listing`). A where-defined question is answered by +// the kind="def" rows; the kind="ref" rows (8-59 per answer on the comparison-table cells, ~8 KB) supplied no gold +// item there. So the default lists the definitions and COUNTS the references in one element whose +// next= lists exactly those rows (Refs). All is the whole hit list, the pre-listing answer and the uncapped variant +// a lean answer is graded against (owner rule: a capped variant is graded against the uncapped one). +// Defs every kind="def" row; the kind="ref" rows counted. Degrades to All (no listing=, no ) when the hit +// list holds no ref row (nothing to elide) or no def row (the mentions ARE the answer then). +// Refs the kind="ref" rows only, in the hit list's order: the page. +// All every row. +enum class WhereisListing : std::uint8_t { Defs, Refs, All }; + // ── blob facts (the per-sha memo payload) ──────────────────────────────────────────────────────────────── // Everything this module needs to know about one blob, reduced from its bytes as they stream past and kept @@ -2820,6 +2832,79 @@ inline void writeWhereisFate( std::FILE* out, const WhereResult& res, const XmlE gitoracle::writeNameFate( out, res.sym, res.fate, ex ); } +// ── the listing and the hoist (lean-answers lane) ──────────────────────────────────────────────────────── + +// The rows one --whereis answer lists (see WhereisListing): indices into WhereResult::hits in the list's own order, +// the root's listing= value ("" when every hit is listed), and how many kind="ref" rows the element +// stands for (0 = no such element). +struct ListedHits +{ + std::vector rows; + std::string_view attr; + std::size_t refsElided = 0; +}; + +inline ListedHits listedHits( const WhereResult& res, WhereisListing listing ) +{ + const std::size_t defs = std::size_t( std::count_if( res.hits.begin(), res.hits.end(), []( const WhereHit& h ) { return h.isDef; } ) ); + const std::size_t refs = res.hits.size() - defs; + ListedHits out; + // Defs with nothing to elide (no ref row) IS the whole list; Defs with no def row would list nothing but a count, + // and then the mentions are the answer (a name the index does not define, a literal): both print every hit. + const bool lean = listing == WhereisListing::Defs && defs > 0 && refs > 0; + if( !lean && listing != WhereisListing::Refs ) + { + out.rows.resize( res.hits.size() ); + for( std::size_t i = 0; i < res.hits.size(); ++i ) { out.rows[ i ] = i; } + return out; + } + const bool wantDefs = lean; + for( std::size_t i = 0; i < res.hits.size(); ++i ) + { + if( res.hits[ i ].isDef == wantDefs ) { out.rows.push_back( i ); } + } + out.attr = wantDefs ? std::string_view( "defs" ) : std::string_view( "refs" ); + out.refsElided = wantDefs ? refs : 0; + ENSURES( out.rows.size() == ( wantDefs ? defs : refs ), "a listing holds exactly the rows of its kind" ); + return out; +} + +// HEAD's committer date, read off the first row on HEAD's commit (every such row carries the same date: the scan +// stamps a ref's rows with its tip's date). "" when no row is on HEAD's commit (a branch-only answer, hits="0"). +inline std::string whereisHeadDate( const WhereResult& res ) +{ + const std::string_view head = std::string_view( res.headSha ).substr( 0, 9 ); + for( const WhereHit& h : res.hits ) + { + if( !head.empty() && std::string_view( h.tip ).substr( 0, 9 ) == head ) + { + return h.date; + } + } + return {}; +} + +// Whether a row's tip= and date= are exactly the root's at= (as printed: 9 hex, without +dirty) and head_date=, so the +// row may omit both and a reader restores them losslessly. Compared on the PRINTED 9-hex spelling, the bytes a reader holds. +inline bool whereisRowOnHeadCommit( const WhereHit& h, std::string_view headSha, std::string_view headDate ) noexcept +{ + const std::string_view head = headSha.substr( 0, 9 ); + return !head.empty() && !headDate.empty() && std::string_view( h.tip ).substr( 0, 9 ) == head && h.date == headDate; +} + +// The listing a validated --whereis-listing= / MCP `listing` value names; "" (absent) is the default, defs. Both surfaces +// refuse any other value before they get here. +inline WhereisListing whereisListingOf( std::string_view v ) noexcept +{ + return v == "refs" ? WhereisListing::Refs : v == "all" ? WhereisListing::All : WhereisListing::Defs; +} + +// The : the same symbol, listing=refs — exactly the kind="ref" rows the default listing counted. +inline std::string whereisRefsNext( const WhereResult& res ) +{ + return nextFlag( "--whereis=", res.sym ) + " --whereis-listing=refs"; +} + // Contract-level defect: this verb said hits="2560" and printed 60, and // --limit/--offset were accepted and ignored, so a paging loop over it never advanced and never ended. // `pageLimit`/`pageOffset` (0 = un-paginated) window the hit list, which is already deterministically @@ -2830,18 +2915,24 @@ inline void writeWhereisFate( std::FILE* out, const WhereResult& res, const XmlE // // Paging lives in its own entry point rather than as two defaulted parameters on writeWhereis() so the // un-paginated contract — the one the MCP `whereis` verb calls — keeps its exact three-argument shape. -inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset ) +inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset, + WhereisListing listing ) { std::vector esc; const XmlEscaper ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; + // The LISTED rows (WhereisListing): indices into res.hits, in the hit list's own order, so a listing is a + // subsequence of the whole list and its rows print byte-identically to the same rows of listing=all. + const ListedHits listed = listedHits( res, listing ); + // The emitted window. An explicit --limit overrides maxHits (the caller's 60-hit display default, or // SIZE_MAX under --detail); --offset skips whole rows and clamps at the end, so offset-past-the-end is // an empty page rather than an out-of-range read. maxHits can be SIZE_MAX, which pageWindow's int limit // cannot carry — clamp the "no explicit --limit" arm to the row count instead of overflowing it. - const int rowCap = pageLimit > 0 ? pageLimit - : ( maxHits >= res.hits.size() ? int( res.hits.size() ) : int( maxHits ) ); - const PageWindow hitPage = pageWindow( res.hits.size(), rowCap, pageOffset ); + const std::size_t listedCount = listed.rows.size(); + const int rowCap = pageLimit > 0 ? pageLimit + : ( maxHits >= listedCount ? int( listedCount ) : int( maxHits ) ); + const PageWindow hitPage = pageWindow( listedCount, rowCap, pageOffset ); rw::emitRaw( out, "" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..a2c67ce27 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4129,8 +4129,31 @@ struct SigRowFacts std::string_view topNext = {}; // L-W (forpage.h): the r=1 row's next= when the caller decided // the answer is THIN — the file-grain widening page. Empty ⇒ // the body follow-up (--expand=FILE:NAME) exactly as before. + bool elideZero = false; // lean-answers lane: the --for lens (CLI and MCP) omits cx=/ccx=/in= + // when "0" — its legend says an absent one IS 0. Only where in= + // is measured on every row (fanIn supplied): without fanIn an + // absent in= means "not measured" (P2.4), so nothing is elided. }; +// Whether this row's zero cx=/ccx=/in= are omitted (SigRowFacts::elideZero): the lens asked AND in= is measured here. +inline bool sigRowElidesZero( const SigRowFacts& facts, NodeId id ) noexcept +{ + return facts.elideZero && facts.metrics && facts.fanIn != nullptr && id < facts.fanIn->size(); +} + +// The bytes the zero elision removed from one row: what the row would have spelled as ` cx="0"` / ` ccx="0"` / +// ` in="0"`. The --for ladder still CHARGES them (SigEntry::elidedBytes), so the rows the budget keeps are exactly the +// rows it kept before the elision — the change is a format change, never a different selection. +inline std::size_t sigRowZeroElidedBytes( const Symbol& s, const SigRowFacts& facts, NodeId id ) noexcept +{ + if( !sigRowElidesZero( facts, id ) ) + { + return 0; + } + return ( s.cx == 0 ? sizeof( " cx=\"0\"" ) - 1 : 0u ) + ( s.ccx == 0 ? sizeof( " ccx=\"0\"" ) - 1 : 0u ) + + ( ( *facts.fanIn )[ id ] == 0 ? sizeof( " in=\"0\"" ) - 1 : 0u ); +} + // P7 (terminality round A, lane R, 2026-09-05): a lens row's own file, spelled root-relative exactly as the // wrapper it replaced was — the ONE spelling both dialects' rows (p= / "p") and the r=1 next= use. inline std::string lensRowPath( const IngestResult& ing, std::uint32_t fileId, std::string_view rootArg ) @@ -4184,7 +4207,18 @@ inline std::string sigRowHead( const IngestResult& ing, NodeId id, const SigRowF rw::formatTo( rankAttr, sizeof( rankAttr ), " r=\"{}\"", facts.rank ); } char tail[ 224 ]; - if( facts.metrics ) + if( facts.metrics && sigRowElidesZero( facts, id ) ) + { + // lean-answers lane: the same three facts, each omitted at 0 (the lens legend: absent = 0) + char cxAttr[ 24 ]; cxAttr[ 0 ] = '\0'; + char ccxAttr[ 24 ]; ccxAttr[ 0 ] = '\0'; + char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; + if( s.cx != 0 ) { rw::formatTo( cxAttr, sizeof( cxAttr ), " cx=\"{}\"", s.cx ); } + if( s.ccx != 0 ) { rw::formatTo( ccxAttr, sizeof( ccxAttr ), " ccx=\"{}\"", s.ccx ); } + if( ( *facts.fanIn )[ id ] != 0 ) { rw::formatTo( inAttr, sizeof( inAttr ), " in=\"{}\"", ( *facts.fanIn )[ id ] ); } + rw::formatTo( tail, sizeof( tail ), "{}{}{}{}{}{}>", rw::cstr( cxAttr ), rw::cstr( ccxAttr ), rw::cstr( inAttr ), facts.lens, facts.pure, rw::cstr( rankAttr ) ); + } + else if( facts.metrics ) { char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; if( facts.fanIn && id < facts.fanIn->size() ) @@ -4634,9 +4668,12 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: std::string_view topRowNext = {}, // L-W (forpage.h): the r=1 row's next= when the caller // judged the answer THIN (the widening page); "" ⇒ the // --expand body follow-up, byte-identical to before. - SigsCutReport* cutOut = nullptr ) // cut-fix lane A: the tag's shown/total/docs_dropped/capped, + SigsCutReport* cutOut = nullptr, // cut-fix lane A: the tag's shown/total/docs_dropped/capped, // for the caller's legend splices. Lens path only; zeroed // (nothing cut) on every other path. + bool elideZeroMetrics = false ) // lean-answers lane: the --for lens's zero cx=/ccx=/in= omitted + // (SigRowFacts::elideZero); its legend says absent = 0. Off ⇒ + // byte-identical (pack-task, from-trace and the map stay as they were). { if( cutOut ) { @@ -4743,6 +4780,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: std::uint32_t globalRank = 0; // 1-based global rank — the ladder's only rank input std::size_t fileSlot = 0; // index into sigFiles (P7: the ladder releases the file's notes at liveCount 0) std::string head; // the exact "" opening tag + std::size_t elidedBytes = 0; // lean-answers lane: the zero cx=/ccx=/in= bytes `head` omits, still CHARGED by + // entryCost so the ladder keeps exactly the rows it kept before the elision std::string doc; // RAW doc text after the rank tiers ("" ⇒ no child) std::string sig; // RAW one-line signature after the rank tiers std::string notes; // W3-N2: this symbol's note children, PRE-RENDERED (the JSON sibling's shape) @@ -4856,7 +4895,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } } - std::string head = sigRowHead( ing, id, SigRowFacts{ metrics, fanIn, qbuf, pure, globalRank, topRowNext }, esc, rootArg ); // d1: rank fact (ladder path) + const SigRowFacts rowFacts{ metrics, fanIn, qbuf, pure, globalRank, topRowNext, elideZeroMetrics }; + std::string head = sigRowHead( ing, id, rowFacts, esc, rootArg ); // d1: rank fact (ladder path) std::string doc = docCommentBefore( src, a ); redactInPlace( doc, redact ); @@ -4874,6 +4914,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: e.globalRank = globalRank; e.fileSlot = fileSlot; e.head = std::move( head ); + e.elidedBytes = sigRowZeroElidedBytes( ing.symbols[ id ], rowFacts, id ); e.doc = std::move( doc ); e.sig = std::move( sig ); e.notes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 key + W3-N2 pre-render @@ -4898,7 +4939,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { return 0; } - std::size_t c = e.head.size() + 4; // "" + "" + std::size_t c = e.head.size() + e.elidedBytes + 4; // "" + "" (+ the elided zeros, see SigEntry) if( !e.doc.empty() ) { c += 11 + escapeXml( e.doc, esc ).size(); // "" + "" diff --git a/src/verbs_for.h b/src/verbs_for.h index dfe6a6967..6ea58ea89 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -736,13 +736,13 @@ inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, inline constexpr std::string_view kForCompactLegendRoot = "bundle=/bodies=/reason= the body posture; "; inline constexpr std::string_view kForCompactLegendRows = - "d: cx= ccx= complexity, in= callers, churn= amp= change, clone= tested= 1, sc= scope, id=p::sc::n; " + "d: cx= ccx= complexity, in= callers (absent cx/ccx/in = 0), churn= amp= change, clone= tested= 1, sc= scope, id=p::sc::n; " "total= shown= capped=1 if cut"; // …and the same clause for a bundle whose served rows carry NO scope (a corpus of free functions, a markdown // tree): 22 B of sc= reading removed, nothing else. Two constants rather than one built at runtime because this // dialect's whole contract is a pinned byte count per schema, and a pin reads a constant. inline constexpr std::string_view kForCompactLegendRowsNoScope = - "d: cx= ccx= complexity, in= callers, churn= amp= change, clone= tested= 1; " + "d: cx= ccx= complexity, in= callers (absent cx/ccx/in = 0), churn= amp= change, clone= tested= 1; " "total= shown= capped=1 if cut"; // L1 fix round (2026-09-19, rv-r1-L1 HIGH-1): task= (the root's echo of the query), next= (the follow-up on a row), pure= // (a row flag) and the rows rode every default answer with no reading in either dialect; @@ -983,7 +983,7 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout "invisible neighbours without hurting, no measured recall lift; see bench/ANSWERQUALITY.md]"; } h += ": reusable building blocks + quality facts for what you're about to touch " - "(cx=complexity ccx=cognitive in=reuse-count churn=recent-commits amp=change-amplification clone=1(duplicated) tested=1) " + "(cx=complexity ccx=cognitive in=reuse-count, each absent when 0; churn=recent-commits amp=change-amplification clone=1(duplicated) tested=1) " "— prefer composing/reusing these; watch the high-churn/high-amp/cloned ones"; // BOTH readings are present-only (PR #215 review): sc= when a served row carries one, route= when the root // carries the attribute. A reading with nothing to define is bytes the bundle could have spent on a row, and @@ -2990,7 +2990,8 @@ std::optional runForLens( const MainDispatch& d ) &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then forTopRowNext, // L-W: the widening page on a thin answer, else the body - &forSigsCut ); // cut-fix lane A: which cut readings the tag owes + &forSigsCut, // cut-fix lane A: which cut readings the tag owes + /*elideZeroMetrics=*/true ); // lean-answers lane: zero cx=/ccx=/in= omitted (legend: absent = 0) }, sigsStr ); if( !sigsPreRendered ) @@ -3472,7 +3473,8 @@ std::optional runForLens( const MainDispatch& d ) packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, /*hasRelevanceFloor=*/true, nullptr, nullptr, nullptr, // LB-A: the direct-emission degrade path selects identically - forTopRowNext ); // L-W: same next= rule on the degrade path + forTopRowNext, // L-W: same next= rule on the degrade path + nullptr, /*elideZeroMetrics=*/true ); // lean-answers lane: the same row spelling } if( legoPreRendered ) { From 9fa1f68acfa2ca1f352977ea15358dde8f6b548e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:27:55 -0400 Subject: [PATCH 030/229] map: a row's bare callees as one comma-joined calls= attribute A map row whose every callee would be a bare (no per-edge attribute such as prov=, no comma in a name) now says them once as calls="a,b,c" on the row itself, the same names in the same order. One attributed edge keeps the rows for the whole symbol, so no per-callee attribute is ever dropped to join. Both legends define it. Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 4 +++- src/serialize.h | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 45b0b186b..764019138 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -60,7 +60,7 @@ struct CompactLegendSpec inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { // ── the ranked-map family (root ) — hinted by the map-shaping flag that rode along ── - { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data" }, + { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees (or ); the header comment is data" }, { "r", "map-diff", "the ranked map anchored at at=: what the diff touched, the map's row vocabulary" }, { "r", "metrics", "the ranked map with per-symbol metrics: in/out, cx/ccx, loc, params, nest, humps/deep, locals, cbo, amp, tested, ev" }, { "r", "around", "call neighbourhood of of=: depth= hops, fanout= kept per hop; absent rows lie outside that boundary" }, @@ -566,6 +566,8 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // ctxRootOpen) rides a bundle whose task was routed to a ranker: --pack-task always, MCP explore unless no_route; it is // ELEMENT-qualified on . compactlegendcheck (D36)/(D37). { "r", ": rank N in this ranking, rows in r= order", true, "d" }, + // lean-answers lane (serialize.h, the map's callee rows): present only where a row joined its callees. + { "calls", ": its resolved callees comma-joined in order, the rows of a row none of whose calls carries an attribute", true, "s" }, { "ccx", ": cyclomatic/cognitive complexity", true, "d" }, { "in", ": N callers in the index (absent: not measured)", true, "d" }, { "amp", ": direct callers + files sharing a commit with its file (absent at 0)", true, "d" }, diff --git a/src/serialize.h b/src/serialize.h index a2c67ce27..9b827113e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2766,8 +2766,8 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // written once the document it describes has been measured (PHASE 2 below) and the legend's own bytes // are part of what it describes. std::string legend = outProv - ? "" - : ""; + ? "" + : ""; // EXTENT HONESTY (src/extentsuspect.h): how many definitions carry extent_suspect= corpus-wide — the header's // extent_suspect_syms= — and the row + header readings, appended ONLY when that is non-zero, so a corpus with // nothing flagged keeps every byte of this legend. @@ -3383,6 +3383,36 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto std::string evRun = " ev=\"" + std::to_string( s.ev ) + "\" ev_floor=\"1\" ev_why=\"" + evWhyString( s ) + "\""; w.write( evRun ); } + // lean-answers lane: a symbol whose every callee row would be a BARE (no per-edge attribute, no + // comma in a name) says them once, comma-joined, as calls="a,b,c" on the row itself — the same names in + // the same order, ~11 B a callee less. One attributed edge (prov=, or any later per-callee hedge) keeps + // the rows for the whole symbol, so no per-callee attribute ever has to be dropped to join. + const bool joinCallees = outOff[id] < outOff[id + 1] && [ & ] + { + for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) + { + const bool attributed = outProv && e < outProv->size() && ( *outProv )[e]; + if( attributed || ing.symbols[ outTargets[e] ].name.find( ',' ) != std::string::npos ) + { + return false; + } + } + return true; + }(); + if( joinCallees ) + { + w.write( " calls=\"" ); + for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) + { + if( e > outOff[id] ) + { + w.write( "," ); + } + w.write( escapeXml( ing.symbols[ outTargets[e] ].name, esc ) ); + } + w.write( "\"/>" ); + continue; + } w.write( ">" ); for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) From e7dc38c1f785f4521ab9ebef7c5cc6bffca2cef2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:27:56 -0400 Subject: [PATCH 031/229] mcp: the legend session opens on its first answer (ref posture by default) A stdio session no longer waits for a read of ripwire://legend-dict. Its first answer the legend dictionary can reduce is served inline and whole, every definition it carried counts as served, and later answers take the existing legend="ref" posture: rows first, each definition sent only the first time the session meets it, last. The first ref answer of a session opened this way also carries the dictionary core, so every definition a ref answer relies on was sent by this process. legend:"compact"/"full" still keep one answer inline; the HTTP transport holds no session; the CLI is unchanged. Co-Authored-By: Claude Opus 5.5 --- src/legenddict.h | 56 ++++++++++++++++++++++++++++++++++++++++++------ src/mcp.h | 19 +++++++++++----- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/legenddict.h b/src/legenddict.h index 285c5ed26..61aabffe0 100644 --- a/src/legenddict.h +++ b/src/legenddict.h @@ -9,10 +9,12 @@ // core (below), one purpose line per compact schema (compactlegend.h kCompactLegendSpecs), one reading per completeness // term (kCompactCompletenessTerms), and the MCP `for` dialect's fixed clauses (graphlegend.h, lexical.h, forpage.h, // serialize.h — named constants, so these are the very bytes the headers append). An entry is served in THIS process -// once its bytes went out verbatim: the core when the client reads ripwire://legend-dict (mcp.h), every other entry the -// first time an answer carries it. The session starts in the inline posture and switches to ref only when the core was -// read; after that an answer DROPS exactly the entries this process already served and KEEPS the rest inline, in a -// trailing comment, marking them served. So every attribute a ref answer carries is defined by bytes this process +// once its bytes went out verbatim: the core when the client reads ripwire://legend-dict (mcp.h) or when the session's +// first ref answer carries it, every other entry the first time an answer carries it. The session starts in the inline +// posture and switches to ref when the core was read or — the MCP stdio default since the lean-answers lane — once its +// first answer this dictionary can reduce went out inline (openSessionWith); after that an answer DROPS exactly the +// entries this process already served and KEEPS the rest (the core included, until served) inline, in a trailing +// comment, marking them served. So every attribute a ref answer carries is defined by bytes this process // already sent, or by that answer itself — never by a dictionary the reader was only pointed at. // // WHY LAZY AND NOT ONE FETCH. The prereg registered a ~5.5 KB dictionary fetched once (the distinct compact clauses the @@ -781,7 +783,7 @@ inline RefParts assembleRef( std::string_view view, const RefShape& s, const Hea // native legend for MCP `for`. Only called once `session.refOn`. inline RefOutcome applyRefPosture( std::string& doc, LegendSession& session ) { - EXPECTS( session.refOn, "the ref posture exists only after the session was served the dictionary core" ); + EXPECTS( session.refOn, "the ref posture exists only once the session opened it (a core read, or its first inline answer)" ); session.served.resize( kEntryCount, 0 ); const std::string_view view = doc; const std::optional shape = detail::inferRefShape( view ); @@ -789,11 +791,29 @@ inline RefOutcome applyRefPosture( std::string& doc, LegendSession& session ) { return RefOutcome::NotApplicable; } - const std::optional head = detail::reduceHead( view, *shape, session ); + std::optional head = detail::reduceHead( view, *shape, session ); if( !head ) { return RefOutcome::NotApplicable; } + // lean-answers lane: a session OPENED by its first inline answer (openSessionWith, the MCP default) was never sent the + // core — the reading of itself. Its first ref answer carries every unserved core entry in the + // same trailing comment as any other unserved entry, so the honesty rule above holds without a resource read. + std::string core; + for( std::size_t id = 0; id < kCoreCount; ++id ) + { + if( !session.isServed( id ) ) + { + head->touched.push_back( id ); + core += core.empty() ? "" : " "; + core.append( entryBody( id ) ); + core += '.'; + } + } + if( !core.empty() ) + { + head->residual.insert( head->residual.begin(), "" ); + } detail::RefParts parts = detail::assembleRef( view, *shape, *head ); const std::size_t refBytes = parts.bytes( *shape ); for( const std::size_t id : head->touched ) { session.markServed( id ); } // sent now, in either posture @@ -820,4 +840,28 @@ inline RefOutcome applyRefPosture( std::string& doc, LegendSession& session ) return RefOutcome::Ref; } +// THE MCP DEFAULT (lean-answers lane): a stdio session that never read ripwire://legend-dict is OPENED by its first answer +// this dictionary can reduce. That answer is served inline and unchanged — its whole legend, the definitions it needs — +// and every entry it carried counts as served; the session then takes the ref posture, whose first ref answer carries the +// core (applyRefPosture). An answer the dictionary cannot reduce (JSON, plain text, a legend it does not hold) leaves the +// session closed. `doc` is read, never changed. Returns whether the session opened. +inline bool openSessionWith( std::string_view doc, LegendSession& session ) +{ + EXPECTS( !session.refOn, "a session opens once; an open one takes the ref posture instead" ); + session.served.resize( kEntryCount, 0 ); + const std::optional shape = detail::inferRefShape( doc ); + if( !shape ) + { + return false; + } + const std::optional head = detail::reduceHead( doc, *shape, session ); + if( !head ) + { + return false; + } + for( const std::size_t id : head->touched ) { session.markServed( id ); } // sent now, inline + session.refOn = true; + return true; +} + } // namespace rw::legenddict diff --git a/src/mcp.h b/src/mcp.h index e36bf308c..f6baf4f5d 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1034,9 +1034,9 @@ inline std::string mcpLegendPointer( const McpDispatchPolicy& policy ) { return {}; } - return " Legends: each answer defines its own attributes until this session reads the resource ripwire://legend-dict " - "once (dictv=" + legenddict::dictionaryVersion() + "); after that answers list rows first, carry a definition " - "only the first time the session meets it, and end with . legend:\"compact\" on a call " + return " Legends: the session's first answer defines its own attributes inline (dictv=" + legenddict::dictionaryVersion() + + "); later answers list rows first, carry a definition only the first time the session meets it, and end with " + "; ripwire://legend-dict/full holds every definition. legend:\"compact\" on a call " "that takes it keeps that answer's legend inline."; } @@ -1533,9 +1533,14 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // and only on the verbs whose legend the dictionary holds: the ones that declare `legend` (compacted first, // then reduced) and `for` (its native dialect). `legend:"ref"` before the read is the compact posture: an // answer is never ref before the dictionary was served. + // lean-answers lane: the session no longer waits for that read. Its FIRST such answer is served inline, whole, + // and OPENS the session (legenddict::openSessionWith); every later one takes the ref posture, the first of them + // carrying the core. legend:"compact"/"full" still keep one answer inline; an HTTP request holds no session. legenddict::LegendSession* const legendSession = policy.legendSession; - const bool legendRefPosture = legendSession != nullptr && legendSession->refOn - && ( legendDeclaredHere ? ( legendArg.empty() || legendArg == "ref" ) : name == "for" ); + const bool legendSessionPosture = legendSession != nullptr + && ( legendDeclaredHere ? ( legendArg.empty() || legendArg == "ref" ) : name == "for" ); + const bool legendRefPosture = legendSessionPosture && legendSession->refOn; + const bool legendOpensSession = legendSessionPosture && !legendSession->refOn; // ── W3FIX H4/M5: every NUMERIC argument through the ONE guarded reader ───────────────────────── // @@ -1657,6 +1662,10 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo } legenddict::applyRefPosture( compacted, *legendSession ); } + else if( legendOpensSession ) + { + legenddict::openSessionWith( *body, *legendSession ); // served as-is; opens the session when reducible + } return "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"" + mcpdetail::jsonEscape( *body ) + "\"}],\"_index\":\"" + mcpdetail::jsonEscape( stamp ) + "\"" + mcpReingestField( passesAtEntry ) + mcpFreshFields( passesAtEntry ) From 26a7ea0a0dc81f672a4e4c397921c1036efaaa13 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:32:31 -0400 Subject: [PATCH 032/229] test(gate): mcpreloadasancheck - one MCP session rebuilt in place 20 times under ASan, answers checked against the CLI A cached object that points into the MCP server's index dangles when the index is rebuilt in place (the ValueRefIndex use-after-free). The gate drives one long-lived `--mcp` session on the sanitizer build through every rebuild cause - a chmod, a new non-source file, an empty directory (stamp-keeping), a file rewrite and the edit verbs (content), a root switch, a dirty git root, a workspace, and a memory release - and compares each answer (find_symbol, find_referencing_symbols, impact, uses, path_between, analyze, rank_by, grep, fetch_body handles, dead-code) with a one-shot CLI run on the same tree state. Every rebuild is proved by the server's own rebuilt=1; zero sanitizer reports; SKIPs by name without a sanitizer build. Red on the build that still had the bug, green after the fix. Registered in test/regression.sh, the gate count regenerated by docs/gatecount_build.py (665), a pargates hang budget, and the CI asan job. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 7 + CHANGELOG.md | 10 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/mcpreloadasancheck.sh | 894 +++++++++++++++++++++++++++++++++ test/pargates.py | 3 + test/regression.sh | 2 +- 8 files changed, 923 insertions(+), 9 deletions(-) create mode 100644 test/mcpreloadasancheck.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ea1ffb85..64a1642c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -723,6 +723,13 @@ jobs: - name: Sanitizer-only class — crashsweepcheck far-future timestamp and fixed-name shapes (test/crashsweepcheck.sh) run: RIPWIRE_BIN=asan/ripwire RIPWIRE_ASAN_BIN=asan/ripwire bash test/crashsweepcheck.sh + # A lifetime class only a sanitizer shows: one long-lived `--mcp` session rebuilt in place (a chmod, a new non-source + # file, an edit, a root switch, a memory release) and queried again each time. A cached ValueRefIndex once outlived + # the index it pointed into (heap-use-after-free) and no single-query gate could see it. The gate drives that session + # on this binary for 20 cycles and checks every answer against a one-shot CLI run; one binary plays both roles. + - name: Sanitizer-only class — MCP index reloads under ASan (test/mcpreloadasancheck.sh) + run: RIPWIRE_BIN=asan/ripwire bash test/mcpreloadasancheck.sh + - name: det-gate — 2-run byte-identical diff (ASan binary) run: | ./asan/ripwire test/fixture --no-cache >/tmp/run_a.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..3dbaae33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `test/mcpreloadasancheck.sh`: MCP index reloads under a sanitizer build + +A cached object that points into the MCP server's index dangles when the index is rebuilt in place; a cached +`ValueRefIndex` once did exactly that (heap-use-after-free after a rebuild that kept its content stamp), and no single-query +gate can see it. The gate runs one long-lived `--mcp` session on the sanitizer binary for 20 cycles of query, stamp-keeping +mutation (chmod, a new non-source file, an empty directory), content mutation (file rewrite or the edit verbs), root +switch, workspace and memory release, and checks every answer against a one-shot CLI run on the same tree state. It +asserts each rebuild happened (`rebuilt=1`), zero sanitizer reports, and SKIPs by name when no sanitizer build is present. +It goes red on the build that still had that bug and is green after the fix. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/README.md b/README.md index 26dd08715..1e1af4329 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-664 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +665 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **664 gate scripts** and is the authoritative list; +`test/regression.sh` names **665 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..30956ac0c 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` | 664 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **664 gate scripts**, all of which exist on disk. +naming **665 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 664. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 665. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..3868b0a5c 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 = [ - ["664 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "664", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh new file mode 100644 index 000000000..db0daa043 --- /dev/null +++ b/test/mcpreloadasancheck.sh @@ -0,0 +1,894 @@ +#!/usr/bin/env bash +# mcpreloadasancheck.sh — ONE long-lived `ripwire --mcp` session on a sanitizer build, put through every index +# REBUILD path 20 times over, with each answer checked against a fresh one-shot CLI run on the same tree state. +# +# THE CLASS. The MCP server keeps one McpIndex for its whole life and rebuilds it IN PLACE when the tree moves. +# Anything derived from the index that holds pointers or ids into it dangles the moment the index is reassigned. +# A cached ValueRefIndex shipped doing exactly that: it outlived the IngestResult it pointed into after a rebuild +# that kept its content stamp (a chmod, a new non-source file) and read freed bindings — a heap-use-after-free that a +# plain build only shows as different rows. It is a LIFETIME bug: no single query shows it, only query → rebuild → +# query on one process does, and only a sanitizer makes it loud. `ix.ing` is rewritten in exactly two places +# (getIndex's rebuild and releaseMcpIndexMemory), so this gate drives both, and every cause that reaches them. +# +# EVERY CACHED VIEW THE INDEX HOLDS (src/mcpindex.h McpIndex), and the query that reads it here: +# ing IngestResult (symbols, references, import bindings) every verb below; fetch_body spans +# g Graph (edges, canonId) find_symbol / find_referencing_symbols / impact / path_between +# valueRefs ValueRefIndex, built lazily, points INTO ing find_symbol valueRefs/valueCallees, find_referencing_symbols, impact, uses, path_between +# rank, prDisclosure PageRank of the graph (+ what the iteration did) analyze, rank_by (pagerank|authority|rrf), under --top-k=6 so rank picks the rows +# workingSetHash, the uncommitted-diff mask the rank was biased to analyze on a DIRTY git root, against a cold server on the same tree +# isCleanWorkingSet +# fileMtime/Size/Ctime, per-file stat + byte-hash arrays, the content stamp fetch_body with a handle minted before a mutation (must refuse, not +# fileByteHash, the handles carry serve the old body) and with a fresh one (must equal the bytes on disk) +# contentHash +# dirMtime, watcher the staleness watch-list and the FS-event watcher the three stamp-keeping mutations below +# incrementalPasses, process history behind _reingest / _fresh asserted through RIPWIRE_MCP_TIMINGS rebuilt= on every request +# lastReingestFiles, +# lastStale/ChangedFiles +# mcpWorkspaceRegistry the multi-root key → roots table find_symbol with `paths` [A,B] +# the ingest cache blob (ix.cacheFile) the warm rebuild reads every rebuild goes through it +# dead-code is a CLI-only verb and holds no index cache; it is read on the CLI side and cross-checked against the +# MCP valueRefs (a function with a value-site is not dead, a static one with neither is). +# +# FIXTURES (generated, never committed: nothing here may move another gate's crawl): JS and TS (import bindings, a +# callback passed to run(), a table), Go (a same-package handler passed and stored), Python (imports, a list, a dict), +# C (a function pointer, a table, static functions). Per language a `gen` file is regenerated every cycle. +# +# ONE CYCLE (k = 1..N), all on one session started with --top-k=6, ASAN_OPTIONS halt/abort + a log dir: +# Q0 query every view on tree A, compare with the CLI on the same state +# M1 a STAMP-KEEPING mutation (k%3: chmod / a new non-source file / an empty directory): content unchanged, so +# contentHash and the reference count survive. Rebuild must fire (rebuilt=1) and every answer must still match. +# This is the arm that went red at the pre-fix head. +# M2 a CONTENT mutation: a gen function added and the previous one removed, a value reference retargeted, written +# by the shell on even cycles and through replace_symbol_body / insert_after_symbol on odd ones (invalidateMcpIndex) +# RS root switch: dirty git root B (its rank checked against a cold server), then the workspace [A,B], then back to A +# RL memory release (cycles 1, 6, 11, ...): --max-memory has a 64M floor and is server-wide, and a sanitizer process +# that has run the whole cycle never falls back under it, so this arm gets its own short process (same binary, +# same options): root R (2400 files) carries the footprint over the floor; the NEXT call on R releases the index +# (releaseMcpIndexMemory) and rebuilds the very same tree, then A again +# Each rebuild is proved by the server's own timing line (rebuilt=1), so an arm that no longer reaches its path FAILs +# instead of passing blind. A mutation that no compared answer shows (the new reference equal to the old) FAILs too. +# +# LeakSanitizer is OFF here (detect_leaks=0, as CI's macOS asan leg): the subject is lifetime, not leaks, and a leak report at +# the exit of a long-lived server would be a different finding with its own gate. +# +# Usage: +# bash test/mcpreloadasancheck.sh [PLAIN_BIN] # CLI reference binary (default build/ripwire); ASan binary asan/ripwire +# RIPWIRE_ASAN_BIN=path bash test/mcpreloadasancheck.sh +# RIPWIRE_BIN=asan/ripwire bash test/mcpreloadasancheck.sh # CI's asan job: one binary plays both roles +# RIPWIRE_RELOAD_CYCLES=20 (default) +# No sanitizer build (no asan/ripwire, llvm@22 tree: cmake -DRIPWIRE_ASAN=ON, see CONTRIBUTING.md) → SKIP, named. +# Exits non-zero on any failure. The memory-release arm needs two host premises (R's parse inside the memory guard's five-second +# clock; the footprint back under the 64M floor after the release): when none of its attempts met them it prints a named SKIP row +# and the other arms still decide the verdict. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" +. "$ROOT/scripts/gatebound.sh" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +CYCLES="${RIPWIRE_RELOAD_CYCLES:-20}" +SESSION_CAP="${RIPWIRE_GATE_SESSION_CAP_SEC:-1500}" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +skip(){ printf ' SKIP %s\n' "$*"; } # an ABSENT PRECONDITION with a named reason — never a silent pass + +# the premises that are FAILURES, not skips: the binary under test and the tools the driver needs +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +command -v git >/dev/null 2>&1 || { echo "git required"; exit 2; } +case "$CYCLES" in ''|*[!0-9]*) echo "RIPWIRE_RELOAD_CYCLES='$CYCLES' is not a number"; exit 2;; esac +[ "$CYCLES" -ge 1 ] || { echo "RIPWIRE_RELOAD_CYCLES must be >= 1"; exit 2; } +BIN_VER="$( "$BIN" --version 2>&1 )" || { echo "$BIN does not run (--version failed): $BIN_VER"; exit 2; } +case "$BIN_VER" in ripwire*) ;; *) echo "$BIN --version printed no ripwire banner: $BIN_VER"; exit 2;; esac + +# is a binary sanitizer-instrumented? (the runtime symbol cachefuzzcheck's bound_mode_of reads) +is_asan(){ LC_ALL=C grep -q -a '__asan_init' "$1" 2>/dev/null; } + +# the sanitizer binary: RIPWIRE_ASAN_BIN, else the binary under test when it is one (RIPWIRE_BIN=asan/ripwire), else asan/ripwire +if [ -n "${RIPWIRE_ASAN_BIN:-}" ]; then + ASAN_BIN="$RIPWIRE_ASAN_BIN"; [ "${ASAN_BIN#/}" = "$ASAN_BIN" ] && ASAN_BIN="$ROOT/$ASAN_BIN" +elif is_asan "$BIN"; then + ASAN_BIN="$BIN" +else + ASAN_BIN="$ROOT/asan/ripwire" +fi +echo "mcpreloadasancheck: BIN=$BIN ASAN_BIN=$ASAN_BIN cycles=$CYCLES" + +# the named, expected missing premise: this checkout has no sanitizer build. Announced BEFORE any PASS row. +if [ ! -x "$ASAN_BIN" ]; then + skip "no sanitizer binary at $ASAN_BIN (configure the llvm@22 tree: cmake -S . -B asan -DRIPWIRE_ASAN=ON, CONTRIBUTING.md; or set RIPWIRE_ASAN_BIN)" + echo "SKIP — nothing asserted (no sanitizer build)" + exit 0 +fi +# present but not a sanitizer build, or one that cannot start: a different premise, and a failure +if ! is_asan "$ASAN_BIN"; then + echo "$ASAN_BIN is not a sanitizer build (no __asan_init): this gate would test a plain binary and say it was ASan"; exit 2 +fi +ASAN_VER="$( gate_bounded 90 "$ASAN_BIN" --version 2>&1 )" || { echo "$ASAN_BIN does not start within 90 s (AppleClang's ASan hangs before main on macOS 26: build the asan tree with llvm@22, CONTRIBUTING.md): $ASAN_VER"; exit 2; } +fromOf(){ printf '%s\n' "$1" | sed -n 's/.*built_from=\([0-9a-f]*\).*/\1/p' | head -1; } +BIN_FROM="$( fromOf "$BIN_VER" )"; ASAN_FROM="$( fromOf "$ASAN_VER" )" +if [ -z "$BIN_FROM" ] || [ -z "$ASAN_FROM" ] || [ "$BIN_FROM" != "$ASAN_FROM" ]; then + echo "the CLI reference binary (built_from='$BIN_FROM') and the sanitizer binary (built_from='$ASAN_FROM') are not one source tree: rebuild asan/ so the answers compared are from the same code"; exit 2 +fi + +TMP="$( mktemp -d )" +TMP="$( cd "$TMP" && pwd -P )" # realpath: both surfaces print the root they were given, and /var is a symlink on macOS +cleanup(){ gate_bounded_reap; chmod -R u+w "$TMP" 2>/dev/null; rm -rf "$TMP"; } +trap cleanup EXIT; gate_bounded_arm +mkdir -p "$TMP/san" "$TMP/srvtmp" "$TMP/clitmp" + +cat >"$TMP/driver.py" <<'PYEOF' +#!/usr/bin/env python3 +"""The session driver. argv: PLAIN_BIN ASAN_BIN WORK CYCLES. Prints PASS/FAIL/SKIP/NOTE rows and one SUMMARY line.""" +import hashlib, json, os, re, select, shutil, signal, stat, subprocess, sys, time +import xml.etree.ElementTree as ET +from concurrent.futures import ThreadPoolExecutor + +BIN, ASAN, WORK, CYCLES = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]) +A, B, R = (os.path.join(WORK, d) for d in ("a", "b", "r")) +SAN = os.path.join(WORK, "san") +SRVTMP, CLITMP = os.path.join(WORK, "srvtmp"), os.path.join(WORK, "clitmp") +DARWIN = sys.platform == "darwin" +SANENV = { + "ASAN_OPTIONS": "detect_leaks=0:halt_on_error=1:abort_on_error=1:quarantine_size_mb=0:allocator_release_to_os_interval_ms=0:log_path=%s/asan" % (SAN,), + "UBSAN_OPTIONS": "halt_on_error=1:print_stacktrace=1:log_path=%s/ubsan" % SAN, +} +TOPK = "6" +fails, notes = [], [] +checks = 0 +stats = {"stamp": 0, "content": 0, "edit": 0, "switch": 0, "release": 0, "cold": 0, "changed": 0} +server = None + + +def row(kind, text): + print(" %-4s %s" % (kind, text), flush=True) + + +def fail(text): + fails.append(text) + row("FAIL", text[:700]) + + +def check(cond, label, detail=""): + global checks + checks += 1 + if not cond: + fail("%s%s" % (label, (": " + detail) if detail else "")) + return cond + + +# ───────────────────────────────────────────────── the one session ────────────────────────────────────────────────── +class Dead(Exception): + pass + + +class Server: + def __init__(self, binp, args, tmpdir, errpath, extra_env): + env = dict(os.environ, RIPWIRE_MCP_TIMINGS="1", TMPDIR=tmpdir) + env.update(extra_env) + self.err = open(errpath, "w+") + self.errpath = errpath + self.p = subprocess.Popen([binp, "--mcp"] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.err, + text=True, env=env) + self.id = 0 + self.timing_seen = 0 + + def timings(self): + self.err.flush() + with open(self.errpath, errors="replace") as f: + return [l for l in f.read().splitlines() if l.startswith("ripwire-timing ")] + + def rpc(self, method, params=None, wait=240): + t0 = time.time() + try: + return self.rpc_(method, params, wait) + finally: + T["mcp"] += time.time() - t0 + T["nmcp"] += 1 + + def rpc_(self, method, params=None, wait=240): + self.id += 1 + msg = {"jsonrpc": "2.0", "id": self.id, "method": method} + if params is not None: + msg["params"] = params + try: + self.p.stdin.write(json.dumps(msg) + "\n") + self.p.stdin.flush() + except (BrokenPipeError, ValueError): + raise Dead("the server closed its input (died)") + waited = 0 + while True: + r, _, _ = select.select([self.p.stdout], [], [], 1.0) + if r: + break + waited += 1 + if self.p.poll() is not None: + raise Dead("the server exited (rc=%s) before answering %s" % (self.p.returncode, method)) + if waited >= 8 and os.listdir(SAN): + # a sanitizer report is on disk and the process has not answered: it is aborting (the abort and the symbolizer + # can take minutes on a loaded macOS host); the report is the finding, so stop waiting + raise Dead("a sanitizer report was written and the server stopped answering %s" % method) + if waited >= wait: + raise Dead("no answer within %d s to %s" % (wait, method)) + line = self.p.stdout.readline() + if not line: + raise Dead("the server exited mid-request (rc=%s)" % self.p.poll()) + resp = json.loads(line) + # one timing line per request, printed after the response: wait for it so rebuilt= is read for THIS request + t0 = time.time() + while True: + tl = self.timings() + if len(tl) > self.timing_seen or time.time() - t0 > 20: + break + time.sleep(0.005) + t = {} + if len(tl) > self.timing_seen: + t = dict(kv.split("=", 1) for kv in tl[self.timing_seen].split()[1:] if "=" in kv) + self.timing_seen = len(tl) + return resp, t + + def tool(self, name, **args): + """-> (kind, payload, timing): kind 'ok' (payload = text) or 'err' (payload = (code, message)).""" + resp, t = self.rpc("tools/call", {"name": name, "arguments": args}) + if "error" in resp: + return "err", (resp["error"].get("code"), resp["error"].get("message", "")), t + return "ok", resp["result"]["content"][0]["text"], t + + def close(self): + try: + self.p.stdin.close() + except Exception: + pass + try: + return self.p.wait(timeout=180) + except subprocess.TimeoutExpired: + self.p.kill() + self.p.wait() + return "timeout" + + def kill(self): + try: + self.p.kill() + self.p.wait(timeout=10) + except Exception: + pass + + +def on_signal(signum, _frame): + if server is not None: + server.kill() + row("FAIL", "session stopped by signal %d (the wall-clock cap, or the gate was stopped)" % signum) + row("NOTE", "time: CLI %.0fs in %d runs, MCP %.0fs in %d requests" % (T["cli"], T["ncli"], T["mcp"], T["nmcp"])) + print("SUMMARY checks=%d fails=%d cycles=-1 aborted=1" % (checks, len(fails) + 1), flush=True) + sys.exit(124) + + +for s in (signal.SIGALRM, signal.SIGTERM, signal.SIGHUP, signal.SIGINT): + signal.signal(s, on_signal) + + +# ───────────────────────────────────────────────── fixtures ───────────────────────────────────────────────────────── +def put(path, text): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write(text) + + +def base_tree(a, v): + """The static + variant part of tree A. v (0/1) retargets the value references the main files pass.""" + cbx = "cb2" if v else "cb" + ev = "on_event2" if v else "on_event" + put(a + "/js/cb.js", "export function cb(x) { return x + 1; }\nexport function cb2(x) { return x + 2; }\n") + put(a + "/js/run.js", "export function run(f, x) { return f(x); }\n") + put(a + "/js/main.js", "import { run } from './run.js';\nimport { cb, cb2 } from './cb.js';\n" + "export function go() { return run(%s, 1); }\nexport const table = { onEvent: cb2 };\n" + "function jsDead() { return 0; }\nfunction jsVonly() { return 1; }\nconst jsSlot = { h: jsVonly };\n" % cbx) + put(a + "/ts/handlers.ts", "export function onTs(x: number): number { return x * 2; }\n" + "export function onTs2(x: number): number { return x * 3; }\n") + put(a + "/ts/app.ts", "import { onTs, onTs2 } from './handlers';\n" + "export function registerTs(f: (n: number) => number): number { return f(1); }\n" + "export function bootTs(): number { return registerTs(%s); }\n" + "export const tsTable: { [k: string]: (n: number) => number } = { k: onTs2 };\n" % ("onTs2" if v else "onTs")) + put(a + "/go/go.mod", "module example.com/gate\n\ngo 1.21\n") + put(a + "/go/handlers/handlers.go", "package handlers\n\nfunc OnEvent(x int) int { return x + 1 }\nfunc OnEvent2(x int) int { return x + 2 }\n") + put(a + "/go/main.go", "package main\n\nimport \"example.com/gate/handlers\"\n\nfunc register(f func(int) int) int { return f(1) }\n\n" + "func localHandler(x int) int { return x * 4 }\n\nfunc Boot() int { return register(handlers.OnEvent) + register(%s) }\n\n" + "var table = map[string]func(int) int{\"k\": handlers.OnEvent2, \"l\": localHandler}\n\nfunc main() { _ = Boot() }\n\n" + "func deadGo() int { return 0 }\n\nfunc vonlyGo() int { return 1 }\n\nvar slots = []func() int{vonlyGo}\n" + % ("localHandler" if not v else "func(x int) int { return x }")) + put(a + "/py/handlers.py", "def on_event(x):\n return x + 1\n\n\ndef on_event2(x):\n return x + 2\n") + put(a + "/py/app.py", "from handlers import on_event, on_event2\n\n\ndef register(f):\n return f(1)\n\n\ndef boot():\n return register(%s)\n\n\n" + "TABLE = {\"k\": on_event2}\n\n\ndef _py_dead():\n return 0\n\n\ndef _py_vonly():\n return 1\n\n\nSLOTS = [_py_vonly]\n" % ev) + put(a + "/c/handlers.h", "int c_on_event(int x);\nint c_on_event2(int x);\n") + put(a + "/c/handlers.c", "#include \"handlers.h\"\nint c_on_event(int x) { return x + 1; }\nint c_on_event2(int x) { return x + 2; }\n") + put(a + "/c/app.c", "#include \"handlers.h\"\ntypedef int (*fn_t)(int);\nstatic fn_t fp;\nstruct ops { fn_t op; };\n" + "static const struct ops tbl = { c_on_event2 };\nvoid c_setup(void) { fp = %s; }\nint c_boot(void) { return fp(1) + tbl.op(2); }\n" + "static int c_dead(void) { return 0; }\nstatic int c_vonly(void) { return 1; }\nstatic fn_t slot2 = c_vonly;\n" + % ("c_on_event2" if v else "c_on_event")) + + +def gen_tree(a, k, v): + """The gen files of cycle k: gen added, the previous one gone, the value reference retargeted by v.""" + put(a + "/js/gen.js", "import { cb, cb2 } from './cb.js';\nexport function jsgen%d() { return %d; }\n" + "export const JSGEN = { h: jsgen%d, c: %s };\n" % (k, k, k, "cb2" if v else "cb")) + put(a + "/ts/gen.ts", "import { onTs, onTs2 } from './handlers';\nexport function tsgen%d(): number { return %d; }\n" + "export const TSGEN = [tsgen%d, %s];\n" % (k, k, k, "onTs2" if v else "onTs")) + put(a + "/go/gen.go", "package main\n\nfunc gogen%d() int { return %d }\n\nvar genSlots = []func() int{gogen%d}\n" % (k, k, k)) + put(a + "/py/gen.py", "from handlers import on_event, on_event2\n\n\ndef pygen_%d():\n return %d\n\n\nPYGEN = [pygen_%d, %s]\n" + % (k, k, k, "on_event2" if v else "on_event")) + put(a + "/c/gen.c", "#include \"handlers.h\"\nstatic int cgen_%d(void) { return %d; }\nstatic int cdead_%d(void) { return 0; }\n" + "static int (*cgen_slot)(void) = cgen_%d;\nint cgen_use(void) { return cgen_slot() + %s(1); }\n" + % (k, k, k, k, "c_on_event2" if v else "c_on_event")) + + +def bulk_tree(r): + """Root R: the JS value-reference trio plus 2400 small C files — enough that the resident index alone carries the + server over the 64M floor, so the NEXT call releases it.""" + put(r + "/js/cb.js", "export function cb(x) { return x + 1; }\n") + put(r + "/js/run.js", "export function run(f, x) { return f(x); }\n") + put(r + "/js/main.js", "import { run } from './run.js';\nimport { cb } from './cb.js';\nexport function go() { return run(cb, 1); }\n") + for d in range(12): + for f in range(200): + put("%s/bulk/d%d/f%d.c" % (r, d, f), "".join( + "int b%d_%d_%d(int x) { return x > 0 ? b%d_%d_%d(x - 1) + %d : %d; }\n" % (d, f, k, d, f, (k + 1) % 30, k, k) + for k in range(30))) + + +def tree_b(b, k): + """Root B: a git repo (so the rank is biased to the uncommitted diff); bgen.js is rewritten every cycle.""" + put(b + "/js/bcb.js", "export function bcb(x) { return x + 1; }\n") + put(b + "/js/brun.js", "export function brun(f, x) { return f(x); }\n") + put(b + "/js/bmain.js", "import { brun } from './brun.js';\nimport { bcb } from './bcb.js';\nexport function bgo() { return brun(bcb, 1); }\n") + put(b + "/py/bh.py", "def bh_event(x):\n return x\n\n\ndef bh_reg(f):\n return f(1)\n\n\ndef bh_boot():\n return bh_reg(bh_event)\n") + bump_b(b, k) + + +def bump_b(b, k): + put(b + "/js/bgen.js", "import { bcb } from './bcb.js';\nexport function bgen%d() { return %d; }\nexport const BGEN = { h: bgen%d, c: bcb };\n" % (k, k, k)) + + +def fingerprint(root): + h = hashlib.sha1() + for dp, dn, fn in os.walk(root): + dn[:] = sorted(d for d in dn if d != ".git") + for f in sorted(fn): + p = os.path.join(dp, f) + h.update(os.path.relpath(p, root).encode() + b"\0") + with open(p, "rb") as fh: + h.update(fh.read()) + return h.hexdigest() + + +# ───────────────────────────────────────── the two surfaces, normalised ───────────────────────────────────────────── +COMMENT = re.compile(r"", re.S) +HINT = re.compile(r' hint="[^"]*"') +VRKEYS = ("in_id", "to", "def", "bind", "into", "called_by", "through", "sites") + + +def xml_clean(t): + return HINT.sub("", COMMENT.sub("", t)).strip() + + +def boolstr(v): + return "1" if v is True else "0" if v is False else str(v) + + +def vr_window(total, shown, capped, rows): + return (str(total), str(shown), boolstr(capped), tuple(sorted(tuple(str(r.get(k, "")) for k in VRKEYS) for r in rows))) + + +NOWIN = ("0", "0", "0", ()) + + +T = {"cli": 0.0, "mcp": 0.0, "ncli": 0, "nmcp": 0} + + +def cli(binp, root, *args, tmp=CLITMP): + t0 = time.time() + try: + return cli_(binp, root, *args, tmp=tmp) + finally: + T["cli"] += time.time() - t0 # summed over the worker threads + T["ncli"] += 1 + + +def cli_(binp, root, *args, tmp=CLITMP): + env = dict(os.environ, TMPDIR=tmp) + env.update(SANENV) + p = subprocess.run([binp, root, "--no-cache", "--legend=compact"] + list(args), capture_output=True, text=True, timeout=300, env=env) + return p.returncode, p.stdout, p.stderr + + +def cli_view(root, verb, sym): + """CLI answer of --callers/--callees as (kind, data); NOTFOUND when the CLI says found=0 (rc 1).""" + rc, out, err = cli(BIN, root, "--%s=%s" % (verb, sym)) + c = xml_clean(out) + if rc == 1 and 'found="0"' in c: + return ("NOTFOUND",) + if rc != 0 or not c.startswith("<"): + return ("CLIERR", rc, err[:200], c[:200]) + el = ET.fromstring(c) + syms = tuple(sorted((s.get("n"), s.get("p")) for s in el.findall("s"))) + w = el.find("vrs") + win = vr_window(w.get("total"), w.get("shown"), w.get("capped"), [v.attrib for v in w.findall("vr")]) if w is not None else NOWIN + return ("OK", el.get("defs"), syms, win) + + +def mcp_fs_view(kind, payload): + if kind == "err": + return ("NOTFOUND",) if payload[0] == -32602 and "not found" in payload[1] else ("MCPERR", payload) + j = json.loads(payload) + + def win(k): + w = j.get(k) + return vr_window(w["total"], w["shown"], w["capped"], w["rows"]) if w else NOWIN + + def syms(k): + return tuple(sorted((s["name"], "%s:%s" % (s["file"], s["line"])) for s in j.get(k, []))) + return ("OK", str(j.get("defs")), syms("calledBy"), win("valueRefs"), syms("calls"), win("valueCallees")) + + +def cli_refs(root, syms, paths, greps, rankbys, with_map=True): + """The one-shot CLI answers for one tree state, keyed like the MCP side (the runs are independent: six at a time).""" + def fs_task(s): + cal = cli_view(root, "callers", s) + # a leaf (a handler or a generated function) calls nothing: the callees run is skipped and the MCP side must say the same + cee = cli_view(root, "callees", s) if s in CALLERS_OF else ("OK", None, (), NOWIN) + if cal[0] == "NOTFOUND" or cee[0] == "NOTFOUND": + return [(("fs", s), ("NOTFOUND",)), (("frs", s), ("NOTFOUND",))] + return [(("fs", s), ("OK", cal[1], cal[2], cal[3], cee[2], cee[3])), (("frs", s), ("OK", cal[1], cal[2], cal[3]))] + + def xml_task(key, *args): + rc, out, err = cli(BIN, root, *args) + return [(key, ("NOTFOUND",) if (rc == 1 and 'found="0"' in out) else ("OK", xml_clean(out)) if rc == 0 else ("CLIERR", rc, err[:200]))] + + def grep_task(g): + rc, out, err = cli(BIN, root, "--grep=%s" % g) + hits = set() + if rc == 0: + for f in ET.fromstring(xml_clean(out)).findall("f"): + for h in f.findall("hit"): + hits.add((f.get("p"), int(h.get("l")))) + return [(("grep", g), ("OK", tuple(sorted(hits))) if rc == 0 else ("CLIERR", rc, err[:200]))] + + def dead_task(): + rc, out, err = cli(BIN, root, "--dead-code") + dead = set() + if rc == 0: + for d in ET.fromstring(xml_clean(out)).findall("d"): + dead.add(d.get("n")) + return [(("dead",), ("OK", tuple(sorted(dead))) if rc == 0 else ("CLIERR", rc, err[:200]))] + + tasks = [lambda s=s: fs_task(s) for s in syms] + for s in syms[:5]: + for verb in ("impact", "uses"): + tasks.append(lambda s=s, verb=verb: xml_task((verb, s), "--%s=%s" % (verb, s))) + for f, t in paths: + tasks.append(lambda f=f, t=t: xml_task(("path", f, t), "--path=%s,%s" % (f, t))) + tasks += [lambda g=g: grep_task(g) for g in greps] + if with_map: + tasks.append(lambda: xml_task(("map",), "--order=stable", "--top-k=" + TOPK)) + for rb in rankbys: + tasks.append(lambda rb=rb: xml_task(("rank_by", rb), "--rank-by=" + rb, "--order=stable", "--top-k=" + TOPK)) + tasks.append(dead_task) + ref = {} + with ThreadPoolExecutor(max_workers=6) as ex: + for rows in ex.map(lambda t: t(), tasks): + ref.update(rows) + return ref + + +def mcp_xml(srv, name, notfound_ok=True, **args): + kind, payload, t = srv.tool(name, **args) + if kind == "err": + return (("NOTFOUND",) if (payload[0] == -32602 and "not found" in payload[1]) else ("MCPERR", payload)), t + return ("OK", xml_clean(payload)), t + + +def mcp_views(srv, root, syms, paths, greps, rankbys, with_map=True): + """Every view, asked of the long-lived session. -> (answers, timings of the first request).""" + got, first = {}, None + for s in syms: + kind, payload, t = srv.tool("find_symbol", path=root, symbol=s) + first = first or t + got[("fs", s)] = mcp_fs_view(kind, payload) + kind, payload, t = srv.tool("find_referencing_symbols", path=root, symbol=s) + v = mcp_fs_view(kind, payload) + got[("frs", s)] = v if v[0] != "OK" else v[:4] + for s in syms[:5]: + for verb in ("impact", "uses"): + got[(verb, s)], _ = mcp_xml(srv, verb, path=root, symbol=s) + for f, tt in paths: + got[("path", f, tt)], _ = mcp_xml(srv, "path_between", path=root, **{"from": f, "to": tt}) + for g in greps: + kind, payload, _ = srv.tool("grep", path=root, pattern=g) + if kind == "err": + got[("grep", g)] = ("MCPERR", payload) + else: + j = json.loads(payload) + got[("grep", g)] = ("OK", tuple(sorted(set((h["file"], int(h["line"])) for h in j["hits"])))) + if with_map: + got[("map",)], _ = mcp_xml(srv, "analyze", path=root) + for rb in rankbys: + got[("rank_by", rb)], _ = mcp_xml(srv, "rank_by", path=root, rank_by=rb) + return got, first + + +def compare(label, ref, got, skip=()): + """Every key the reference has must be answered identically; returns (compared, bad keys).""" + bad = [] + for key, rv in ref.items(): + if key[0] in skip or key[0] == "dead": + continue + gv = got.get(key) + if gv != rv: + bad.append((key, rv, gv)) + return bad + + +def short(v, n=330): + s = repr(v) + return s if len(s) <= n else s[:n] + "...(%d)" % len(s) + + +def check_dead(label, ref, got): + """dead-code (CLI, fresh) against the session's valueRefs: a static function with no caller is dead only when no + value-site holds it. The CLI lists the dead set; the session says per function whether anything holds it.""" + dead = set(ref[("dead",)][1]) if ref[("dead",)][0] == "OK" else None + if dead is None: + check(False, "%s dead-code ran on the CLI" % label, short(ref[("dead",)])) + return + for s, expectDead in (("c_dead", True), ("c_vonly", False)): + g = got.get(("fs", s)) + if not g or g[0] != "OK": + check(False, "%s MCP find_symbol(%s) answered" % (label, s), short(g)) + continue + noCallers = g[2] == () + holdsValue = g[3][0] != "0" + check(noCallers and ((s in dead) == expectDead) and (holdsValue == (not expectDead)), + "%s dead-code agrees with the session's valueRefs for %s" % (label, s), + "dead=%s callers=%s valueRefs=%s" % (s in dead, g[2], g[3][0])) + + +# ───────────────────────────────────────────────── the session ────────────────────────────────────────────────────── +CALLERS_OF = {"run", "register", "c_boot", "go", "Boot", "cgen_use", "brun", "bgo", "bh_reg", "bh_boot"} # the symbols that call something; every other one is a leaf + + +def symbols_at(k): + cur = ["jsgen%d" % k, "pygen_%d" % k, "cgen_%d" % k, "cdead_%d" % k] + old = ["jsgen%d" % (k - 1)] if k > 1 else [] + return ["cb", "cb2", "run", "on_event", "c_on_event", "onTs", "localHandler", "c_vonly", "c_dead", "jsVonly", + "register", "c_boot", "go", "Boot", "cgen_use"] + cur + old + + +PATHS = [("Boot", "register"), ("go", "cb"), ("c_boot", "c_on_event")] +GREPS = ["cb2"] +RANKBYS = ["pagerank", "rrf"] + + +def cold_map(root): + """A fresh server (fresh TMPDIR, plain binary) answering `analyze` on `root`: the instrument for a rank this CLI has no twin of.""" + tmp = os.path.join(WORK, "coldtmp") + shutil.rmtree(tmp, ignore_errors=True) + os.makedirs(tmp) + s = Server(BIN, ["--top-k=" + TOPK], tmp, os.path.join(WORK, "cold.err"), {}) + try: + s.rpc("initialize") + kind, payload, _ = s.tool("analyze", path=root) + finally: + s.close() + stats["cold"] += 1 + return ("OK", xml_clean(payload)) if kind == "ok" else ("MCPERR", payload) + + +def release_attempt(k, refR, refA, kk): + """One fresh process under the 64M floor. -> 'ok' | 'partial' | 'refused' | 'noreleased'; the last three are premises this + host did not meet (named by the caller), never a pass. --max-memory has a 64M floor and is server-wide, and a sanitizer + process that has run the whole cycle never falls back under it, so the release path gets a fresh process. Root R's + resident index alone carries it over the line, so the SECOND call on R releases that index (releaseMcpIndexMemory) and + rebuilds the unchanged tree; the next call, on A, releases R's rebuild again and rebuilds A. Only those calls are made.""" + s = Server(ASAN, ["--top-k=" + TOPK, "--max-memory=64M"], SRVTMP + "_rl", os.path.join(WORK, "release.err"), SANENV) + status = "ok" + try: + s.rpc("initialize") + kind, payload, t1 = s.tool("find_symbol", path=R, symbol="cb") + v1 = mcp_fs_view(kind, payload) + if v1[0] != "OK": + return "partial" # the memory guard's soft stop (a parse past five seconds under a 64M limit) left R without its symbols + check(t1.get("rebuilt") == "1", "cycle %d RL: root R built (rebuilt=1)" % k, short(t1)) + check(v1 == refR, "cycle %d RL: root R's answer equals the one-shot CLI" % k, "cli=%s mcp=%s" % (short(refR), short(v1))) + kind, payload, t2 = s.tool("find_symbol", path=R, symbol="cb") + if kind == "err" and "memory limit" in payload[1]: + return "refused" + v2 = mcp_fs_view(kind, payload) + if v2[0] != "OK": + return "partial" + if t2.get("rebuilt") != "1": + return "noreleased" + stats["release"] += 1 + check(v2 == refR, "cycle %d RL: the unchanged tree R, released and rebuilt, answers equal the one-shot CLI" % k, "cli=%s mcp=%s" % (short(refR), short(v2))) + gotA, fA = mcp_views(s, A, symbols_at(kk), PATHS, GREPS, RANKBYS) + if gotA[("fs", "cb")][0] == "MCPERR" and "memory limit" in str(gotA[("fs", "cb")][1]): + row("NOTE", "cycle %d RL: the release was proved; the call on A after it was refused by name (the footprint stayed over the floor)" % k) + return "ok" + bad = compare("RL A", refA, gotA) + check(not bad, "cycle %d RL: after the release, tree A answers equal the one-shot CLI" % k, + "; ".join("%s: cli=%s mcp=%s" % (a_, short(b_), short(c_)) for a_, b_, c_ in bad[:2])) + check(fA and fA.get("rebuilt") == "1", "cycle %d RL: A was rebuilt after R's release" % k, short(fA)) + finally: + rc = s.close() + check(rc == 0, "cycle %d RL: the release session ended with exit status 0" % k, "rc=%s" % rc) + with open(os.path.join(WORK, "release.err"), errors="replace") as fh: + e = fh.read() + check("Sanitizer" not in e and "runtime error:" not in e, "cycle %d RL: no sanitizer text on the release session's stderr" % k, e[:600]) + return status + + +def release_session(k, refR, refA, kk): + os.makedirs(SRVTMP + "_rl", exist_ok=True) + if not stats.get("rl_warm"): + # the memory guard stops a parse that runs past five seconds, and a cold sanitizer parse of R can: warm the ingest + # cache once, unlimited, so every limited session below reads facts from it and its ingest is short + w = Server(ASAN, ["--top-k=" + TOPK], SRVTMP + "_rl", os.path.join(WORK, "warm.err"), SANENV) + try: + w.rpc("initialize") + kind, payload, _ = w.tool("find_symbol", path=R, symbol="cb") + check(kind == "ok", "release sessions: the unlimited warm-up build of R answered", short(payload)) + finally: + check(w.close() == 0, "release sessions: the warm-up session ended with exit status 0") + stats["rl_warm"] = 1 + for attempt in range(1, 5): + st = release_attempt(k, refR, refA, kk) + if st == "ok": + return + stats["rl_miss"] = stats.get("rl_miss", 0) + 1 + row("NOTE", "cycle %d RL attempt %d: premise not met (%s)" % (k, attempt, { + "partial": "the memory guard's 5 s soft-stop clock cut R's parse on this host", + "refused": "the footprint stayed over the 64M floor after the release, so the rebuild was refused by name", + "noreleased": "the footprint stayed under the floor, so no release fired"}[st])) + + +def sanitizer_findings(): + found = [] + for f in sorted(os.listdir(SAN)): + with open(os.path.join(SAN, f), errors="replace") as fh: + found.append((f, fh.read())) + err = "" + if server is not None: + server.err.flush() + with open(server.errpath, errors="replace") as fh: + err = fh.read() + for pat in ("ERROR: AddressSanitizer", "ERROR: LeakSanitizer", "runtime error:", "SUMMARY: AddressSanitizer", "SUMMARY: UndefinedBehaviorSanitizer"): + if pat in err: + found.append(("server stderr", err[err.index(pat):err.index(pat) + 3000])) + break + return found + + +def main(): + global server + shutil.rmtree(WORK + "/a", ignore_errors=True) + for r in (A, B, R): + os.makedirs(r, exist_ok=True) + base_tree(A, 0) + gen_tree(A, 0, 0) + tree_b(B, 0) + bulk_tree(R) + gitenv = dict(os.environ) + for cmd in (["init", "-q"], ["add", "-A"], ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "init"]): + subprocess.run(["git", "-C", B] + cmd, check=True, capture_output=True, env=gitenv) + row("NOTE", "fixtures: A (js/ts/go/py/c, %d files), B (git repo, dirty), R (%d files); CLI reference binary %s" % ( + sum(len(f) for _, _, f in os.walk(A)), sum(len(f) for _, _, f in os.walk(R)), BIN)) + + server = Server(ASAN, ["--top-k=" + TOPK], SRVTMP, os.path.join(WORK, "server.err"), SANENV) + refs = {} # fingerprint of tree A -> CLI answers (the CLI is a function of tree state) + refR = None + prev_ref = None + live_handles = {} # symbol -> handle minted at some earlier state + done = 0 + try: + server.rpc("initialize") + v = 0 + state = {"k": 0, "v": 0} + + def ref_for(k, root=A, syms=None): + fp = fingerprint(root) + if fp not in refs: + refs[fp] = cli_refs(root, syms or symbols_at(k), PATHS, GREPS, RANKBYS) + return refs[fp] + + def qpoint(label, k, expect_rebuild, kind): + """query every view on A, compare with the CLI on this state; assert the first request rebuilt (or not).""" + nonlocal prev_ref + ref = ref_for(k) + got, first = mcp_views(server, A, symbols_at(k), PATHS, GREPS, RANKBYS) + bad = compare(label, ref, got) + check(not bad, "cycle %d %s: all %d answers equal the one-shot CLI" % (k, label, len(ref) - 1), + "; ".join("%s: cli=%s mcp=%s" % (key, short(rv), short(gv)) for key, rv, gv in bad[:3]) + (" (+%d more)" % (len(bad) - 3) if len(bad) > 3 else "")) + check_dead("cycle %d %s" % (k, label), ref, got) + if expect_rebuild is not None: + check(first and first.get("rebuilt") == ("1" if expect_rebuild else "0"), + "cycle %d %s: the first request %s the index (rebuilt=%s)" % (k, label, "rebuilt" if expect_rebuild else "reused", first and first.get("rebuilt")), + "timing %s" % first) + if expect_rebuild and first and first.get("rebuilt") == "1": + stats[kind] += 1 + return ref, got + + # cycle 0: the cold build + ref0, got0 = qpoint("cold", 0, True, "stamp") + stats["stamp"] -= 1 + prev_ref = ref0 + stableK = 0 + + for k in range(1, CYCLES + 1): + kk = k + # ── M1: a stamp-keeping mutation ────────────────────────────────────────────────────────────────────── + which = k % 3 + if which == 1: + f = A + "/js/main.js" + m = os.stat(f).st_mode + os.chmod(f, m ^ stat.S_IXUSR) + os.chmod(f, m) + what = "chmod (ctime only)" + elif which == 2: + put(A + "/notes_%d.txt" % k, "x\n") + what = "a new non-source file" + else: + os.makedirs(A + "/empty_%d" % k, exist_ok=True) + what = "an empty directory" + # the state before this cycle's content change (the previous cycle's, plus the stamp-keeping change) + ref, got = qpoint("M1 " + what, kk - 1, True, "stamp") + changed_by_m1 = ref != prev_ref + prev_ref = ref + + # ── M2: a content mutation ──────────────────────────────────────────────────────────────────────────── + v = k % 2 + old_handles = dict(live_handles) + base_tree(A, v) + gen_tree(A, k, v) + via_edit = (k % 2 == 1) + if via_edit: + kind, payload, t = server.tool("insert_after_symbol", path=A, symbol="jsgen%d" % k, file="js/gen.js", + text="export function jsins%d() { return %d; }" % (k, k)) + check(kind == "ok" and t.get("rebuilt") == "1", "cycle %d M2: insert_after_symbol applied and the index was rebuilt around it" % k, short((kind, payload, t))) + kind, payload, t = server.tool("replace_symbol_body", path=A, symbol="jsVonly", file="js/main.js", + new_body="function jsVonly() { return %d; }" % (100 + k)) + check(kind == "ok", "cycle %d M2: replace_symbol_body applied" % k, short((kind, payload))) + stats["edit"] += 1 + # an edit verb rebuilds the index itself, after its write: the first QUERY then reuses it (rebuilt=0) + ref2, got2 = qpoint("M2 content (%s)" % ("MCP edit verbs" if via_edit else "file rewrite"), kk, None if via_edit else True, "content") + if via_edit: + stats["content"] += 1 + check(ref2 != prev_ref, "cycle %d M2: the mutation is visible in at least one compared answer (a stale index would be caught)" % k) + stats["changed"] += 1 if ref2 != prev_ref else 0 + prev_ref = ref2 + + # handles: minted before the mutation must refuse, minted now must serve the bytes on disk + for sym in ("jsgen%d" % (k - 1), "jsVonly"): + h = old_handles.get(sym) + if h: + kind, payload, _ = server.tool("fetch_body", path=A, handle=h) + check(kind == "err" and ("stale handle" in payload[1] or "does not resolve" in payload[1]), "cycle %d: a handle minted before the rewrite is refused as stale (%s)" % (k, sym), short((kind, payload))) + for sym, rel in (("jsgen%d" % k, "js/gen.js"), ("cgen_%d" % k, "c/gen.c"), ("pygen_%d" % k, "py/gen.py")): + kind, payload, _ = server.tool("find_symbol", path=A, symbol=sym) + if kind == "ok": + h = json.loads(payload)["symbol"]["handle"] + live_handles[sym] = h + kind, payload, _ = server.tool("fetch_body", path=A, handle=h) + body = json.loads(payload)["body"] if kind == "ok" else None + text = open(os.path.join(A, rel)).read() + check(body is not None and sym in body and body in text, "cycle %d: fetch_body(%s) with a fresh handle is the bytes on disk" % (k, sym), short((kind, payload))) + else: + check(False, "cycle %d: find_symbol(%s) answered" % (k, sym), short(payload)) + for sym in ("jsVonly",): + kind, payload, _ = server.tool("find_symbol", path=A, symbol=sym) + if kind == "ok": + live_handles[sym] = json.loads(payload)["symbol"]["handle"] + + # ── RS: root switches ───────────────────────────────────────────────────────────────────────────────── + bump_b(B, k) + bsyms = ["bcb", "bgen%d" % k, "bh_event", "bh_reg"] + bref = cli_refs(B, bsyms, [("bh_boot", "bh_reg")], ["bcb"], [], with_map=False) + bgot, first = mcp_views(server, B, bsyms, [("bh_boot", "bh_reg")], ["bcb"], [], with_map=False) + check(first and first.get("rebuilt") == "1", "cycle %d RS: switching to root B rebuilt the index" % k, short(first)) + stats["switch"] += 1 if first and first.get("rebuilt") == "1" else 0 + bad = compare("B", bref, bgot) + check(not bad, "cycle %d RS: root B (dirty git repo) answers equal the one-shot CLI" % k, "; ".join("%s: cli=%s mcp=%s" % (a, short(b), short(c)) for a, b, c in bad[:2])) + kind, payload, _ = server.tool("analyze", path=B) + coldB = cold_map(B) + check(kind == "ok" and ("OK", xml_clean(payload)) == coldB, "cycle %d RS: analyze on the dirty root equals a cold server's (working-set rank)" % k, short(coldB)) + # the workspace key + kind, payload, t = server.tool("find_symbol", paths=[A, B], symbol="c_vonly") + wsOK = kind == "ok" + check(wsOK and t.get("rebuilt") == "1", "cycle %d RS: the workspace [A,B] rebuilt the index" % k, short((kind, payload if kind == "err" else "", t))) + if wsOK: + wj = json.loads(payload) + check(wj.get("valueRefs", {"total": 0})["total"] == int(ref2[("fs", "c_vonly")][3][0]), "cycle %d RS: the workspace answer for c_vonly carries A's value references" % k, + "ws=%s a=%s" % (wj.get("valueRefs", {}).get("total"), ref2[("fs", "c_vonly")][3][0])) + stats["switch"] += 1 if t.get("rebuilt") == "1" else 0 + # and back to A + refA, gotA = qpoint("RS back to A", kk, True, "switch") + + # ── RL: the memory-release path, in its own short session under the 64M floor ──────────────────────── + if refR is None: + cal, cee = cli_view(R, "callers", "cb"), cli_view(R, "callees", "cb") + refR = ("OK", cal[1], cal[2], cal[3], cee[2], cee[3]) + if k % 5 == 1: + release_session(k, refR, ref_for(kk), kk) + stats["rl_cycles"] = stats.get("rl_cycles", 0) + 1 + done = k + row("PASS" if not fails else "NOTE", "cycle %d/%d done (checks so far %d, failures %d)" % (k, CYCLES, checks, len(fails))) + except Dead as e: + fail("the server died: %s" % e) + time.sleep(3) # let a report finish landing in the log directory + server.kill() + except Exception as e: # a driver bug must be loud, never a pass + import traceback + fail("driver error: %r\n%s" % (e, traceback.format_exc()[-1500:])) + rc = server.close() if server.p.poll() is None else server.p.returncode + found = sanitizer_findings() + check(not found, "no sanitizer report in the log directory or on stderr (halt_on_error, abort_on_error, log_path)", + " | ".join("%s: %s" % (n, t[:1800]) for n, t in found[:1])) + check(rc == 0, "the session ended with exit status 0 after its input closed", "rc=%s" % rc) + row("NOTE", "time: CLI %.0fs in %d runs, MCP %.0fs in %d requests" % (T["cli"], T["ncli"], T["mcp"], T["nmcp"])) + print("SUMMARY checks=%d fails=%d cycles=%d stamp=%d content=%d edit=%d switch=%d release=%d cold=%d changed=%d missed=%d rc=%s san=%d" % ( + checks, len(fails), done, stats["stamp"], stats["content"], stats["edit"], stats["switch"], stats["release"], stats["cold"], + stats["changed"], stats.get("rl_miss", 0), rc, len(found)), flush=True) + sys.exit(1 if fails else 0) + + +main() +PYEOF + +echo "-- one session, $CYCLES cycles: stamp-keeping rebuild, content rebuild (shell + MCP edit verbs), root switch, workspace, memory release" +START="$( date +%s )" +# the driver's stdout goes to a file so its status is the gate's own: $( ) would swallow a stop +gate_bounded "$SESSION_CAP" python3 "$TMP/driver.py" "$BIN" "$ASAN_BIN" "$TMP" "$CYCLES" >"$TMP/driver.out" 2>"$TMP/driver.err" +drc=$? +ELAPSED=$(( $( date +%s ) - START )) +cat "$TMP/driver.out" +[ -s "$TMP/driver.err" ] && sed 's/^/ | /' "$TMP/driver.err" | head -20 + +# ── the verdict, read off values the run produced present, numeric, and the premises met ───────────── +SUMMARY="$( grep '^SUMMARY ' "$TMP/driver.out" | tail -1 )" +getn(){ printf '%s\n' "$SUMMARY" | tr ' ' '\n' | sed -n "s/^$1=//p" | head -1; } +if [ -z "$SUMMARY" ]; then + no "the driver printed no SUMMARY line (rc=$drc): the session did not run to its end" +else + for key in checks fails cycles stamp content edit switch release cold changed missed san; do + val="$( getn "$key" )" + case "$val" in ''|*[!0-9-]*) no "SUMMARY field $key='$val' is not numeric: $SUMMARY"; val=-9;; esac + eval "S_$key=\$val" + done + if [ "${S_cycles:--9}" = "$CYCLES" ]; then ok "all $CYCLES cycles ran (checks=$S_checks)"; else no "cycles=$S_cycles, expected $CYCLES"; fi + if [ "${S_san:--9}" = 0 ]; then ok "zero sanitizer reports (ASAN_OPTIONS halt_on_error=1 abort_on_error=1 log_path; log directory and stderr empty of reports)"; else no "sanitizer reports: $S_san"; fi + if [ "${S_stamp:--9}" -ge "$CYCLES" ]; then ok "stamp-keeping rebuilds proved by rebuilt=1: $S_stamp"; else no "only $S_stamp stamp-keeping rebuilds were proved (need >= $CYCLES)"; fi + if [ "${S_content:--9}" -ge "$CYCLES" ]; then ok "content rebuilds proved: $S_content (MCP edit-verb cycles: $S_edit)"; else no "only $S_content content rebuilds were proved"; fi + if [ "${S_switch:--9}" -ge $(( CYCLES * 3 )) ]; then ok "root-switch / workspace rebuilds proved: $S_switch"; else no "only $S_switch root-switch rebuilds were proved (need >= $(( CYCLES * 3 )))"; fi + if [ "${S_cold:--9}" -ge "$CYCLES" ]; then ok "cold-server rank comparisons: $S_cold"; else no "only $S_cold cold-server comparisons ran"; fi + if [ "${S_changed:--9}" -ge "$CYCLES" ]; then ok "every content mutation changed a compared answer ($S_changed/$CYCLES)"; else no "only $S_changed of $CYCLES mutations changed a compared answer"; fi + # the release arm needs the host to meet two premises (R's parse inside the memory guard's 5 s clock; the footprint back + # under the 64M floor after the release). Each missed attempt is counted and named above; with NONE proven the arm is a + # named SKIP, never a pass, and the other arms still stand. + if [ "${S_release:--9}" -ge 1 ]; then + ok "memory-release rebuilds proved (rebuilt=1 on an unchanged tree, in a session that released the index): $S_release (premise misses: $S_missed)" + elif [ "${S_release:--9}" = 0 ]; then + skip "memory-release arm: not one release session met its premises on this host in $S_missed attempts (a parse past the memory guard's 5 s clock, or a footprint that stayed over/under the 64M floor); the stamp-keeping, content and root-switch arms are unaffected" + else + no "memory-release counter missing" + fi + if [ "${S_fails:--9}" = 0 ]; then ok "no comparison failed"; else no "$S_fails comparison(s) failed"; fi +fi +if [ "$drc" = 0 ]; then ok "the session driver exited 0"; else no "the session driver exited $drc (124 = the ${SESSION_CAP} s cap)"; fi +# an orphan is a failure of this gate, whatever else passed (no orphans) +if pgrep -f "$TMP" >/dev/null 2>&1; then + no "a process started by this gate is still running: $( pgrep -fl "$TMP" | head -3 | tr '\n' ' ' )" + pkill -f "$TMP" 2>/dev/null +else + ok "no process of this gate outlives it" +fi +echo "mcpreloadasancheck: ${ELAPSED}s" +if [ "$fail" -eq 0 ]; then echo "mcpreloadasancheck: ALL PASS"; else echo "mcpreloadasancheck: FAIL"; fi +exit "$fail" diff --git a/test/pargates.py b/test/pargates.py index 7d4682fb1..ad67f5a50 100644 --- a/test/pargates.py +++ b/test/pargates.py @@ -252,6 +252,9 @@ def shard_plan_for(gate_names, n): "mcpframehonestycheck.sh": 900, # 2026-09-07 (first sharded CI run 34145918269): rc=124 at 300.1 s on three of # four Linux legs' shard 2 -- "exactly the cap" again. ~150 s local; a shard # job hands it fewer neighbours to hide behind than the whole suite did. + "mcpreloadasancheck.sh": 1800, # one long-lived sanitizer MCP session, 20 cycles, every answer checked against a one-shot CLI run + # (a few hundred of them); its own wall-clock cap is 1500 s (RIPWIRE_GATE_SESSION_CAP_SEC), this is + # the hang tripwire above it. Idle local wall time is in the lane report, not here. "knownitemcheck.sh": 900, # 2026-09-05: --eval-retrieval stopped sampling 150 symbols in PATH order and # now grades its whole population exhaustively (the sampler measured the corpus, # not the ranker -- docs/EVALS.md section 7). The gate runs it twice on src/ for diff --git a/test/regression.sh b/test/regression.sh index 6ebb7fd68..c0b0a9725 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 8bdce27eb8107ec9061f31788534a3f1c6ef2648 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:44:00 -0400 Subject: [PATCH 033/229] test: whereis listing arms (LEAN L1-L10) and listing=all twins in crossref/complete The arms that read the whole hit list (the doc-quoting ref row, shown + more == hits, every hit printed, the stale-HEAD-row invariant) keep their assertions on --whereis-listing=all, the pre-listing answer; each gains a twin holding the same facts on the default listing (def rows equal, refs counted, shown + more + refs count == hits). New LEAN arms: def rows byte-equal to listing=all's, refs count exact, next= reproduces the ref rows byte for byte across pages, zero refs / zero defs print the whole list, the tip/date hoist, the listed-row window, refusals, legends, and the MCP twin. Co-Authored-By: Claude Opus 5.5 --- test/completecheck.sh | 21 ++++++- test/crossrefcheck.sh | 137 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/test/completecheck.sh b/test/completecheck.sh index 873422baf..d73ac4cd5 100755 --- a/test/completecheck.sh +++ b/test/completecheck.sh @@ -439,12 +439,16 @@ wwhere zqOther | grep -q ']*p="src/other.c" l="1" kind="def"' # 18f) the INVARIANT: no complete="1" answer carries a HEAD row for a path that differs from HEAD CHANGED="$( git -C "$WT" diff --name-only HEAD; git -C "$WT" ls-files --others --exclude-standard )" bad=0 +# lean-answers lane: the invariant reads BOTH listings — the default (def rows) and the whole list (every row), so the +# default's shorter row set never makes it pass vacuously. for s in zqKeep zqFresh zqOldName zqNewName zqDoomed zqGone zqUntracked zqOther zqUser; do - out="$( wwhere "$s" )" + for lst in defs all; do + out="$( wwhere "$s" --whereis-listing=$lst )" printf '%s' "$out" | wroot | grep -q 'complete="1"' || continue for p in $CHANGED; do - printf '%s' "$out" | grep -q "]*p=\"$p\"" && { bad=1; no "whereis (18f): $s claims complete=\"1\" beside a stale HEAD row for changed $p"; } + printf '%s' "$out" | grep -q "]*p=\"$p\"" && { bad=1; no "whereis (18f): $s ($lst) claims complete=\"1\" beside a stale HEAD row for changed $p"; } done + done done [ $bad -eq 0 ] && ok 'whereis (18f): no complete="1" answer carries a HEAD row for a path the working tree changed' @@ -532,13 +536,24 @@ printf 'def helper( x ):\n return x\n' >"$TL/pkg/lib.py" printf 'from pkg.lib import helper as real\n\ndef helper( x ):\n return real( x )\n\ndef only_in_test():\n return helper( 1 )\n' >"$TL/tests/test_lib.py" ( cd "$TL" && git init -q -b main . && git add -A \ && git -c user.name=fx -c user.email=fx@example.invalid -c commit.gpgsign=false commit -qm seed ) >/dev/null 2>&1 -TLO="$( "$BIN" "$TL" --whereis=helper --no-cache 2>/dev/null )" +# lean-answers lane: the "every hit printed" half reads the WHOLE list, so it asks for it (--whereis-listing=all, the +# pre-listing answer); the twin below holds the same facts on the default listing, where the refs are counted. +TLO="$( "$BIN" "$TL" --whereis=helper --no-cache --whereis-listing=all 2>/dev/null )" FIRSTDEF="$( printf '%s' "$TLO" | grep -o ']*kind="def"[^>]*>' | head -1 )" TESTDEF="$( printf '%s' "$TLO" | grep -o ']*p="tests/test_lib.py" l="3" kind="def"[^>]*>' )" { printf '%s' "$FIRSTDEF" | grep -q 'p="pkg/lib.py" l="1" kind="def" t=' && printf '%s' "$TESTDEF" | grep -q 'kind="def" test_local="1"' \ && [ "$( printf '%s' "$TLO" | sed 's///' | grep -o '//' | head -c 900; echo; } +# TWIN on the default listing (lean-answers lane): the same order and mark on the def rows, and every hit accounted for — +# printed (the def rows) or counted (), the two summing to hits=. +TLD="$( "$BIN" "$TL" --whereis=helper --no-cache 2>/dev/null )" +{ printf '%s' "$TLD" | grep -o ']*kind="def"[^>]*>' | head -1 | grep -q 'p="pkg/lib.py" l="1" kind="def" t=' \ + && printf '%s' "$TLD" | grep -o ']*p="tests/test_lib.py" l="3" kind="def"[^>]*>' | grep -q 'kind="def" test_local="1"' \ + && [ "$(( $( printf '%s' "$TLD" | sed 's///' | grep -o '//' | head -c 900; echo; } printf '%s' "$TLO" | grep -q 'TEST-LOCAL: \|test_local=1: ' \ && ok 'whereis (18n): test_local= is defined in the legend where it rides' || no 'whereis (18n): test_local= rides undefined' "$BIN" "$TL" --whereis=only_in_test --no-cache 2>/dev/null | grep -q 'test_local' \ diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index 044f7734d..966a43da0 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -161,7 +161,10 @@ grep -q 'ref="feat-unmerged"' "$TMP/w1" \ grep -q 'kind="def"' "$TMP/w1" \ && ok "whereis: classifies the definition site as def" || { no "whereis: no def-kind hit"; head -c 600 "$TMP/w1"; } -"$BIN" "$R" --whereis=computeBudget >"$TMP/w2" 2>/dev/null +# lean-answers lane: the ordering arms below read the doc-quoting row, a kind="ref" row the DEFAULT listing counts +# rather than prints — so they read the whole list (--whereis-listing=all, the pre-listing answer), and the twin +# after them holds the same facts on the default listing. +"$BIN" "$R" --whereis=computeBudget --whereis-listing=all >"$TMP/w2" 2>/dev/null grep -q 'on-head="1"' "$TMP/w2" \ && ok 'whereis: a live-line symbol reports on-head="1"' || { no 'whereis: expected on-head="1"'; head -c 600 "$TMP/w2"; } grep -q 'ref="HEAD"' "$TMP/w2" \ @@ -223,6 +226,17 @@ srcRow="$( grep -n 'engine\.' "$TMP/w2rows" | tail -1 | cut -d: -f1 )" && ok "whereis: every source row (last at $srcRow) precedes the doc row (at $docRow)" \ || no "whereis: docs interleave with source (doc $docRow, last source $srcRow)" +# TWIN on the default listing (lean-answers lane): the same def rows in the same order (source before the doc), the +# doc-quoting kind="ref" row not dropped but COUNTED — refs count= equals the whole list's kind="ref" rows. +"$BIN" "$R" --whereis=computeBudget >"$TMP/w2d" 2>/dev/null +tr '<' '\n' <"$TMP/w2d" | sed -n 's/^hit ref="\([^"]*\)".* p="\([^"]*\)" l="\([0-9]*\)" kind="\([a-z]*\)".*/\1 \4 \2:\3/p' >"$TMP/w2dall" +W2REFS="$( grep -c ' ref ' "$TMP/w2all" )" +{ [ "$( grep ' def ' "$TMP/w2dall" )" = "$( grep ' def ' "$TMP/w2all" )" ] && ! grep -q ' ref ' "$TMP/w2dall" \ + && [ "$( sed -n 1p "$TMP/w2dall" )" = "HEAD def engine.cpp:3" ] && [ "$W2REFS" -ge 1 ] \ + && grep -q "" \ + || { no "whereis (default listing twin): the default lost a def row or miscounted the refs"; cat "$TMP/w2dall"; grep -o ']*>' "$TMP/w2d"; } + # The finding's own repro, on this repo, when it is a git tree deep enough to answer. Skipped rather # than failed on a shallow/absent checkout: this arm is a bonus over the fixture arms above, which # already pin the ordering deterministically. @@ -316,7 +330,9 @@ legend_of(){ printf '%s' "$1" | grep -oE '' | head -1; } # L1 (2026-09-19): the CLI default legend is compact; §B8.2/§B12.2/§B11.2 read the FULL legend prose and count real rows # (the compact legend spells row shapes inside its comment), so these documents ask for the full legend. -W1="$( "$BIN" "$R" --whereis=computeBudget --limit=1 --legend=full 2>/dev/null )" +# lean-answers lane: the arithmetic below is over the WHOLE list (hits=), so it pages listing=all; the twin after it +# holds the default listing's own sum (shown + more + refs count = hits). +W1="$( "$BIN" "$R" --whereis=computeBudget --limit=1 --legend=full --whereis-listing=all 2>/dev/null )" WLEG="$( legend_of "$W1" )" { printf '%s' "$WLEG" | grep -q 'TRUNCATION' && printf '%s' "$WLEG" | grep -q 'more hits=N'; } \ && ok "§B8.2 whereis: the legend DEFINES its own remainder" \ @@ -335,13 +351,27 @@ if [ -n "$W_MORE" ]; then && ok "§B8.2 whereis: shown($W_SHOWN) + more($W_MORE) == hits($W_HITS), and $W_ROWS rows were really emitted" \ || no "§B8.2 whereis: shown=$W_SHOWN more=$W_MORE hits=$W_HITS rows=$W_ROWS — the remainder does not add up" # past-the-end page: the element must VANISH exactly when nothing is left, never print more="0". - WEND="$( "$BIN" "$R" --whereis=computeBudget --limit=1 --offset="$W_HITS" 2>/dev/null )" + WEND="$( "$BIN" "$R" --whereis=computeBudget --limit=1 --offset="$W_HITS" --whereis-listing=all 2>/dev/null )" printf '%s' "$WEND" | grep -q ' remainder" \ || ok "§B8.2 whereis: the remainder is absent on a page with nothing left" else no "§B8.2 whereis: --limit=1 produced no to check (fixture has too few hits)" fi +# TWIN on the default listing: the same arithmetic, the counted refs included — shown + more + refs count == hits. +W1D="$( "$BIN" "$R" --whereis=computeBudget --limit=1 2>/dev/null )" +WD_SHOWN="$( printf '%s' "$W1D" | grep -oE ']*' | grep -oE 'shown="[0-9]+"' | grep -oE '[0-9]+' )" +WD_HITS="$( printf '%s' "$W1D" | grep -oE ']*' | grep -oE ' hits="[0-9]+"' | grep -oE '[0-9]+' )" +WD_MORE="$( printf '%s' "$W1D" | grep -oE '//' | grep -oE '/dev/null )" +printf '%s' "$WDEND" | grep -q ' remainder" \ + || ok "§B8.2 whereis (default listing twin): no past the end (the count is not a page remainder)" # the stray-content sibling: force the per-ref file listing (capped at 12) past its cap on its own branch. g checkout -q main @@ -451,6 +481,107 @@ else printf ' SKIP §B11.2 MCP parity (no python3)\n' fi +# ── LEAN: the default listing and the tip/date hoist (lean-answers lane, src/crossref.h WhereisListing) ───────── +# The default lists every kind="def" row and COUNTS the kind="ref" rows in one element; the +# next= (--whereis-listing=refs) lists exactly those rows; --whereis-listing=all is the whole hit list, the uncapped +# variant the lean answer is graded against. A row on HEAD's commit omits tip=/date= (at= and head_date= carry them). +# Every assertion below compares the lean answer against the SAME binary's listing=all answer, so the arms read +# "nothing was lost" directly, not against a remembered byte shape. +LR="$TMP/lean"; mkdir -p "$LR" +lg(){ git -C "$LR" "$@" >/dev/null 2>&1; } +lg init -q -b main; lg config commit.gpgsign false +printf '# lean fixture\n' >"$LR/README.md"; lg add -A; lg commit -qm root +# the branch forks from the bare root, so its tree holds ONLY its own definition (the HEAD symbols stay single-tree) +lg checkout -qb feat-lean +printf 'int zqLean( int x, int y )\n{\n return x + y;\n}\n' >"$LR/branch_only.c" +lg add -A +GIT_COMMITTER_DATE="2026-02-02T00:00:00Z" GIT_AUTHOR_DATE="2026-02-02T00:00:00Z" lg commit -qm "branch def" +lg checkout -q main +printf 'int zqLean( int x );\nint zqDefOnly( void );\n' >"$LR/lean.h" +printf '#include "lean.h"\nint zqLean( int x )\n{\n return x + 1;\n}\nint zqDefOnly( void )\n{\n return 3;\n}\n' >"$LR/lean.c" +printf '#include "lean.h"\nint useA( void ) { return zqLean( 1 ); }\nint useB( void ) { return zqLean( 2 ); }\n' >"$LR/use.c" +printf '# notes\n\nzqLean is the entry point; zqMentionOnly is a word no code defines.\n' >"$LR/NOTES.md" +{ printf '#include "lean.h"\n'; i=0; while [ $i -lt 70 ]; do printf 'int many%d( void ) { return zqLean( %d ); }\n' $i $i; i=$((i+1)); done; } >"$LR/many.c" +lg add -A; lg commit -qm base +LHEAD="$( git -C "$LR" rev-parse --short=9 HEAD )"; LFEAT="$( git -C "$LR" rev-parse --short=9 feat-lean )" +lw(){ "$BIN" "$LR" --whereis="$1" --no-cache "${@:2}" 2>/dev/null; } +hits_of(){ tr '<' '\n' | grep "^hit " | grep "kind=\"$1\"" | sed 's|/>$||; s|/>.*||'; } +lroot(){ grep -o ']*>' | head -1; } +LDEF="$( lw zqLean )"; LALL="$( lw zqLean --whereis-listing=all --limit=1000 )" +# (L1) the def rows: every kind="def" row of the whole list, byte for byte, in order — and no kind="ref" row +[ -n "$( printf '%s' "$LALL" | hits_of def )" ] && [ "$( printf '%s' "$LDEF" | hits_of def )" = "$( printf '%s' "$LALL" | hits_of def )" ] \ + && [ -z "$( printf '%s' "$LDEF" | hits_of ref )" ] \ + && ok "LEAN (L1): the default lists every kind=\"def\" row of listing=all, byte for byte, and no kind=\"ref\" row" \ + || { no "LEAN (L1): the default's def rows differ from listing=all's (or a ref row is listed)"; printf '%s\n' "$LDEF"; } +# (L2) the count is exact: refs count= == the kind="ref" rows of the whole list +NREF="$( printf '%s' "$LALL" | hits_of ref | wc -l | tr -d ' ' )" +CNT="$( printf '%s' "$LDEF" | grep -oE ' equals the $NREF kind=\"ref\" rows listing=all prints" \ + || no "LEAN (L2): refs count='${CNT:-none}' vs $NREF ref rows in listing=all" +# (L3) next= round-trips: the pasted next= (all its pages) prints exactly listing=all's ref rows, byte for byte +NXT="$( printf '%s' "$LDEF" | grep -oE ']*next="[^"]*"' | sed 's/.*next="//; s/"$//' )" +case "$NXT" in "--whereis=zqLean --whereis-listing=refs") ok "LEAN (L3a): next= names the refs listing of the same symbol" ;; + *) no "LEAN (L3a): next='$NXT'" ;; esac +P1="$( lw zqLean --whereis-listing=refs )"; P2="$( lw zqLean --whereis-listing=refs --offset=60 )" +{ printf '%s' "$P1" | hits_of ref; printf '%s' "$P2" | hits_of ref; } >"$TMP/lean_next" +printf '%s' "$LALL" | hits_of ref >"$TMP/lean_allref" +[ -s "$TMP/lean_allref" ] && cmp -s "$TMP/lean_next" "$TMP/lean_allref" && [ -z "$( printf '%s' "$P1" | hits_of def )" ] \ + && printf '%s' "$P1" | lroot | grep -q ' listing="refs"' && printf '%s' "$P1" | grep -q "" \ + && ok "LEAN (L3b): next= (two pages, the first ending in ) reproduces listing=all's ref rows byte for byte" \ + || { no "LEAN (L3b): the refs listing differs from listing=all's ref rows"; diff "$TMP/lean_next" "$TMP/lean_allref" | head -5; } +# (L4) zero refs: no , no listing=, byte-identical to listing=all +D0="$( lw zqDefOnly )"; A0="$( lw zqDefOnly --whereis-listing=all )" +[ -n "$( printf '%s' "$D0" | hits_of def )" ] && [ "$D0" = "$A0" ] && ! printf '%s' "$D0" | grep -q ', identical to listing=all +M0="$( lw zqMentionOnly )"; MA="$( lw zqMentionOnly --whereis-listing=all )" +[ -n "$( printf '%s' "$M0" | hits_of ref )" ] && [ "$M0" = "$MA" ] && ! printf '%s' "$M0" | grep -q '), drops complete= +L1R="$( lw zqLean --limit=1 )" +printf '%s' "$L1R" | lroot | grep -q ' shown="1" capped="1" total="3"' && printf '%s' "$L1R" | grep -q '' \ + && ! printf '%s' "$L1R" | lroot | grep -q 'complete=' && printf '%s' "$LDEF" | lroot | grep -q ' complete="1"' \ + && ok "LEAN (L7): shown=/capped=/ window the def rows; complete= rides the uncut defs listing, not the cut one" \ + || { no "LEAN (L7): the window does not read the listed rows"; printf '%s\n' "$L1R" | lroot; } +# (L8) refusals: an unknown value, and the modifier without --whereis +"$BIN" "$LR" --whereis=zqLean --whereis-listing=bogus >/dev/null 2>&1; rcb=$? +"$BIN" "$LR" --whereis-listing=all >/dev/null 2>&1; rcn=$? +[ $rcb -eq 1 ] && [ $rcn -eq 1 ] && ok "LEAN (L8): --whereis-listing=bogus and a bare --whereis-listing refuse (exit 1)" \ + || no "LEAN (L8): refusal exits $rcb / $rcn (want 1 / 1)" +# (L9) both legends define what the lean answer carries +printf '%s' "$LDEF" | grep -oE '' | head -1 | grep -q 'listing=defs' \ + && printf '%s' "$LDEF" | grep -oE '' | head -1 | grep -q 'head_date=' && printf '%s' "$LDEF" | grep -oE '' | head -1 | grep -q 'refs count=' \ + && lw zqLean --legend=full | grep -oE '' | head -1 | grep -q 'LISTING:.*TIP AND DATE:' \ + && ok "LEAN (L9): compact and full legends define listing=, head_date=, refs count=" \ + || no "LEAN (L9): a lean attribute rides undefined" +# (L10) MCP twin: the same answers over MCP (default, listing all, refs) and the same refusal; one call per session +mcpw(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whereis","arguments":{"path":"%s","symbol":"zqLean"%s}}}\n' "$LR" "$1" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line); c=d.get("result",{}).get("content") + if c: print(c[0].get("text",""),end="") + elif "error" in d: print("ERROR",d["error"].get("code"),end="")'; } +if command -v python3 >/dev/null 2>&1; then + [ "$( mcpw '' )" = "$( lw zqLean )" ] && [ "$( mcpw ',"listing":"all"' )" = "$( lw zqLean --whereis-listing=all )" ] \ + && [ "$( mcpw ',"listing":"refs"' )" = "$( lw zqLean --whereis-listing=refs )" ] && [ "$( mcpw ',"listing":"bogus"' )" = "ERROR -32602" ] \ + && ok "LEAN (L10): MCP whereis default / listing:all / listing:refs are byte-identical to the CLI; listing:bogus refuses -32602" \ + || no "LEAN (L10): the MCP twin differs from the CLI" +else + printf ' SKIP LEAN (L10) MCP twin (no python3)\n' +fi + if command -v xmllint >/dev/null 2>&1; then if printf '%s' "$SW" | xmllint --noout - 2>/dev/null; then ok "capped stray-content still G4 clean"; else no "capped stray-content XML malformed"; fi if printf '%s' "$W1" | xmllint --noout - 2>/dev/null; then ok "paged whereis still G4 clean"; else no "paged whereis XML malformed"; fi From 62d2f85b71cfa120bee7c700358652d24c2b6b1b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:56:44 -0400 Subject: [PATCH 034/229] for: the via clause rides on the sections' own node sets, not the row head The --for header asked "may a row carry via=name" over the whole ranked row head, so an answer whose rows were all evidence-bound still paid the clause (docdemote's --no-route bundle: +71 tokens, no via row). It now reads the node set of the one section that renders : the compact route's hop ids, T3's auto-body candidates, or --detail's head (forCompactHopIds / computeAutoBodyCandidateIds / forDetailIds, each shared with the section that renders it). estchargecheck #18: the wide control moves 1200 -> 1400 (its fixture's bare cross-file calls are name-only; the document now prices at 1269, logged). docdemote: the conceptual golden re-pinned (logged); --no-route unchanged. Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 75 +++++++++++++++++++++++++----------- test/docdemotecheck.sh | 11 +++--- test/docdemotegolden_for.xml | 2 +- test/estchargecheck.sh | 6 ++- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index a56adbe8e..5e366a728 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2070,6 +2070,36 @@ inline std::size_t enrichmentLegendBytesEmitted( const ForEnrichmentPlan& plan, return plan.compact ? kForCompactLegendHops.size() : kForCompactLegendBodies.size(); } +// The compact route's rows: the positive-score head of the ranked surface, capped at kPackTaskBodyCandidates. One +// spelling, read by buildForCompactHops and by the header's via="name" question (FE-B), so the two cannot disagree. +inline std::vector forCompactHopIds( const std::vector& lensSurfaceIds, const std::vector& lensRank ) +{ + std::vector hopIds; + for( rw::NodeId sid : lensSurfaceIds ) + { + if( hopIds.size() >= rw::kPackTaskBodyCandidates || lensRank[sid] <= 0.0f ) + { + break; + } + hopIds.push_back( sid ); + } + return hopIds; +} +// --detail=N's bodies: the top min(N, forTopN) symbols by (score desc, id asc) — the order the sigs use. One spelling, read +// by the --detail section and by the header's via="name" question (FE-B). +inline std::vector forDetailIds( const rw::IngestResult& ing, const std::vector& lensRank, int detail, int forTopN ) +{ + const std::size_t S = ing.symbols.size(); + std::vector ids( S ); + for( rw::NodeId i = 0; i < S; ++i ) + { + ids[i] = i; + } + rw::sortutil::radixSortByScoreDescId( ids, lensRank ); + ids.resize( std::min( { std::size_t( detail ), std::size_t( forTopN ), S } ) ); + return ids; +} + ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector& lensSurfaceIds, const std::vector& lensRank, std::size_t committedBytes, std::size_t bundleBudget, rw::RedactCounts* redactPtr, @@ -2082,15 +2112,7 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest // candidates: EXACTLY the head buildForAutoBodies would have bodied — same rule, same order, so the // two shapes describe the same symbols and only differ in how much of each they serve. - std::vector hopIds; - for( rw::NodeId sid : lensSurfaceIds ) - { - if( hopIds.size() >= rw::kPackTaskBodyCandidates || lensRank[sid] <= 0.0f ) - { - break; - } - hopIds.push_back( sid ); - } + const std::vector hopIds = forCompactHopIds( lensSurfaceIds, lensRank ); std::size_t leftBytes = bundleBudget > committedBytes ? bundleBudget - committedBytes : 0; if( cfg.tokenBudget == 0 ) @@ -2630,12 +2652,27 @@ std::optional runForLens( const MainDispatch& d ) const rw::NodeId sid = lensSurfaceIds[i]; forModScopePresent = lensRank[sid] > 0.0f && ing.symbols[sid].kind == rw::SymKind::ModuleScope; } - // FE-B: the same row head again — may one of its rows carry via="name"? + // FE-B: may a row of this bundle carry via="name"? Only the sections that render can, and each one's + // node set is known before the header: the compact route's hop ids, T3's auto-body candidates, or --detail's head + // (autoBundleMode excludes --detail). Their name-only out-edges decide it — over-approximating only the byte ladder + // and the 16-per-symbol cap inside those sections, never the rows the header itself lists. bool forViaPresent = false; - for( std::size_t i = 0; i < lensSurfaceIds.size() && i < forRowHead && !forViaPresent; ++i ) + if( !g.outNameOnly.empty() ) { - const rw::NodeId sid = lensSurfaceIds[i]; - forViaPresent = lensRank[sid] > 0.0f && rw::namesOnlyOutAny( g.outOff, g.outNameOnly, std::span( &sid, 1 ) ); + std::vector viaIds; + if( autoBundleMode && plan.compact ) + { + viaIds = forCompactHopIds( lensSurfaceIds, lensRank ); + } + else if( autoBundleMode && plan.autoBodies ) + { + viaIds = computeAutoBodyCandidateIds( ing, lensSurfaceIds, lensRank, routeAnchorDefs ); + } + else if( cfg.detail > 0 ) + { + viaIds = forDetailIds( ing, lensRank, cfg.detail, forTopN ); + } + forViaPresent = rw::namesOnlyOutAny( g.outOff, g.outNameOnly, viaIds ); } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, @@ -3222,15 +3259,7 @@ std::optional runForLens( const MainDispatch& d ) // the lens. N=0 emits nothing → byte-identical to a run without --detail. if( cfg.detail > 0 ) { - const std::size_t S = ing.symbols.size(); - detailIds.resize( S ); - for( NodeId i = 0; i < S; ++i ) - { - detailIds[i] = i; - } - rw::sortutil::radixSortByScoreDescId( detailIds, lensRank ); // (score desc, id asc) — same order as the sigs - const std::size_t detN = std::min( { std::size_t( cfg.detail ), std::size_t( forTopN ), S } ); - detailIds.resize( detN ); + detailIds = forDetailIds( ing, lensRank, cfg.detail, forTopN ); // (score desc, id asc) — same order as the sigs // Composes with --max-tokens: when set, it bounds the body byte budget (same conservative rate the // map path uses). §F1: --token-budget SHAPES this lens (D10 — trims to fit, always exit 0), so it // has to bound the bodies as well; before this it bounded ONLY and the bodies rode along on @@ -3283,7 +3312,7 @@ std::optional runForLens( const MainDispatch& d ) if( enrich.surfaceOff || enrich.legendOff ) { headerParts.autoBundle = headerParts.compactBundle = false; - headerParts.viaPresent = headerParts.viaPresent && cfg.detail > 0; // FE-B: only --detail's bodies are left to carry it + headerParts.viaPresent = false; // FE-B: the section that could carry it is gone (autoBundleMode excludes --detail) headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } } diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 21edc8357..be7706f1a 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,12 +99,13 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null -# RE-PIN 2026-10-04 (lane FE-B, receiver evidence): docdemotegolden_for.xml 5809 -> 6302 B (est_tokens "2328" -> "2521"). +# RE-PIN 2026-10-04 (lane FE-B, receiver evidence): docdemotegolden_for.xml 5821 -> 6010 B (est_tokens "2328" -> "2404"). # CAUSE: pagecache.py's `evict_one( self )` names a function its file never imports — Python resolves it nowhere — so -# FE-B binds it by NAME ALONE: the hop's row carries via="name", and the hops block carries the -# via="name" reading as its own comment (graphlegend.h kViaNameLegend). Verified before re-pinning, against the pre-change -# golden: those three lines (the root's est_tokens=, the comment, the row's attribute) are the whole diff — no ranking, -# demotion or route byte moved, and arm (f)'s own assertions still hold. +# FE-B binds it by NAME ALONE: the hop's row carries via="name", and --for's header defines it (one +# present-only clause, graphlegend.h kForViaNameClause, on the hop ids the compact route serves). Verified before +# re-pinning, against the pre-change golden: the root's est_tokens=, the header clause and the row's attribute are the whole +# diff — no ranking, demotion or route byte moved, and arm (f)'s own assertions still hold. The noroute golden is +# UNCHANGED: its carry no row through a name-only edge, so the clause does not ride (present-only). # RE-PIN 2026-09-13 (merge of lane/sc-legend and lane/for-widen): docdemotegolden_for.xml RE-MEASURED on the # MERGED tree at 5,809 B (est_tokens "2328"), from 5,887 on for-widen's tree and 5,425 on sc-legend's. Neither # lane's own number is the merged one, so this is measured, not summed. Three identified changes, and the golden diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index d7f19d8a7..b459c0cbe 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 82d1100da..e7d0a721d 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -1670,7 +1670,11 @@ PYRZ rz_run(){ ( cd "$RZ" && "$BIN" corpus --for="widget ping box router" --detail=1 --token-budget="$1" --no-cache --legend=full ) >"$RZ/o.xml" 2>/dev/null; } rz_est(){ grep -aoE 'est_tokens="[0-9]+"' "$RZ/o.xml" | head -1 | tr -dc '0-9'; } rz_note(){ grep -acF '[legend clauses:' "$RZ/o.xml"; } -RZ_WIDE=1200 +# FE-B (test/receiverevidencecheck.sh): 1200 -> 1400. The fixture's bare widgetPingStepN() calls name a function another +# file defines with no import, so their rows are via="name" and --for's header now defines it (~190 B, never +# dropped). The full document prices at est_tokens=1269 (it was 1162, 38 under the old 1200), so 1200 no longer carried +# its legend; 1400 is the wide control again with ~130 tokens of headroom. The probe and the control still read E off it. +RZ_WIDE=1400 rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" # THE CLAUSES THIS ARM COUNTS — all THREE of the droppable trio (CodeRabbit, PR #215). It counted two: the # confidence reading and the tail reading, but not the route= reading the arm's own paragraph above names. From 04d6fdcda9e79a21cabee4d5fe30df4fa631f2a1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:14:30 -0400 Subject: [PATCH 035/229] mcp: --mcp-legend=inline keeps the pre-lane posture; core marked served only when sent --mcp-legend=session|inline (stdio only): inline keeps every answer's legend until the client reads ripwire://legend-dict, byte for byte the posture before the legend session became the default (initialize pointer included). A ref answer that falls back to the inline answer (it would be longer) no longer marks the dictionary core served: the core is marked only when a ref answer carried it. legendrefcheck: arms (A)-(I) run on --mcp-legend=inline (their contract lives there); new arm (J) is their twin on the default session (first answer inline and byte-identical, later answers ref or inline, the core carried once, no entry sent twice, roster honesty, size, G4, opt-out). Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 24 ++++++++++++++++++ src/legenddict.h | 7 ++++-- src/main.cpp | 3 ++- src/mcp.h | 16 ++++++++++-- test/legendrefcheck.sh | 57 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/cli.h b/src/cli.h index 19c4868db..5efc7bdc7 100644 --- a/src/cli.h +++ b/src/cli.h @@ -47,6 +47,7 @@ struct Config // transport. Required for a non-loopback bind and for --allow-remote-edits. bool allowRemoteEdits = false; // --allow-remote-edits: permit the 3 edit verbs over --listen (refused by // default); forces the token requirement even on loopback. + std::string_view mcpLegend; // --mcp-legend=session|inline: the stdio server's legend posture (default session) std::string_view mcpTools; // --mcp-tools=SPEC: list (and answer) only these MCP tools — names and/or the // core/full profiles, validated against the tool table in main.cpp (mcp.h). std::vector excludes; // --exclude=SUBSTR (repeatable): drop matching paths @@ -2720,6 +2721,12 @@ inline constexpr char kHelpTail[] = " and a run without prints usage, or that same line from such a directory. Each\n" " tool call over the --max-memory limit is refused by name; an answer from an index the\n" " memory guard cut carries _memory_stop in its envelope.\n" + " --mcp-legend=WHEN the stdio MCP server's legend posture: session (the default) or inline\n" + " session: the first answer of a session carries its legend inline; later answers list\n" + " rows first, carry each definition only the first time the session meets it, and end\n" + " with (the first of them also carries the core).\n" + " inline: every answer keeps its legend until the client reads ripwire://legend-dict.\n" + " legend:\"compact\"/\"full\" on a call keeps that answer inline either way. Stdio only.\n" " --mcp-tools=LIST list only these MCP tools (names and/or the core/full profiles, default full).\n" " A comma list of tool names and/or profiles, unioned. core = explore, batch, from_trace,\n" " impact, uses, fetch_body, edit_check, quality_delta (the loop the server's own\n" @@ -4539,6 +4546,12 @@ inline void validateModifierGuards( Config& c ) noexcept rw::emitRaw( stderr, "ripwire: --mcp-token is read by the --listen HTTP transport only — pass both (e.g. ripwire . --listen=127.0.0.1:8765 --mcp-token=SECRET)\n" ); c.ok = false; } + // --mcp-legend= shapes the stdio session's legend posture; the HTTP transport holds no session (every answer inline). + if( !c.mcpLegend.empty() && ( !c.mcp || !c.listen.empty() ) ) + { + rw::emitRaw( stderr, "ripwire: --mcp-legend is read by the stdio MCP server only (--listen holds no legend session) — e.g. ripwire . --mcp --mcp-legend=inline\n" ); + c.ok = false; + } if( !c.mcpTools.empty() && !c.mcp ) // --listen sets c.mcp, so this covers both transports { rw::emitRaw( stderr, "ripwire: --mcp-tools is read by the MCP server only — pass --mcp (or --listen) too, e.g. ripwire . --mcp --mcp-tools=core\n" ); @@ -5336,6 +5349,17 @@ inline Config parseArgs( int argc, char** argv ) noexcept } c.grepScope = v; } + else if( startsWith( a, "--mcp-legend=" ) ) + { + // A closed value set, refused on an unknown value (the --whereis-listing= rule just below). + const std::string_view v = a.substr( 13 ); + if( v != "session" && v != "inline" ) + { + rw::emitTo( stderr, "ripwire: --mcp-legend={} — unknown value (supported: session|inline), e.g. --mcp-legend=inline\n", std::string_view( v.data(), v.size() ) ); + c.ok = false; return c; + } + c.mcpLegend = v; + } else if( startsWith( a, "--whereis-listing=" ) ) { // A closed value set, refused on an unknown value (the --grep-in= rule): a typo must not quietly read diff --git a/src/legenddict.h b/src/legenddict.h index 61aabffe0..60918f24b 100644 --- a/src/legenddict.h +++ b/src/legenddict.h @@ -799,12 +799,14 @@ inline RefOutcome applyRefPosture( std::string& doc, LegendSession& session ) // lean-answers lane: a session OPENED by its first inline answer (openSessionWith, the MCP default) was never sent the // core — the reading of itself. Its first ref answer carries every unserved core entry in the // same trailing comment as any other unserved entry, so the honesty rule above holds without a resource read. - std::string core; + // The core ids are marked served ONLY when the ref answer goes out: an answer kept inline (below) never carried them. + std::string core; + std::vector coreIds; for( std::size_t id = 0; id < kCoreCount; ++id ) { if( !session.isServed( id ) ) { - head->touched.push_back( id ); + coreIds.push_back( id ); core += core.empty() ? "" : " "; core.append( entryBody( id ) ); core += '.'; @@ -836,6 +838,7 @@ inline RefOutcome applyRefPosture( std::string& doc, LegendSession& session ) out += parts.trailer; out += shape->closeTag; ENSURES( out.size() <= doc.size(), "a ref answer is never longer than the inline answer it replaces" ); + for( const std::size_t id : coreIds ) { session.markServed( id ); } // carried by this ref answer's trailing comment doc.swap( out ); return RefOutcome::Ref; } diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..fd814e8dd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4329,7 +4329,8 @@ static int dispatchMain( const rw::Config& cfg, char** argv ) mcpRoots.emplace_back( r ); } return runMcp( { .topK = cfg.topK, .stable = cfg.stable, .noRedact = cfg.noRedact, .root = std::string( cfg.rootPath ), - .roots = mcpRoots, .toolMask = tools.mask, .toolSpec = std::string( cfg.mcpTools ) } ); // P2-C: --mcp turns --stable on by default (set in parseArgs); A3-F3: the server redacts by default like the CLI + .roots = mcpRoots, .toolMask = tools.mask, .toolSpec = std::string( cfg.mcpTools ), + .legendInline = cfg.mcpLegend == "inline" } ); // P2-C: --mcp turns --stable on by default (set in parseArgs); A3-F3: the server redacts by default like the CLI } // ── multi-root workspace refusals: each cut verb refuses with ONE clear stderr diff --git a/src/mcp.h b/src/mcp.h index f6baf4f5d..2aec52dc0 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -531,6 +531,9 @@ struct McpDispatchPolicy // time); the HTTP transport passes none, so every answer there keeps its legend inline and the resource read that // switches a stdio session to legend="ref" only serves text. See legenddict.h. legenddict::LegendSession* legendSession = nullptr; + // lean-answers lane: whether that session OPENS on its first reducible answer (the stdio default) or only on a read of + // ripwire://legend-dict (--mcp-legend=inline, the pre-lane posture). Read only where legendSession is set. + bool legendOpensOnFirstAnswer = false; // --mcp-tools: bit i = kMcpVerbTable[ i ] is listed and callable. The whole catalog unless the flag narrowed it; // toolSpec is the flag's value as typed, rendered only under a subset (the refusal and the instructions note). McpToolMask toolMask = kMcpAllToolsMask; @@ -1034,6 +1037,13 @@ inline std::string mcpLegendPointer( const McpDispatchPolicy& policy ) { return {}; } + if( !policy.legendOpensOnFirstAnswer ) // --mcp-legend=inline: the pre-lane pointer, byte for byte + { + return " Legends: each answer defines its own attributes until this session reads the resource ripwire://legend-dict " + "once (dictv=" + legenddict::dictionaryVersion() + "); after that answers list rows first, carry a definition " + "only the first time the session meets it, and end with . legend:\"compact\" on a call " + "that takes it keeps that answer's legend inline."; + } return " Legends: the session's first answer defines its own attributes inline (dictv=" + legenddict::dictionaryVersion() + "); later answers list rows first, carry a definition only the first time the session meets it, and end with " "; ripwire://legend-dict/full holds every definition. legend:\"compact\" on a call " @@ -1364,7 +1374,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "edit_check", pathIsRequired ) + "}," // The cross-branch + dark-content verbs. Read-only git plumbing, no index // coupling for the first two (they read OTHER refs' blobs, which the index never ingested). - + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"whereis\",\"description\":\"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-addressed), so N branches cost about one tree. kind=def on a REF row is a LEXICAL heuristic (ref blobs are raw text, never ingested) — for HEAD's parsed answer use find_symbol/fetch_body. symbol = the name, or an @FILE:LINE seed; kind = optional ref-name substring filter, echoed as filter=; limit/offset page the hits (first 60). listing = defs (default: the kind=def rows, the kind=ref rows counted in ), refs or all. at= is HEAD's sha; +dirty = changed paths read from the working tree (worktree=). Single-root; read-only.\"," + + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"whereis\",\"description\":\"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-addressed), so N branches cost about one tree. kind=def on a REF row is a LEXICAL heuristic (ref blobs are raw text, never ingested) — for HEAD's parsed answer use find_symbol/fetch_body. symbol = the name, or an @FILE:LINE seed; kind = optional ref-name substring filter, echoed as filter=; limit/offset page the hits (first 60). at= is HEAD's sha; +dirty = changed paths read from the working tree (worktree=). Single-root; read-only.\"," + mcprefuse::toolMetadataFor( "whereis", pathIsRequired ) + "}," ) + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"stray_content\",\"description\":\"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the work — the case `git cherry` structurally cannot see; merged branches are omitted and counted; v=unknown is a branch this scan could NOT analyse at all (no merge-base, unrelated history), not a fourth kind of divergence. Every file row carries its raw del/redone/sim evidence. Line-granular, not semantic. kind = optional ref-name substring filter, echoed as filter=; limit/offset page the refs. Single-root; read-only.\"," + mcprefuse::toolMetadataFor( "stray_content", pathIsRequired ) + "}," ) + @@ -1540,7 +1550,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo const bool legendSessionPosture = legendSession != nullptr && ( legendDeclaredHere ? ( legendArg.empty() || legendArg == "ref" ) : name == "for" ); const bool legendRefPosture = legendSessionPosture && legendSession->refOn; - const bool legendOpensSession = legendSessionPosture && !legendSession->refOn; + const bool legendOpensSession = legendSessionPosture && !legendSession->refOn && policy.legendOpensOnFirstAnswer; // ── W3FIX H4/M5: every NUMERIC argument through the ONE guarded reader ───────────────────────── // @@ -2857,6 +2867,7 @@ struct McpStdioConfig std::vector roots; McpToolMask toolMask = kMcpAllToolsMask; std::string toolSpec; + bool legendInline = false; // --mcp-legend=inline: the session opens only on a read of the core }; // The stdio server's dispatch policy. Same root plumbing as runMcpHttp(), building McpDispatchPolicy::defaultRoot @@ -2887,6 +2898,7 @@ inline McpDispatchPolicy mcpStdioPolicy( const McpStdioConfig& config ) policy.defaultRoot = defaultRoot; // "" unless a startup root was given — see the comment above policy.toolMask = config.toolMask; // --mcp-tools policy.toolSpec = config.toolSpec; + policy.legendOpensOnFirstAnswer = !config.legendInline; // lean-answers lane: the default session posture // R2a (the 2026-08-12 usage mine): with NO startup root, resolve the launch cwd ONCE as the softest // default — see McpDispatchPolicy::assumedRoot for the full contract and mcpResolveAssumedRoot for diff --git a/test/legendrefcheck.sh b/test/legendrefcheck.sh index c9a5dd9e9..bb41db8c0 100755 --- a/test/legendrefcheck.sh +++ b/test/legendrefcheck.sh @@ -57,8 +57,13 @@ def ok(m): print(f" PASS {m}") def no(m): print(f" FAIL {m}"); fails.append(m) def check(c, m): (ok if c else no)(m) -def session(calls): +# lean-answers lane: the stdio default now OPENS the session on its first reducible answer. Arms (A)-(I) assert the +# pre-lane contract ("ref only after the read") and run it where it still lives, --mcp-legend=inline; arm (J) is their +# twin on the default posture. +OLD_POSTURE = ["--mcp-legend=inline"] +def session(calls, server=None): """One stdio MCP session. calls: [(tool, args)] or ('@method', params). Returns [(kind, text)].""" + server = OLD_POSTURE if server is None else server reqs = [{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}}] for i, (name, args) in enumerate(calls, 1): if name.startswith("@"): @@ -66,7 +71,7 @@ def session(calls): else: a = dict(args); a.setdefault("path", FX) reqs.append({"jsonrpc": "2.0", "id": i, "method": "tools/call", "params": {"name": name, "arguments": a}}) - p = subprocess.run([BIN, "--mcp"], input="".join(json.dumps(r) + "\n" for r in reqs).encode(), capture_output=True, cwd=FX) + p = subprocess.run([BIN, "--mcp"] + server, input="".join(json.dumps(r) + "\n" for r in reqs).encode(), capture_output=True, cwd=FX) out = [] for line in p.stdout.decode(errors="replace").splitlines(): if not line.strip(): continue @@ -314,6 +319,54 @@ for i, t in refs: illformed.append(f"{CALLS[i][0]}:{e}") check(NREF >= 12 and not illformed, f"(I) over {NREF} ref answers, every one parses and keeps its newlines inside CDATA ({illformed})") +# ── (J) THE DEFAULT SESSION (lean-answers lane): the twin of (A)-(F)/(I) on the stdio default ───────────────────────────── +# The session opens on its first answer the dictionary can reduce: that answer is the inline answer, byte for byte; +# every later reducible answer is ref (or, where ref would be longer, the inline answer); the core reaches the session +# once, in its first ref answer; no entry is sent twice; every roster attribute is defined where the reader holds it. +print("=== (J) the default session: first answer inline, later answers ref ===") +DEF = [] +D = session(CALLS + CALLS, server=DEF) +check(len(D) == 1 + 2 * N, f"(J) the default session answered every request ({len(D) - 1} of {2 * N})") +dinit = D[0][1] if D and D[0][0] == "init" else "" +check(len(dinit.encode()) <= 1000 and f"dictv={DICTV}" in dinit and "ripwire://legend-dict/full" in dinit, + f"(J) the default initialize names the posture, the same dictv and the full dictionary in <= 1,000 B ({len(dinit.encode())} B)") +dans = D[1:] +check(dans[0][1] == pre[0][1] and not is_ref(dans[0][1]), "(J) the session's first answer is the inline answer, byte-identical to the pre-read one") +drefs = [(i, t) for i, (_, t) in enumerate(dans) if i > 0 and t.lstrip().startswith("<")] +DNREF = sum(1 for _, t in drefs if is_ref(t)) +dinline = [CALLS[i % N][0] for i, t in drefs if not is_ref(t) and t != pre[i % N][1]] +check(DNREF >= 12 and not dinline, f"(J) every later XML answer is ref ({DNREF}) or the inline answer byte for byte (third shapes: {dinline})") +core0 = "" +firstref = next((t for _, t in drefs if is_ref(t)), "") +check(core0 in "".join(comments(firstref)), "(J) the first ref answer carries the dictionary core (the reading of )") +dpost = "".join("".join(comments(t)) for _, t in drefs if is_ref(t)) +dtwice = [e[:60] for e in entries if len(e) > 12 and dpost.count(e) > 1] +check(not dtwice and dpost.count(core0) == 1, f"(J) no dictionary entry reaches the session twice through its ref answers ({dtwice[:3]})") +ddiff = [CALLS[i % N][0] for i, t in drefs if is_ref(t) and pairs(t) != pairs(full_by.get(i % N) if CALLS[i % N][0] != "for" else pre[i % N][1])] +check(not ddiff, f"(J) each ref answer carries the same attribute name=value multiset as its inline twin ({ddiff})") +dheld, dgaps = "", [] +for i, (_, t) in enumerate(dans): + dheld += "".join(comments(t)) + if t.lstrip().startswith("<") and is_ref(t): + dgaps += [f"{CALLS[i % N][0]}@{a}" for a in sorted(roster_hits(t)) if not defined(a, dheld)] +check(not dgaps, f"(J) no roster attribute of a ref answer rides undefined by the bytes this session sent ({dgaps[:6]})") +dlong = [f"{CALLS[i % N][0]} {len(t)}>{len(pre[i % N][1])}" for i, t in drefs if is_ref(t) and len(t) > len(pre[i % N][1])] +check(not dlong, f"(J) every ref answer <= the inline answer it replaces ({dlong})") +dill = [] +for i, t in drefs: + if not is_ref(t): continue + try: + ET.fromstring(t) + if "\n" in strip_cdata(t).rstrip("\n"): dill.append(CALLS[i % N][0] + ":newline") + except ET.ParseError as e: + dill.append(f"{CALLS[i % N][0]}:{e}") +check(not dill, f"(J) every default-session ref answer is well-formed G4 XML ({dill})") +DI = session([CALLS[0]] + [(v, dict(a, legend="compact")) for v, a in CALLS if v != "for"], server=DEF) +check([t for _, t in DI[2:]] == pre_nf, "(J) legend:\"compact\" in an open default session keeps that answer inline, byte-identical") +r = subprocess.run([BIN, "--mcp-legend=inline"], capture_output=True, text=True) +r2 = subprocess.run([BIN, "--mcp", "--mcp-legend=bogus"], capture_output=True, text=True, input="") +check(r.returncode != 0 and r2.returncode != 0, f"(J) --mcp-legend without --mcp, and an unknown value, refuse (rc={r.returncode}/{r2.returncode})") + print(f"legendrefcheck: {'FAIL' if fails else 'PASS'} ({len(fails)} failing)") sys.exit(1 if fails else 0) PY From 874eb65d56d2bb1fbe27797fec46fe334b289e5d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:14:30 -0400 Subject: [PATCH 036/229] for: the zero reading is exempt from the sig ledger Spelling 'an absent cx/ccx/in is 0' in the legend cost two round-1 answers a row (the header charge grew). The three readings (compact, full, MCP for) are now exempt like the other legend readings, so the rows kept are exactly the rows kept before the elision. Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 17 +++++++++++++++++ src/mcpverbs.h | 1 + src/verbs_for.h | 13 ++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 80a40bf4a..aa49c6b23 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -352,8 +352,25 @@ inline constexpr std::string_view kForRouteCodeLegend = // bytes verbatim AND the session was already served them, so a clause spelled twice would silently stop being // dropped (the answer stays honest — it just keeps the clause inline). Byte-identical to the inline spellings. // No "--" in any of them: they ride inside an XML comment (G4). +// lean-answers lane: the zero-elision readings (an absent cx=/ccx=/in= is 0 on a --for lens row), one per dialect. Each +// is EXEMPT from the sig ledger (forZeroNoteBytes): spelling the reading must never cost the answer a row, so the rows +// kept are exactly the rows kept before the elision. Each is checked verbatim inside the clause that carries it. +inline constexpr std::string_view kForZeroAbsentCompactNote = " (absent cx/ccx/in = 0)"; +inline constexpr std::string_view kForZeroAbsentFullNote = ", each absent when 0;"; +inline constexpr std::string_view kForZeroAbsentMcpNote = "; an absent cx/ccx/in is 0"; +// The bytes of those readings a header carries — what its sig ledger leaves uncharged. +inline std::size_t forZeroNoteBytes( std::string_view header ) noexcept +{ + std::size_t n = 0; + for( const std::string_view note : { kForZeroAbsentCompactNote, kForZeroAbsentFullNote, kForZeroAbsentMcpNote } ) + { + n += header.find( note ) != std::string_view::npos ? note.size() : 0u; + } + return n; +} inline constexpr std::string_view kMcpForBuildingBlocksLegend = ": reusable building blocks (cx=complexity, in=reuse-count; an absent cx/ccx/in is 0) — prefer composing/reusing these over reimplementing"; +static_assert( kMcpForBuildingBlocksLegend.find( kForZeroAbsentMcpNote ) != std::string_view::npos, "the MCP for zero reading is spelled once" ); inline constexpr std::string_view kMcpForBundleSigsLegend = "; bundle=sigs: signatures only in this bundle, no inline bodies — fetch a symbol's full body with the fetch_body verb"; inline constexpr std::string_view kMcpForLensColumnsLegend = diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 3474134bb..3a8723f8b 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2196,6 +2196,7 @@ inline std::optional forTaskText( const std::string& root, const st const std::size_t mcpIdRouteExemptBytes = mcpIdRouteParts.bytes(); const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes + - rw::forZeroNoteBytes( headerStr ) // lean-answers: the zero reading never costs a row + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" diff --git a/src/verbs_for.h b/src/verbs_for.h index 6ea58ea89..5b6aabd74 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -744,6 +744,9 @@ inline constexpr std::string_view kForCompactLegendRows = inline constexpr std::string_view kForCompactLegendRowsNoScope = "d: cx= ccx= complexity, in= callers (absent cx/ccx/in = 0), churn= amp= change, clone= tested= 1; " "total= shown= capped=1 if cut"; +static_assert( kForCompactLegendRows.find( rw::kForZeroAbsentCompactNote ) != std::string_view::npos + && kForCompactLegendRowsNoScope.find( rw::kForZeroAbsentCompactNote ) != std::string_view::npos, + "the compact zero reading is the exempt note, verbatim (graphlegend.h forZeroNoteBytes)" ); // L1 fix round (2026-09-19, rv-r1-L1 HIGH-1): task= (the root's echo of the query), next= (the follow-up on a row), pure= // (a row flag) and the rows rode every default answer with no reading in either dialect; // legendcoveragecheck's default rows now fail on that. Charged like the rows clause; runForLens caps the compact charge at @@ -1447,7 +1450,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For constexpr std::size_t kJsonSurfaceCountsBytes = 96; const std::size_t bundleBudget = in.tokenBudget > 0 ? budgetBytesForTokens( in.tokenBudget ) : kForPayloadBudgetBytes; - const std::size_t fixedBytes = header.size() + kJsonEnvelopeBytes + kJsonSurfaceCountsBytes + const std::size_t fixedBytes = header.size() - std::min( header.size(), rw::forZeroNoteBytes( header ) ) // lean-answers: exempt + + kJsonEnvelopeBytes + kJsonSurfaceCountsBytes + ( in.noteIndex ? kJsonNotesStanzaBytes : 0 ); const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; @@ -2899,6 +2903,7 @@ std::optional runForLens( const MainDispatch& d ) DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); } std::size_t chargedHeaderBytes = exemptBytes > headerStr.size() ? headerStr.size() : headerStr.size() - exemptBytes; + chargedHeaderBytes -= std::min( chargedHeaderBytes, rw::forZeroNoteBytes( headerStr ) ); // lean-answers: the zero reading never costs a row // L1 fix round (rv-r1-L1 HIGH-1 / MED-5): THE DEFAULT NEVER CHARGES ITS SIGNATURES MORE HEADER THAN --legend=full WOULD. // The compact header now defines task=/next=/pure=/ (kForCompactLegendFacts), and charging those bytes cost the // default a signature row --legend=full carried (compactlegendcheck (P1): 19 vs 20 rows at --token-budget=3000); making @@ -2910,11 +2915,13 @@ std::optional runForLens( const MainDispatch& d ) { ForLensHeaderParts fullParts = headerParts; fullParts.compactLegend = false; - const std::size_t fullHeaderBytes = forLensHeaderText( fullParts, /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ).size(); + const std::string fullHeader = forLensHeaderText( fullParts, /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + const std::size_t fullHeaderBytes = fullHeader.size(); const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes(); - const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; + std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; + fullCharged -= std::min( fullCharged, rw::forZeroNoteBytes( fullHeader ) ); // lean-answers: exempt, as above chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } const std::size_t fixedBytes = chargedHeaderBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" From 4f149b8c25d44719d8060ea09002129845eec60c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:14:31 -0400 Subject: [PATCH 037/229] legend: shorter whereis/map readings; mcp whereis listing twin arms; manifest re-anchor The compact readings of listing=, head_date=, refs count= and are tightened (the map purpose line is unchanged). mcptwinclaimscheck (A) reads the whole list on both surfaces; (A-default) is its twin on the default listing. mcpmanifestcheck re-anchors 46,750 -> 46,850 for the one declared argument (whereis listing, +96 B schema). Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 12 ++++++------ src/mcprefusal.h | 2 +- test/mcpmanifestcheck.sh | 6 +++++- test/mcptwinclaimscheck.sh | 18 ++++++++++++++++-- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 764019138..eb99d9a65 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -60,7 +60,7 @@ struct CompactLegendSpec inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { // ── the ranked-map family (root ) — hinted by the map-shaping flag that rode along ── - { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees (or ); the header comment is data" }, + { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data" }, { "r", "map-diff", "the ranked map anchored at at=: what the diff touched, the map's row vocabulary" }, { "r", "metrics", "the ranked map with per-symbol metrics: in/out, cx/ccx, loc, params, nest, humps/deep, locals, cbo, amp, tested, ev" }, { "r", "around", "call neighbourhood of of=: depth= hops, fanout= kept per hop; absent rows lie outside that boundary" }, @@ -567,7 +567,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // ELEMENT-qualified on . compactlegendcheck (D36)/(D37). { "r", ": rank N in this ranking, rows in r= order", true, "d" }, // lean-answers lane (serialize.h, the map's callee rows): present only where a row joined its callees. - { "calls", ": its resolved callees comma-joined in order, the rows of a row none of whose calls carries an attribute", true, "s" }, + { "calls", ": its callees, comma-joined", true, "s" }, { "ccx", ": cyclomatic/cognitive complexity", true, "d" }, { "in", ": N callers in the index (absent: not measured)", true, "d" }, { "amp", ": direct callers + files sharing a commit with its file (absent at 0)", true, "d" }, @@ -932,9 +932,9 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "worktree", "worktree=read|partial|unlisted: the checkout differs from HEAD (at= +dirty); each changed path is read from disk as ref=\"worktree\" rows replacing HEAD's, and on-head=/hits=/head_labels= count them; on-head= then reads the checkout, not HEAD's commit; partial = some changed path unreadable or a directory (nested repo, submodule), its HEAD rows may be stale; unlisted = git could not list the changes; either withholds complete=", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "hits", "more hits=N: rows after this page; page on with offset=next_offset", true, "more", MapHeaderRead::No, {}, "whereis" }, // lean-answers lane (crossref.h WhereisListing / the tip-date hoist): each rides only the answer that carries it. - { "listing", "listing=defs: kind=def rows only, the kind=ref rows counted by refs count= (next= lists them); refs: kind=ref rows only; the window counts listed rows, hits= all", false, "whereis", MapHeaderRead::No, {}, "whereis" }, - { "head_date", "head_date=: a hit without tip= date= is on HEAD's commit: tip= is at=, date= is this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, - { "count", "refs count=N: the kind=ref rows not listed", true, "refs", MapHeaderRead::No, {}, "whereis" }, + { "listing", "listing=defs|refs: only those kind= rows listed (defs: refs counted in refs count=, its next= lists them); the window counts listed rows", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "head_date", "head_date=: a hit without tip= date= has tip= at=, date= this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "count", "refs count=N: kind=ref rows not listed", true, "refs", MapHeaderRead::No, {}, "whereis" }, // the GREY ZONE of the same sweep: attributes the compact prose named in passing ("in/out, cx/ccx", " groups") but never // DEFINED as name= — legendcoveragecheck's default rows hold the definitional predicate, so each gets its reading here. // affected: src/verbs_change.h runAffected @@ -991,7 +991,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // doc-drift: src/docdrift.h { "filter", "filter=: the path filter this run was narrowed to; docs outside it were not checked", false, "doc-drift", MapHeaderRead::No, {}, "doc-drift" }, // whereis: src/crossref.h (the exhaustiveness claim) - { "complete", "complete=1: the scan read every ref AND this page lists every hit of its listing (absent: one of the two is a floor)", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "complete", "complete=1: the scan read every ref AND this page lists every hit (of its listing=) (absent: one of the two is a floor)", false, "whereis", MapHeaderRead::No, {}, "whereis" }, // plan-lint: src/planlint.h { "file", "file=/dialect=: the plan read and whether the PLAN dialect was detected (dialect=0: nothing to lint)", false, "plan-lint", MapHeaderRead::No, {}, "plan-lint" }, // also defines dialect= { "cards", "cards=/ledger=: card rows found / whether the doc carries a ledger (ledger_line= names its line)", false, "plan-lint", MapHeaderRead::No, {}, "plan-lint" }, // also defines ledger= diff --git a/src/mcprefusal.h b/src/mcprefusal.h index aba572328..786a7223b 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -338,7 +338,7 @@ inline constexpr McpValueSpec kMcpValueFields[] = { { "var", "a STRING variable name inside the resolved definition (omit it to list the sliceable locals)", "var=\"out\"" }, { "flow", "a STRING flow direction: back, fwd or both (omit it for the flat per-line rows)", "flow=\"back\"" }, // lean-answers lane: whereis's CLOSED listing set, the CLI --whereis-listing= values verbatim. - { "listing", "a STRING: defs (default), refs or all", "listing=\"all\"" }, + { "listing", "a STRING: defs (default; refs counted), refs or all", "listing=\"all\"" }, // lane/t10-mcp-coverage: rank_by's own CLOSED value set, the CLI --rank-by= spelling verbatim (cli.h's // parse arm). churn/churn-decay are valid CLI values that this row still names — the closed-set check // must accept them so the refusal can then say WHY they are unsupported HERE (a named gap, not an diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 24d6e1591..c22ab9ca7 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -284,7 +284,11 @@ tools = json.loads( line )[ "result" ][ "tools" ] # description, so each says "value uses (tables, args), not a proven call" (+70 B and +71 B, 141 B in all, no schema # byte moved, no tool added). Headroom after this line: 28 B (the final review tightened a first 46,800 anchor to the # #214-sized margin: 78 B of unattributed headroom was wider than any re-anchor here has taken). -CEILING = 46750 +# RE-ANCHORED 2026-10-04 (lane/lean-answers-068): 46,750 -> 46,850, measured 46,828 (from 46,732, 33 tools). ONE DECLARED +# ARGUMENT, no prose: whereis takes `listing` (defs|refs|all, the CLI --whereis-listing= twin), whose default (defs) lists the +# definitions and counts the references. Attributed: whereis schema 714 -> 810 B (+96 B, the field and its obliged +# description), whereis description unchanged at 701 B, every other tool unchanged. Headroom after this line: 22 B. +CEILING = 46850 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) diff --git a/test/mcptwinclaimscheck.sh b/test/mcptwinclaimscheck.sh index 3b2593d84..013ad81e6 100755 --- a/test/mcptwinclaimscheck.sh +++ b/test/mcptwinclaimscheck.sh @@ -100,8 +100,10 @@ def headRows( xml ): return rows # ── (A) whereis: the call site on use.c:4 is a reference on BOTH surfaces, labelled by the same mechanism ── -cx, _, _ = cli( [ "--whereis=helper" ] ) -mx, merr = mcp( "whereis", { "path": FIX, "symbol": "helper" } ) +# lean-answers lane: the use.c:4 row is a kind="ref" row, which the DEFAULT listing counts rather than prints, so (A) reads +# the whole list on both surfaces (--whereis-listing=all / listing:"all"); (A-default) below is its twin on the default. +cx, _, _ = cli( [ "--whereis=helper", "--whereis-listing=all" ] ) +mx, merr = mcp( "whereis", { "path": FIX, "symbol": "helper", "listing": "all" } ) if mx is None: no( "(A) MCP whereis refused: %s" % merr ) else: @@ -112,6 +114,18 @@ else: hl = lambda x: ( re.search( r'head_labels="([^"]*)"', x ) or [ None, "" ] )[ 1 ] ( ok if hl( cx ) == hl( mx ) == "index" else no )( "(A) head_labels= is index on both surfaces (cli=%s mcp=%s)" % ( hl( cx ), hl( mx ) ) ) +# ── (A-default) the twin on the default listing: the same def rows on both surfaces, the ref rows counted identically ── +cd, _, _ = cli( [ "--whereis=helper" ] ) +md, mderr = mcp( "whereis", { "path": FIX, "symbol": "helper" } ) +if md is None: + no( "(A-default) MCP whereis refused: %s" % mderr ) +else: + crd, mrd = headRows( cd ), headRows( md ) + rc = lambda x: ( re.search( r'), not printed, on both surfaces (mcp count=%s)" % ( rc( cd ), rc( md ) ) ) + # ── (B) uses: a refusal claims only what was checked ── _, cerr, crc = cli( [ "--uses=valueToken" ] ) ( ok if crc != 0 and "no indexed definition" in cerr else no )( "(B) CLI --uses=valueToken refuses on no indexed definition (fixture sanity; rc=%d)" % crc ) From df5822a695f313b3e652c95a886f42c650f32815 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:20:27 -0400 Subject: [PATCH 038/229] =?UTF-8?q?test(countfloorcheck):=20F2=20=E2=80=94?= =?UTF-8?q?=20a=20declined=20call=20from=20a=20caller=20that=20also=20bind?= =?UTF-8?q?s=20the=20name=20elsewhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unbound-call scan reads a caller that bound ANY same-named definition as bound, so the first declined-call arm stayed green with the declined evidence removed (mutation M2 survived). F2 puts a bound x.head() and a declined q.head() in one caller: only the declined evidence can floor Selection.head. Co-Authored-By: Claude Opus 5.5 --- test/countfloorcheck.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index 5a352d74d..a6c7e702b 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -28,6 +28,7 @@ # (the clue) lists the reading file. # F grep on a method whose only call the resolver DECLINED (two same-named defs in other dirs, an # untyped receiver): callers="0" callers_floor="1". +# F2 the same, from a caller that also BINDS a same-named call elsewhere — the declined evidence alone floors it. # G grep on a TS method whose only call is spelled like it and bound NOWHERE (unresolved, no decline). # H grep on a C function stored in a table and never called directly (a value use). # I (negative) grep on a C function with only direct, bound calls: callers="2", no marker. @@ -119,6 +120,12 @@ cat > "$W/py/pkg/use.py" <<'EOF' def use_param(q): return q.tail() EOF +# F2: one caller binds a same-named call elsewhere (x = LRU(); x.head()) and has a second one DECLINED (q.head()): +# the unbound scan treats that caller as bound, so only the declined evidence can floor Selection.head +mkdir -p "$W/py2/pkg/css" "$W/py2/pkg/other" +printf 'class Selection:\n def head(self):\n raise ValueError("first item marker")\n' > "$W/py2/pkg/css/query.py" +printf 'class LRU:\n def head(self):\n return "lru head"\n' > "$W/py2/pkg/other/cache.py" +printf 'def mixed(q):\n x = LRU()\n x.head()\n return q.head()\n' > "$W/py2/pkg/mix.py" cat > "$W/ts/router.ts" <<'EOF' export interface Router { add(m: string): void; match(p: string): T | null } EOF @@ -208,6 +215,13 @@ CALLERS_F="$( root_tag "$( run py --callers=Selection.tail )" callers )" [ "$( attr "$ROW_F" callers )" = 0 ] && [ "$( attr "$ROW_F" callers_floor )" = 1 ] && [ "$( attr "$ROW_F" floor_next )" = "--uses=tail" ] \ && ok "(F) callers=\"0\" callers_floor=\"1\" floor_next=\"--uses=tail\"" || no "(F) declined row: $ROW_F" +echo "=== F2: a declined call from a caller that ALSO binds the same name elsewhere — only the decline can floor it ===" +CALLERS_F2="$( root_tag "$( run py2 --callers=Selection.head )" callers )" +CALLERS_F2B="$( root_tag "$( run py2 --callers=LRU.head )" callers )" +[ -n "$( attr "$CALLERS_F2" declined_calls )" ] && [ "$( attr "$CALLERS_F2B" count )" = 1 ] || no "(F2) premise: want q.head() declined and x.head() bound: $CALLERS_F2 / $CALLERS_F2B" +ROW_F2="$( enc_row "$( run py2 --grep='first item marker' )" 'Selection::head' )" +if [ "$( attr "$ROW_F2" callers )" = 0 ] && [ "$( attr "$ROW_F2" callers_floor )" = 1 ]; then ok "(F2) declined-only evidence: callers_floor=\"1\""; else no "(F2) row: $ROW_F2"; fi + # ── G: an unbound call, no decline ─────────────────────────────────────────────────────────────────────── echo "=== G: grep on a TS method whose one call bound nowhere ===" ROW_G="$( enc_row "$( run ts --grep='lonely marker' )" lonelyMatch )" From e2a4977f4cbc6e93297cf331218fbff092f18b3e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:28:35 -0400 Subject: [PATCH 039/229] legend pins for the new definitions; docs and skill text for the whereis listing compactlegendcheck: the per-verb pins move by the measured bytes of the new readings (map family +49 B for calls=, for +23 B, whereis +276 B), and the for.xml/map.xml full-dialect pins are re-anchored by hand after a mechanical check (restore the zeros and rows, drop the two legend clauses: the old pins byte for byte, at=/est_tokens= masked). The refs count= reading folds into listing='s. docs/COMMANDS.md regenerated; the change-check skill says the default lists definitions. Co-Authored-By: Claude Opus 5.5 --- docs/COMMANDS.md | 27 ++++++++++++++++--- skills/ripwire-change-check/SKILL.md | 3 ++- src/compactlegend.h | 5 ++-- test/compactlegendcheck.sh | 33 ++++++++++++++++------- test/compactlegendfix/pre_l1_full/for.xml | 2 +- test/compactlegendfix/pre_l1_full/map.xml | 2 +- 6 files changed, 53 insertions(+), 19 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..d222366af 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -44,13 +44,13 @@ Two limits apply to nearly everything here and are not repeated in every section **zoom the detail ladder** — [`--detail`](#--detailn) · [`--pack-signatures`](#--pack-signatures) · [`--outline`](#--outlineab) · [`--expand`](#--expandab) · [`--compress`](#--compress) · [`--pack-top-n`](#--pack-top-nn) · [`--no-redact`](#--no-redact) -**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) **self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) **security — scan skill files for injection / exfiltration patterns (exit 2 = CRITICAL, 1 = WARN,** — [`--scan-skill`](#--scan-skillfile) · [`--scan-skills`](#--scan-skillsdir) · [`--force`](#--force) -**knobs / modes** — [`--rank-by`](#--rank-bypagerankauthorityhubrrfchurnchurn-decay) · [`--in`](#--indir) · [`--format`](#--formatxmlcolumnarrows) · [`--format`](#--formatcandidates) · [`--legend`](#--legendfullcompact) · [`--legend-dict`](#--legend-dictroster) · [`--json`](#--json) · [`--limit`](#--limitn---offsetm) · [`--exclude`](#--excludesubstr) · [`--map-diff`](#--map-diff) · [`--cache`](#--cachepath) · [`--index-out`](#--index-outbase) · [`--no-cache`](#--no-cache) · [`--no-ignore`](#--no-ignore) · [`--max-file-size`](#--max-file-sizenkmg) · [`--max-memory`](#--max-memorynkmg) · [`--refetch`](#--refetch) · [`--scip`](#--scipindexscip) · [`--pin-census`](#--pin-censusfile) · [`--mcp`](#--mcp) · [`--mcp-tools`](#--mcp-toolslist) · [`--lsp`](#--lsp) · [`--listen`](#--listenhostport) · [`--mcp-token`](#--mcp-tokent) · [`--allow-remote-edits`](#--allow-remote-edits) · [`--eval-stray`](#--eval-strayfile) · [`--eval`](#--eval) · [`--eval-retrieval`](#--eval-retrieval) · [`--eval-mined`](#--eval-minedfile) · [`--eval-skills`](#--eval-skillsfile) · [`-h`](#-h---help) · [`-v`](#-v---version) +**knobs / modes** — [`--rank-by`](#--rank-bypagerankauthorityhubrrfchurnchurn-decay) · [`--in`](#--indir) · [`--format`](#--formatxmlcolumnarrows) · [`--format`](#--formatcandidates) · [`--legend`](#--legendfullcompact) · [`--legend-dict`](#--legend-dictroster) · [`--json`](#--json) · [`--limit`](#--limitn---offsetm) · [`--exclude`](#--excludesubstr) · [`--map-diff`](#--map-diff) · [`--cache`](#--cachepath) · [`--index-out`](#--index-outbase) · [`--no-cache`](#--no-cache) · [`--no-ignore`](#--no-ignore) · [`--max-file-size`](#--max-file-sizenkmg) · [`--max-memory`](#--max-memorynkmg) · [`--refetch`](#--refetch) · [`--scip`](#--scipindexscip) · [`--pin-census`](#--pin-censusfile) · [`--mcp`](#--mcp) · [`--mcp-legend`](#--mcp-legendwhen) · [`--mcp-tools`](#--mcp-toolslist) · [`--lsp`](#--lsp) · [`--listen`](#--listenhostport) · [`--mcp-token`](#--mcp-tokent) · [`--allow-remote-edits`](#--allow-remote-edits) · [`--eval-stray`](#--eval-strayfile) · [`--eval`](#--eval) · [`--eval-retrieval`](#--eval-retrieval) · [`--eval-mined`](#--eval-minedfile) · [`--eval-skills`](#--eval-skillsfile) · [`-h`](#-h---help) · [`-v`](#-v---version) --- @@ -3546,7 +3546,7 @@ $ ./build/ripwire . --stray-content=main --abi **Answers:** find which branch's tree defines or mentions SYM, HEAD first which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch. -Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. +Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed; the kind="ref" rows are counted in one element (see --whereis-listing). **Try it** @@ -3564,7 +3564,7 @@ $ ./build/ripwire . --whereis=rankGraphTeleport ... [24 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--with-history`, `--limit` +**Shaped by:** `--whereis-listing`, `--with-history`, `--limit` **Caveats (stated by the binary):** @@ -3572,6 +3572,19 @@ $ ./build/ripwire . --whereis=rankGraphTeleport - head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). - refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. +### `--whereis-listing=WHICH` + +**Answers:** with --whereis: which rows to list, defs (the default), refs or all defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). + +With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. + +**Shaped by:** `--whereis` + +**Caveats (stated by the binary):** + +- shown=/capped= and --limit/--offset window the LISTED rows; +- Refused without --whereis, and on an unknown value. + ### `--flags[=SUBSTR]` **Answers:** the dark-content dashboard: what is built but switched OFF in this repo the dark-content dashboard: what is BUILT but OFF in this repo. @@ -4787,6 +4800,12 @@ $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jso - A root that is $HOME itself (a git repository or not), a filesystem or drive root, or a system tree (/usr, /etc, /System, %WINDIR% ...) is refused with "no project root: is a home/system directory; - Each tool call over the --max-memory limit is refused by name; +### `--mcp-legend=WHEN` + +**Answers:** the stdio MCP server's legend posture: session (the default) or inline session: the first answer of a session carries its legend inline; + +later answers list rows first, carry each definition only the first time the session meets it, and end with (the first of them also carries the core). inline: every answer keeps its legend until the client reads ripwire://legend-dict. legend:"compact"/"full" on a call keeps that answer inline either way. Stdio only. + ### `--mcp-tools=LIST` **Answers:** list only these MCP tools (names and/or the core/full profiles, default full). diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index bc2a23fb1..789867b71 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -147,7 +147,8 @@ emits a flat `` top-K — identity + score + signatu a symbol, HEAD first; `on-head="0"` alongside branch hits is content that exists ONLY on a branch. Each distinct blob is read once (git is content-addressed), so 30 branches cost about one tree. `kind="def"` on a branch row is a lexical heuristic — branch blobs are raw text, never ingested; for HEAD's parsed - answer use `--expand`/`--callers`. + answer use `--expand`/`--callers`. By default only the `kind="def"` rows are listed and the mentions are + counted in one `` row (its `next=` lists them); `--whereis-listing=all` lists every row. A tree scan only finds what some ref *still carries*, so `hits="0"` cannot by itself tell a name this repo never had from one it deleted. Add `--with-history` and a `` row says which: `v="never"`, or `v="removed" commit=… date=… p=…` naming the commit that took it out. One `git log` pass, memoized per diff --git a/src/compactlegend.h b/src/compactlegend.h index eb99d9a65..38fe00584 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -567,7 +567,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // ELEMENT-qualified on . compactlegendcheck (D36)/(D37). { "r", ": rank N in this ranking, rows in r= order", true, "d" }, // lean-answers lane (serialize.h, the map's callee rows): present only where a row joined its callees. - { "calls", ": its callees, comma-joined", true, "s" }, + { "calls", ": its rows, comma-joined", true, "s" }, { "ccx", ": cyclomatic/cognitive complexity", true, "d" }, { "in", ": N callers in the index (absent: not measured)", true, "d" }, { "amp", ": direct callers + files sharing a commit with its file (absent at 0)", true, "d" }, @@ -932,9 +932,8 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "worktree", "worktree=read|partial|unlisted: the checkout differs from HEAD (at= +dirty); each changed path is read from disk as ref=\"worktree\" rows replacing HEAD's, and on-head=/hits=/head_labels= count them; on-head= then reads the checkout, not HEAD's commit; partial = some changed path unreadable or a directory (nested repo, submodule), its HEAD rows may be stale; unlisted = git could not list the changes; either withholds complete=", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "hits", "more hits=N: rows after this page; page on with offset=next_offset", true, "more", MapHeaderRead::No, {}, "whereis" }, // lean-answers lane (crossref.h WhereisListing / the tip-date hoist): each rides only the answer that carries it. - { "listing", "listing=defs|refs: only those kind= rows listed (defs: refs counted in refs count=, its next= lists them); the window counts listed rows", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "listing", "listing=defs|refs: only those kind= rows listed; under defs counts the kind=ref rows and next= lists them; the window counts listed rows", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "head_date", "head_date=: a hit without tip= date= has tip= at=, date= this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, - { "count", "refs count=N: kind=ref rows not listed", true, "refs", MapHeaderRead::No, {}, "whereis" }, // the GREY ZONE of the same sweep: attributes the compact prose named in passing ("in/out, cx/ccx", " groups") but never // DEFINED as name= — legendcoveragecheck's default rows hold the definitional predicate, so each gets its reading here. // affected: src/verbs_change.h runAffected diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 9a103f962..5c628af18 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -288,6 +288,12 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # outside-import/use,super-past-the-tree;...)" (was "(builtin/stdlib-name-...,external-import,...)"), +106 B — and the # est_tokens= that prices them, 858 -> 901 (header and root). Checked: the new output differs from the pin by exactly # those two clauses and that figure; the six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-04 (lane/lean-answers-068), for.xml and map.xml: two lossless row spellings and the legend +# sentences that define them. for.xml: a --for lens row omits a zero cx=/ccx=/in= and the v1 legend says "each absent when +# 0;"; map.xml: a row whose callees are all bare carries them as calls="a,b" and the v1 header legend defines calls=. +# Checked mechanically: restoring the zeros (on every r= row) and the rows, and removing those two legend clauses, +# gives the previous pins byte for byte with at= and est_tokens= masked (est_tokens= prices the shorter bytes). The five +# other pins are unchanged. PIN_DIR="$ROOT/test/compactlegendfix/pre_l1_full" # the one normalisation, in python on BOTH sides so no sed dialect decides it (BSD sed appends a final newline, GNU # sed does not): at="…" masked, trailing newlines dropped. @@ -510,12 +516,21 @@ probeFor() # its definition as two contracts — so the schema had no XML answer to pin. It now answers about the definition. # the pins follow the definitions, measured + 10 rounded up to 10. # schema pin measured +# RE-PINNED 2026-10-04 (lane/lean-answers-068), three new definitions, each measured on this gate's fixture against the +# base binary (255dc199) — no prose was added to any reading that already rode: +# the map family (map 892->935 at --top-k=3, map-diff 885->939, metrics 1021->1066, query 707->761, around 760->814, +# pack-top-n 745->799, pack-signatures 759->813): the present-only reading (+49 B with its separator) — a +# map row whose callees are all bare now carries them as one comma-joined attribute (~11 B a callee less in the rows). +# ripwire.for/v1 738->761: " (absent cx/ccx/in = 0)" (+23 B) — the --for lens omits a zero cx=/ccx=/in=. +# ripwire.whereis/v1 611->887: the default listing's readings — listing=, the element (and the +# generic next= reading it pulls in), head_date= (the tip/date hoist) and complete='s "(of its listing=)" (+276 B). The +# answer they ride is the lean one: its rows shrink from every reference to the definitions alone. PIN_TABLE=' ripwire.edit-check/v1 610 595 -ripwire.map/v1 910 892 -ripwire.map-diff/v1 900 885 -ripwire.pack-signatures/v1 770 759 -ripwire.metrics/v1 1040 1021 +ripwire.map/v1 950 935 +ripwire.map-diff/v1 955 939 +ripwire.pack-signatures/v1 830 813 +ripwire.metrics/v1 1080 1066 ripwire.deps/v1 780 764 ripwire.hotspots/v1 530 518 ripwire.clones/v1 810 799 @@ -556,12 +571,12 @@ ripwire.mentions/v1 260 243 ripwire.affected/v1 840 826 ripwire.verify/v1 440 421 ripwire.help-task/v1 440 429 -ripwire.query/v1 720 707 +ripwire.query/v1 775 761 ripwire.grep/v1 1380 1368 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 -ripwire.around/v1 770 760 +ripwire.around/v1 830 814 ripwire.callers/v1 330 317 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 @@ -571,14 +586,14 @@ ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 ripwire.merge-scout/v1 570 558 -ripwire.whereis/v1 630 611 +ripwire.whereis/v1 900 887 ripwire.community/v1 730 719 ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 -ripwire.pack-top-n/v1 760 745 +ripwire.pack-top-n/v1 815 799 ripwire.expand/v1 280 265 ripwire.expand-file/v1 360 342 -ripwire.for/v1 750 738 +ripwire.for/v1 775 761 ' pinFor() { diff --git a/test/compactlegendfix/pre_l1_full/for.xml b/test/compactlegendfix/pre_l1_full/for.xml index 60a453fec..1cb72e5db 100644 --- a/test/compactlegendfix/pre_l1_full/for.xml +++ b/test/compactlegendfix/pre_l1_full/for.xml @@ -1 +1 @@ -double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file +double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file diff --git a/test/compactlegendfix/pre_l1_full/map.xml b/test/compactlegendfix/pre_l1_full/map.xml index 01d2de816..81b630175 100644 --- a/test/compactlegendfix/pre_l1_full/map.xml +++ b/test/compactlegendfix/pre_l1_full/map.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 783fbe2b59104c917e9b88d898f88302bd83f9a8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:37:04 -0400 Subject: [PATCH 040/229] test(gate): mcpreloadasancheck waits for the sanitizer report to finish before it stops the session The abort and the symbolizer can take a while on a loaded host; the first version stopped the server three seconds after the report appeared and printed only its first lines. It now waits for the SUMMARY line (two minutes at most) and prints the whole report. Co-Authored-By: Claude Sonnet 5.5 --- test/mcpreloadasancheck.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index db0daa043..9c1a416a6 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -148,7 +148,7 @@ def row(kind, text): def fail(text): fails.append(text) - row("FAIL", text[:700]) + row("FAIL", text[:3500]) def check(cond, label, detail=""): @@ -822,7 +822,13 @@ def main(): row("PASS" if not fails else "NOTE", "cycle %d/%d done (checks so far %d, failures %d)" % (k, CYCLES, checks, len(fails))) except Dead as e: fail("the server died: %s" % e) - time.sleep(3) # let a report finish landing in the log directory + # let the report finish landing in the log directory (the symbolizer can take a while on a loaded host): wait for its + # SUMMARY line, or for the process to be gone, for at most two minutes + t0 = time.time() + while time.time() - t0 < 120 and server.p.poll() is None: + if any("SUMMARY:" in open(os.path.join(SAN, f), errors="replace").read() for f in os.listdir(SAN)): + break + time.sleep(1) server.kill() except Exception as e: # a driver bug must be loud, never a pass import traceback @@ -830,7 +836,7 @@ def main(): rc = server.close() if server.p.poll() is None else server.p.returncode found = sanitizer_findings() check(not found, "no sanitizer report in the log directory or on stderr (halt_on_error, abort_on_error, log_path)", - " | ".join("%s: %s" % (n, t[:1800]) for n, t in found[:1])) + " | ".join("%s: %s" % (n, t[:3000]) for n, t in found[:1])) check(rc == 0, "the session ended with exit status 0 after its input closed", "rc=%s" % rc) row("NOTE", "time: CLI %.0fs in %d runs, MCP %.0fs in %d requests" % (T["cli"], T["ncli"], T["mcp"], T["nmcp"])) print("SUMMARY checks=%d fails=%d cycles=%d stamp=%d content=%d edit=%d switch=%d release=%d cold=%d changed=%d missed=%d rc=%s san=%d" % ( From d6acc955e5f6c2c2d9df4ba38378a406e6d9e467 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:52:56 -0400 Subject: [PATCH 041/229] resolve: the two sides of a JS/TS class lookup; a fallback hook only after a miss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A call on a JS/TS class object (`Gauge.read()`, or `this` inside a static member) resolved to every member of that name, the instance one included, and an instance call (`new Gauge().read()`, a typed parameter) to the static one too. Ingest records a StaticMember binding inside each `static` member's body (kParserVer 146); ReceiverEvidence::narrow keeps only the receiver's side and the ladder's class-name / this / super proofs read the same side, so a wrong-side definition is never resolved — a class call naming an instance-only member is hedged. Gate (S): JS and TS sides, `this` in a static member, a wrong-side class call (red on the previous binary: 6 arms); Kotlin companion vs instance and Python __getattr__ (a defined member resolves, a missed name is never proven onto another class) pinned clean — both already held. qschemetrip re-pinned for kParserVer 146 (logged). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ++- src/graph.h | 8 ++-- src/ingest_binds.h | 18 +++++++- src/ingest_cache.h | 4 +- src/model.h | 12 +++-- src/quality.h | 4 +- src/receiverevidence.h | 47 +++++++++++++++++++- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 2 +- test/receiverevidencecheck.sh | 26 +++++++++++ test/receiverevidencefix/README.md | 4 ++ test/receiverevidencefix/js/lib/sides.js | 13 ++++++ test/receiverevidencefix/kt/src/app/Sides.kt | 16 +++++++ test/receiverevidencefix/py/src/tui/ghost.py | 20 +++++++++ test/receiverevidencefix/ts/src/sides.ts | 7 +++ 15 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 test/receiverevidencefix/js/lib/sides.js create mode 100644 test/receiverevidencefix/kt/src/app/Sides.kt create mode 100644 test/receiverevidencefix/py/src/tui/ghost.py create mode 100644 test/receiverevidencefix/ts/src/sides.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6009b5415..353e6db22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. (a TS/Python/Go annotation, a Go method receiver, a JS `new Foo()`, Python `Foo()` or Go `Foo{}` initializer, an import alias of the class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields whose classes are stated (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields); - Python `feed = parser.feed`. Ruby keeps only the language-neutral proof below (its own method lookup is separate work). + Python `feed = parser.feed`. A JS/TS call on the class object reaches its `static` members only, and a call on an + instance never one. Ruby keeps only the language-neutral proof below (its own method lookup is separate work). - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an implicit receiver's class and bases, a free function in scope, the module a receiver alias names). With none proven, every same-file and same-directory candidate is listed — a lone global one too, because one candidate is @@ -42,7 +43,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. are listed at edge weight zero, so a guess never moves PageRank. - Measured on the round-1 comparison-table calls for this tool: see the lane report; the gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses - for every evidence rule). Ingest records the receiver chain (`kParserVer` 145, `kCacheVersion` 29), so a cache + for every evidence rule). Ingest records the receiver chain (`kParserVer` 146, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. diff --git a/src/graph.h b/src/graph.h index a3fe9c708..e1b4d906a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4107,14 +4107,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); if( !callerCls.empty() ) { - return !owner.empty() && recvEv.inCone( callerCls, owner ); + return !owner.empty() && recvEv.inCone( callerCls, owner ) && recvEv.onSide( ref, c, recvEv.callerIsStatic( ref.fromSymbol ) ); } return owner.empty() && cs.fileId == ref.fileId && cs.kind == SymKind::Method; // an object literal's own member } if( isSuperRoot( ch.root ) ) { const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); - return !callerCls.empty() && !owner.empty() && owner != callerCls && recvEv.inCone( callerCls, owner ); + return !callerCls.empty() && !owner.empty() && owner != callerCls && recvEv.inCone( callerCls, owner ) + && recvEv.onSide( ref, c, recvEv.callerIsStatic( ref.fromSymbol ) ); } if( recvEv.isLocalName( ref.fromSymbol, ch.root ) || externalVeto.hasLocal( ref, ch.root ) ) { @@ -4133,7 +4134,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } } const std::string_view cls = recvEv.unalias( ref.fileId, ch.root ); - return !owner.empty() && recvEv.namesClass( ref, ch.root, cls ) && recvEv.inCone( cls, owner ); // a class-name receiver: the class or a base + return !owner.empty() && recvEv.namesClass( ref, ch.root, cls ) && recvEv.inCone( cls, owner ) // a class-name receiver: the class or a base, + && recvEv.onSide( ref, c, /*classSide=*/true ); // its CLASS side } if( implicitReceiverLang( ref.lang ) ) { diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 73cb7d0ed..b3ddb8d84 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -2612,7 +2612,8 @@ inline std::string_view annotatedClass( TSNode node, std::string_view src ) inline void pushEvidenceBind( BindCtx& cx, LocalBindKind kind, std::string_view var, std::string_view type, std::string_view imported, std::uint32_t at, bool flag ) { - EXPECTS( kind == LocalBindKind::RecvType || kind == LocalBindKind::MemberType || kind == LocalBindKind::MethodAlias || kind == LocalBindKind::NameAlias, + EXPECTS( kind == LocalBindKind::RecvType || kind == LocalBindKind::MemberType || kind == LocalBindKind::MethodAlias || kind == LocalBindKind::NameAlias + || kind == LocalBindKind::StaticMember, "only the receiver-evidence kinds ride this emitter" ); if( type.empty() || ( var.empty() && !( kind == LocalBindKind::RecvType && flag ) ) ) { @@ -2738,6 +2739,21 @@ inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); } } + else if( kindIs( t, "method_definition" ) ) + { + // `static m () {}`: the member's side of the lookup, recorded inside its body so it attributes to the member + const TSNode name = fieldChild( n, NodeField::Name ); + const TSNode body = fieldChild( n, NodeField::Body ); + bool isStatic = false; + for( std::uint32_t i = 0, k = ts_node_child_count( n ); i < k && !isStatic; ++i ) + { + isStatic = kindIs( ts_node_type( ts_node_child( n, i ) ), "static" ); + } + if( isStatic && !ts_node_is_null( name ) && !ts_node_is_null( body ) ) + { + pushEvidenceBind( cx, LocalBindKind::StaticMember, nodeTextOf( name, src ), "static", {}, ts_node_start_byte( body ), false ); + } + } else if( lang == Lang::TypeScript && kindIs( t, "public_field_definition" ) ) { const TSNode name = fieldChild( n, NodeField::Name ); diff --git a/src/ingest_cache.h b/src/ingest_cache.h index ce039709b..032cd03eb 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -308,7 +308,9 @@ constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiver // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 145; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 146; // bump on any grammar/.scm/extraction change + // 146 = lane FE-B fix round: JS/TS `static` members record a StaticMember + // binding (the class side of the lookup); the lane's 145 builds lack it. // 145 = lane FE-B (receiver evidence): member calls record memberPath/memberCtor and // Java/Kotlin/C#/Swift member calls are memberCall; typed parameters/locals, field types // method aliases and aliased class imports are RecvType/MemberType/MethodAlias/NameAlias diff --git a/src/model.h b/src/model.h index 4dca37c64..d52fef2a5 100644 --- a/src/model.h +++ b/src/model.h @@ -910,7 +910,7 @@ enum class LocalBindKind : std::uint8_t // var "." is a dot import. fromSymbol kNoNode, spans {0,0}. Read only by graph.h FalseEdgeRules; // every other binding consumer filters by kind or skips file-scope records. APPENDED (cache u8). // FE-B (test/receiverevidencecheck.sh): the receiver-evidence facts graph.h ReceiverEvidence reads, and NOTHING else. - // Every other binding consumer filters by kind or skips these four by isReceiverEvidenceKind below — a field name + // Every other binding consumer filters by kind or skips these five by isReceiverEvidenceKind below — a field name // recorded here is not a local of the method that assigns it, and a type written on a parameter is not a class the // file "names" for the builtin-method gate. APPENDED (cache u8). RecvType, // a parameter or local whose class the source states: var = the name, typeName = the class's final @@ -926,13 +926,17 @@ enum class LocalBindKind : std::uint8_t // importedName = the method (Python `feed = parser.feed`). NameAlias, // a file's local spelling of an imported class: var = the local name, typeName = the name it imports // (Python `from m import Stylesheet as Sheet`; JS/TS read their JsImport records instead). + StaticMember, // a JS/TS class member declared `static`: var = the member's name, typeName "static", recorded INSIDE its + // body so it attributes to the member. A call on the CLASS reaches only these, a call on an INSTANCE + // only the others (the two sides of the lookup; #373's Ruby class-object rule, here for JS/TS). }; // The number of LocalBindKind enumerators — the bound readBind validates a cached kind byte against (see kSymKindCount). -inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::NameAlias ) + 1; -// FE-B: the four kinds only graph.h ReceiverEvidence reads (see RecvType above). +inline constexpr std::size_t kLocalBindKindCount = static_cast( LocalBindKind::StaticMember ) + 1; +// FE-B: the five kinds only graph.h ReceiverEvidence reads (see RecvType above). inline bool isReceiverEvidenceKind( LocalBindKind k ) noexcept { - return k == LocalBindKind::RecvType || k == LocalBindKind::MemberType || k == LocalBindKind::MethodAlias || k == LocalBindKind::NameAlias; + return k == LocalBindKind::RecvType || k == LocalBindKind::MemberType || k == LocalBindKind::MethodAlias || k == LocalBindKind::NameAlias + || k == LocalBindKind::StaticMember; } static_assert( enumCountIsExact(), "kLocalBindKindCount must name the LAST LocalBindKind enumerator — move it with the append" ); diff --git a/src/quality.h b/src/quality.h index a30dee4aa..4907c57fa 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,8 +2180,8 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 145; // MUST equal ingest.cpp's kParserVer (gated) - // 145 = lane FE-B (receiver evidence, see kParserVer note; above train 25's 143/144; the train renumbers) +constexpr std::uint32_t kIngestParserVerMirror = 146; // MUST equal ingest.cpp's kParserVer (gated) + // 146 = lane FE-B fix round (JS/TS static members); 145 = lane FE-B (receiver evidence, see kParserVer note; above train 25's 143/144; the train renumbers) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) // 140 = lane refval-edges (reference-as-value rows; see kParserVer note) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 6f9e7f26d..5399c4016 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -94,6 +94,7 @@ struct ReceiverEvidence const HashMap>& chaUp; // class name → its direct base names (sorted) const HashMap& classNames; // every in-repo class-like name std::vector ownerClass; // per symbol: the class that owns it, "" none + std::vector staticMember; // per symbol: 1 = a JS/TS `static` member (the CLASS side) HashMap> methodsByType; // "Class::name" → its callables HashMap memberType; // "Class#field" → field class ("" = tombstone) HashMap> embeds; // "Class" → its other bases: Go embedded classes (promotion) @@ -234,6 +235,7 @@ struct ReceiverEvidence void build() { buildOwners(); + staticMember.assign( ing.symbols.size(), 0u ); for( const Symbol& s : ing.symbols ) { // a BODIED callable only: an interface's or an abstract class's bodyless signature is the contract a call @@ -278,7 +280,14 @@ struct ReceiverEvidence const bool typedLocal = b.kind == LocalBindKind::RecvType || ( b.kind == LocalBindKind::Type && ( ing.symbols[ b.fromSymbol ].lang == Lang::TypeScript || ing.symbols[ b.fromSymbol ].lang == Lang::Python ) ); - if( typedLocal && !b.var.empty() ) + if( b.kind == LocalBindKind::StaticMember ) + { + if( ing.symbols[ from ].name == b.var ) // recorded inside the member's body: the member itself + { + staticMember[ from ] = 1; + } + } + else if( typedLocal && !b.var.empty() ) { tombstoneInsert( localType, keyOf( from, '#', b.var ), b.typeName ); } @@ -572,6 +581,22 @@ struct ReceiverEvidence return {}; } + // The two SIDES of a JS/TS class lookup (#373's Ruby class-object rule, here): a call on the CLASS object reaches its + // `static` members only, a call on an INSTANCE its other members only. True for every target in another language. + bool onSide( const Reference& r, NodeId target, bool classSide ) const noexcept + { + if( r.lang != Lang::JavaScript && r.lang != Lang::TypeScript ) + { + return true; + } + return target < staticMember.size() && ( staticMember[ target ] != 0 ) == classSide; + } + // Does `this` inside the caller name the CLASS? Only inside a static member. + bool callerIsStatic( NodeId from ) const noexcept + { + return from < staticMember.size() && staticMember[ from ] != 0; + } + // RESOLVE: the definitions this member call's typed receiver reaches, in `found`; false when the receiver proves // nothing (untyped, a type this tree does not define, or a type that defines no such member anywhere in its cone). bool narrow( const Reference& r ) const @@ -598,6 +623,9 @@ struct ReceiverEvidence { return false; } + // the side the receiver names: the class object (a class-name receiver, or `this`/`super` inside a static member), + // else an instance; a stated field below always holds an instance + bool classSide = isStatic || ( ch.ctor.empty() && ( isThisRoot( ch.root ) || isSuperRoot( ch.root ) ) && callerIsStatic( r.fromSymbol ) ); std::string_view rest = ch.path; while( !rest.empty() ) // walk the stated field classes { @@ -606,12 +634,27 @@ struct ReceiverEvidence rest = dot == std::string_view::npos ? std::string_view{} : rest.substr( dot + 1 ); type = fieldOf( type, field ); superOnly = false; + classSide = false; if( type.empty() ) { return false; } } - return methodOf( type, r.calleeName, superOnly ); + if( !methodOf( type, r.calleeName, superOnly ) ) + { + return false; + } + // a wrong-side definition is never the answer: what is left is, and nothing left proves nothing (the ladder hedges) + std::size_t kept = 0; + for( std::size_t i = 0; i < found.size(); ++i ) + { + if( onSide( r, found[ i ], classSide ) ) + { + found[ kept++ ] = found[ i ]; + } + } + found.resize( kept ); + return !found.empty(); } // Python `feed = parser.feed; … feed( data )`: the alias names a typed object's method diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 451f4b12d..9d31ed892 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -e7817db464abaea1493ac8eb822c8223b200ffa3ba49e4f32086772471d8fc2f +9e06e28ba52b7fc702b37dd455f57b23655698523fc7a5044a6343d336253dbb diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index ee35d04e4..824699ce8 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,7 +34,7 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). -# 2026-10-04, lane/fe-b-receiver-evidence: RE-DERIVED with UPDATE_GOLDEN=1 (hash e7817db464…71d8fc2f). kParserVer 141 -> 145 +# 2026-10-04, lane/fe-b-receiver-evidence: RE-DERIVED with UPDATE_GOLDEN=1 (hash 9e06e28ba5…36253dbb). kParserVer 141 -> 146 # (above train 25's 143/144; the ref record gains the receiver chain memberPath/memberCtor and the receiver-evidence # binding kinds), kCacheVersion 28 -> 29; quality.h's mirrors move with them. kQSnapCacheScheme stays 17: a snapshot's meaning is unchanged, and the # edges it reads are re-derived under the new kParserVer. The train renumbers and re-derives. diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index 290ea9535..7c4612011 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -85,6 +85,10 @@ # misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), # a free / top-level function and a Ruby top-level def; a Ruby included module is contract-only (resolved # or hedged, never a false proven row): Ruby's own lookup is separate work. +# (S) the two sides of a class lookup: a JS/TS call on the CLASS object (`Gauge.read()`, `this` in a static member) +# resolves to the static member only and an instance call never to it; a Kotlin companion member likewise; +# a class-name call naming an instance-only member is never proven. A Python `__getattr__` changes nothing +# for a member the class defines, and a name its lookup misses is never proven onto another class. # (H) propagation and parity: the name-only witness `respondWith → reply.send()` is marked in --callees, # --callers, --impact (d=1), --path, --connect, --expand , --for , the map's rows, MCP # find_symbol, find_referencing_symbols and analyze; the false witness `respond → ctx.onerror()` is absent-or-marked on every @@ -504,6 +508,28 @@ hopnotproven ts "how does reply build a Response" reply getTime 5 hopnotproven ts "how does stamped get the time" stamped getTime 5 hopnotproven py "how does gather append nodes" gather append 5 +echo "=== (S) the two SIDES of a class lookup, and a fallback hook only after a miss (#373's Ruby class-object rule) ===" +# JS: Gauge.read() is the static (line 3) and never the instance read (line 4); new Gauge().read() the reverse; `this` inside +# a static member is the class. Plain.read() names an instance-only member on the CLASS: never proven (hedged or absent). +proven js callees lib/sides.js:onClass "method read lib/sides.js:3" +notproven js callees lib/sides.js:onClass "read lib/sides.js:4" +proven js callees lib/sides.js:onInstance "method read lib/sides.js:4" +notproven js callees lib/sides.js:onInstance "read lib/sides.js:3" +proven js callees lib/sides.js:make "method read lib/sides.js:3" +notproven js callees lib/sides.js:make "read lib/sides.js:4" +notproven js callees lib/sides.js:wrongSide "read lib/sides.js:8" "read lib/sides.js:4" +# TS: the same through a class-name receiver and a typed parameter +proven ts callees src/sides.ts:onClass "method read src/sides.ts:3" +notproven ts callees src/sides.ts:onClass "read src/sides.ts:4" +proven ts callees src/sides.ts:onTyped "method read src/sides.ts:4" +notproven ts callees src/sides.ts:onTyped "read src/sides.ts:3" +# Kotlin: Dial.read() is the companion's; Knob().read() never the companion's +exactproven kt callees src/app/Sides.kt:onClass "fn read src/app/Sides.kt" +notproven kt callees src/app/Sides.kt:onInstance "read src/app/Sides.kt:6" +# Python __getattr__ runs only after a miss: run()'s self.real() resolves to Proxyish.real (the hook changes nothing for a +# member the class defines); self.ghost() — a name the lookup misses — is never proven onto another class's ghost +proven py callees src/tui/ghost.py:run "fn real src/tui/ghost.py:7" +notproven py callees src/tui/ghost.py:run "real src/tui/ghost.py:19" "ghost src/tui/ghost.py:16" echo "=== (H) propagation: one hedge, every surface ===" # the name-only TRUE witness respondWith → reply.send(): kept and marked everywhere marked js callers lib/reply.js:send "respondWith lib/reply.js" diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md index a183dc0f8..c01ae056c 100644 --- a/test/receiverevidencefix/README.md +++ b/test/receiverevidencefix/README.md @@ -40,4 +40,8 @@ definition beside them, plus near-miss calls whose receiver IS proven and whose too), an in-repo superclass's member (and Java `super.flush()`), a free / top-level function, a Ruby top-level def; a Ruby included module's method is resolved or hedged (the contract only: Ruby's own lookup is separate work). +Sides of a class lookup (js/lib/sides.js, ts/src/sides.ts, kt/src/app/Sides.kt): a call on the class object reaches +its static (companion) members, a call on an instance the others. A fallback hook only after a miss +(py/src/tui/ghost.py): `__getattr__` leaves a defined member's call resolved. + The names are paraphrases of graded false rows; the code is minimal and is never built. diff --git a/test/receiverevidencefix/js/lib/sides.js b/test/receiverevidencefix/js/lib/sides.js new file mode 100644 index 000000000..92019aa94 --- /dev/null +++ b/test/receiverevidencefix/js/lib/sides.js @@ -0,0 +1,13 @@ +// The two SIDES of a class lookup: a call on the CLASS object reaches its static members, a call on an INSTANCE the others. +class Gauge { + static read () { return 1 } + read () { return 2 } + static make () { return this.read() } +} +class Plain { + read () { return 3 } +} +function onClass () { return Gauge.read() } +function onInstance () { return new Gauge().read() } +function wrongSide () { return Plain.read() } +module.exports = { Gauge, Plain, onClass, onInstance, wrongSide } diff --git a/test/receiverevidencefix/kt/src/app/Sides.kt b/test/receiverevidencefix/kt/src/app/Sides.kt new file mode 100644 index 000000000..aea3c7234 --- /dev/null +++ b/test/receiverevidencefix/kt/src/app/Sides.kt @@ -0,0 +1,16 @@ +package app + +// The two SIDES of a class lookup: a companion member is the CLASS side, an instance member the other. +class Dial { + companion object { + fun read(): Int = 1 + } + fun turn(): Int = 2 +} + +class Knob { + fun read(): Int = 3 +} + +fun onClass(): Int = Dial.read() +fun onInstance(): Int = Knob().read() diff --git a/test/receiverevidencefix/py/src/tui/ghost.py b/test/receiverevidencefix/py/src/tui/ghost.py new file mode 100644 index 000000000..3ec136a73 --- /dev/null +++ b/test/receiverevidencefix/py/src/tui/ghost.py @@ -0,0 +1,20 @@ +# A class with __getattr__: Python calls the hook only after normal lookup MISSES, so a method the class defines +# resolves as usual and only a name the lookup misses is left to the name ladder. +class Proxyish: + def __getattr__(self, name): + return lambda: 0 + + def real(self): + return 1 + + def run(self): + self.real() + return self.ghost() + + +class Haunted: + def ghost(self): + return 2 + + def real(self): + return 3 diff --git a/test/receiverevidencefix/ts/src/sides.ts b/test/receiverevidencefix/ts/src/sides.ts new file mode 100644 index 000000000..3ef99360f --- /dev/null +++ b/test/receiverevidencefix/ts/src/sides.ts @@ -0,0 +1,7 @@ +// The two SIDES of a class lookup, typed: the class object reaches its static members, an instance the others. +export class Meter { + static read(): number { return 1 } + read(): number { return 2 } +} +export function onClass(): number { return Meter.read() } +export function onTyped(m: Meter): number { return m.read() } From eb553122631f0dcddee9519414322ed664aebbd0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:55:11 -0400 Subject: [PATCH 042/229] refactor: split writeWhereisPage's root and rows, sigRowHead's tail (quality-delta) writeWhereisRoot / writeWhereisRows carry the open tag and the row listing; sigRowTail the cx/ccx/in + lens + r= tail. No output byte moves. Co-Authored-By: Claude Opus 5.5 --- src/crossref.h | 186 ++++++++++++++++++++++++++++-------------------- src/serialize.h | 55 +++++++------- 2 files changed, 139 insertions(+), 102 deletions(-) diff --git a/src/crossref.h b/src/crossref.h index 731787729..de1df4ce1 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -2905,6 +2905,111 @@ inline std::string whereisRefsNext( const WhereResult& res ) return nextFlag( "--whereis=", res.sym ) + " --whereis-listing=refs"; } +// The page a --whereis answer prints: the listed rows and the window over them (writeWhereisPage's own reading of its +// arguments, handed to the two writers below so neither re-derives it). +struct WhereisPageView +{ + const ListedHits& listed; + PageWindow page; + int pageLimit = 0; + int pageOffset = 0; +}; + +// The open tag (writeWhereisPage). +inline void writeWhereisRoot( std::FILE* out, const WhereResult& res, const WhereisPageView& v, const std::string& headDate, + const XmlEscaper& ex ) +{ + const ListedHits& listed = v.listed; + const PageWindow& hitPage = v.page; + const std::size_t listedCount = listed.rows.size(); + const int pageLimit = v.pageLimit; + const int pageOffset = v.pageOffset; + char pab[ kPageDisclosureCap ]; + // §A7(iii): refs_scanned=, not refs=. --stray-content and --abi both spell the MATCHED set refs=; this one + // counted every branch the sweep READ (73 here, matched or not) under the same attribute name — one noun, + // two meanings, across sibling verbs an agent reads together. + // T1: the claim is scan-exhaustiveness AND listing-wholeness — an uncut page over a clean scan. Appended + // LAST (after at=) so no existing attribute-adjacency assertion can break on it, the same placement rule + // the graph verbs' floor marker follows. When either half fails, NOTHING is added: the truncation + // vocabulary above already covers every partial shape, and complete-equals-zero would be noise. + // A dotted selector was searched as a literal the question did not mean: exhaustive over that literal, but not + // an answer to "where is Class.method", so it never claims (its selector-note below says why). + // listing= (WhereisListing): the claim reads the LISTED rows — under listing=defs every def row on this page (the + // ref rows are counted exactly by , never silently gone); under listing=refs every ref row. + const bool completeClaim = res.scanExhaustive && hitPage.begin == 0 && hitPage.end == listedCount && res.dottedRetry.empty(); + // H14/M6: refs_scanned="80" under a ref-name filter is a total for the FILTER, not for the repo (the + // audit measured 80 filtered vs 189 unfiltered) — so the filter is named beside the number it bounds. + const std::string whFilterAttr = res.filter.empty() ? std::string() : ( " filter=\"" + ex( res.filter ) + "\"" ); + // The overlay's two marks, both absent on a clean checkout (so its answer is byte-identical to before): +dirty + // on the stamp whenever a differing path was seen, and worktree= naming what the overlay managed. + static constexpr std::string_view kWorktreeAttr[] = { "", " worktree=\"read\"", " worktree=\"partial\"", " worktree=\"unlisted\"" }; + const bool dirty = res.worktree == WorktreeOverlay::Read || res.worktree == WorktreeOverlay::Partial; + // THE HOIST (lossless): a checkout row on HEAD's own commit repeats tip= (= at= without +dirty) and date= (HEAD's + // committer date) on every row. Those rows omit both, and the root says the date ONCE (head_date=) — only when a + // printed row actually omitted it, so a page of branch rows alone is unchanged. Placed before at= so the at= / + // worktree= / complete= adjacency every existing assertion reads stays byte-stable. + bool anyHoist = false; + for( std::size_t k = hitPage.begin; k < hitPage.end; ++k ) + { + anyHoist = anyHoist || whereisRowOnHeadCommit( res.hits[ listed.rows[ k ] ], res.headSha, headDate ); + } + const std::string listingAttr = listed.attr.empty() ? std::string() : ( " listing=\"" + std::string( listed.attr ) + "\"" ); + const std::string headDateAttr = anyHoist ? ( " head_date=\"" + ex( headDate ) + "\"" ) : std::string(); + rw::emitTo( out, "", + ex( res.sym ).c_str(), res.onHead ? 1 : 0, res.refsScanned, res.distinctBlobs, res.hits.size(), + res.headLabelsFromIndex ? "index" : "lexical", whFilterAttr.c_str(), + pageDisclosure( pab, sizeof( pab ), hitPage.end - hitPage.begin, listedCount, hitPage.end, + pageLimit, pageOffset, true ), + listingAttr.c_str(), headDateAttr.c_str(), + res.headSha.c_str(), dirty ? "+dirty" : "", + kWorktreeAttr[ std::size_t( res.worktree ) ], + completeClaim ? " complete=\"1\"" : "" ); + +} + +// The rows of the page, its remainder and the element (writeWhereisPage). +inline void writeWhereisRows( std::FILE* out, const WhereResult& res, const ListedHits& listed, const PageWindow& hitPage, + const std::string& headDate, const XmlEscaper& ex ) +{ + const std::size_t listedCount = listed.rows.size(); + // The contract, restated because it was false here: shown + dropped == the listed rows, ALWAYS. The + // count must be taken against the CAP, not against a loop variable `shown++ >= cap` has already pushed to + // cap+1 — that off-by-one under-reported every drop by exactly one row (81 hits, 60 shown, "20" dropped), + // and at exactly cap+1 hits `size > shown` went false, so the element vanished and a row disappeared + // unmarked. Nothing is dropped without a number is a headline claim; keep it arithmetically true. + std::size_t shownCount = 0; + for( std::size_t k = hitPage.begin; k < hitPage.end; ++k ) + { + const WhereHit& h = res.hits[ listed.rows[ k ] ]; + ++shownCount; + if( whereisRowOnHeadCommit( h, res.headSha, headDate ) ) + { + rw::emitTo( out, "", + ex( h.ref ).c_str(), ex( h.path ).c_str(), + h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); + continue; + } + rw::emitTo( out, "", + ex( h.ref ).c_str(), h.tip.c_str(), ex( h.date ).c_str(), ex( h.path ).c_str(), + h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); + } + ASSUME( shownCount == hitPage.end - hitPage.begin ); + // = the LISTED rows AFTER this page, so shown + more == the listed rows from this page's offset + // on. Un-paginated that is the historic "listed rows minus the 60 printed"; paged it is what the NEXT page holds + // (and next_offset= on the root says where to ask for it). + if( hitPage.end < listedCount ) + { + rw::emitTo( out, "", listedCount - hitPage.end ); + } + // The elided ref rows, COUNTED and one pasteable call away: count= is exact (every kind="ref" row of the hit list, + // whatever page this is), and next= lists exactly them. Not a element: is this listing's own + // page remainder, and a reader (or a gate) that reads " 0 ) + { + rw::emitTo( out, "", listed.refsElided, nextAttrXml( whereisRefsNext( res ) ) ); + } +} + // Contract-level defect: this verb said hits="2560" and printed 60, and // --limit/--offset were accepted and ignored, so a paging loop over it never advanced and never ended. // `pageLimit`/`pageOffset` (0 = un-paginated) window the hit list, which is already deterministically @@ -3003,48 +3108,8 @@ inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_ "raise the default cap with limit=N (offset=M pages; a cut listing carries total=/has_more=/next_offset= so a paging loop can continue from it). " ); writeWhereisLegendTail( out, res ); std::fputs( "-->", out ); - char pab[ kPageDisclosureCap ]; - // §A7(iii): refs_scanned=, not refs=. --stray-content and --abi both spell the MATCHED set refs=; this one - // counted every branch the sweep READ (73 here, matched or not) under the same attribute name — one noun, - // two meanings, across sibling verbs an agent reads together. - // T1: the claim is scan-exhaustiveness AND listing-wholeness — an uncut page over a clean scan. Appended - // LAST (after at=) so no existing attribute-adjacency assertion can break on it, the same placement rule - // the graph verbs' floor marker follows. When either half fails, NOTHING is added: the truncation - // vocabulary above already covers every partial shape, and complete-equals-zero would be noise. - // A dotted selector was searched as a literal the question did not mean: exhaustive over that literal, but not - // an answer to "where is Class.method", so it never claims (its selector-note below says why). - // listing= (WhereisListing): the claim reads the LISTED rows — under listing=defs every def row on this page (the - // ref rows are counted exactly by , never silently gone); under listing=refs every ref row. - const bool completeClaim = res.scanExhaustive && hitPage.begin == 0 && hitPage.end == listedCount && res.dottedRetry.empty(); - // H14/M6: refs_scanned="80" under a ref-name filter is a total for the FILTER, not for the repo (the - // audit measured 80 filtered vs 189 unfiltered) — so the filter is named beside the number it bounds. - const std::string whFilterAttr = res.filter.empty() ? std::string() : ( " filter=\"" + ex( res.filter ) + "\"" ); - // The overlay's two marks, both absent on a clean checkout (so its answer is byte-identical to before): +dirty - // on the stamp whenever a differing path was seen, and worktree= naming what the overlay managed. - static constexpr std::string_view kWorktreeAttr[] = { "", " worktree=\"read\"", " worktree=\"partial\"", " worktree=\"unlisted\"" }; - const bool dirty = res.worktree == WorktreeOverlay::Read || res.worktree == WorktreeOverlay::Partial; - // THE HOIST (lossless): a checkout row on HEAD's own commit repeats tip= (= at= without +dirty) and date= (HEAD's - // committer date) on every row. Those rows omit both, and the root says the date ONCE (head_date=) — only when a - // printed row actually omitted it, so a page of branch rows alone is unchanged. Placed before at= so the at= / - // worktree= / complete= adjacency every existing assertion reads stays byte-stable. - const std::string headDate = whereisHeadDate( res ); - bool anyHoist = false; - for( std::size_t k = hitPage.begin; k < hitPage.end; ++k ) - { - anyHoist = anyHoist || whereisRowOnHeadCommit( res.hits[ listed.rows[ k ] ], res.headSha, headDate ); - } - const std::string listingAttr = listed.attr.empty() ? std::string() : ( " listing=\"" + std::string( listed.attr ) + "\"" ); - const std::string headDateAttr = anyHoist ? ( " head_date=\"" + ex( headDate ) + "\"" ) : std::string(); - rw::emitTo( out, "", - ex( res.sym ).c_str(), res.onHead ? 1 : 0, res.refsScanned, res.distinctBlobs, res.hits.size(), - res.headLabelsFromIndex ? "index" : "lexical", whFilterAttr.c_str(), - pageDisclosure( pab, sizeof( pab ), hitPage.end - hitPage.begin, listedCount, hitPage.end, - pageLimit, pageOffset, true ), - listingAttr.c_str(), headDateAttr.c_str(), - res.headSha.c_str(), dirty ? "+dirty" : "", - kWorktreeAttr[ std::size_t( res.worktree ) ], - completeClaim ? " complete=\"1\"" : "" ); - + const std::string headDate = whereisHeadDate( res ); + writeWhereisRoot( out, res, WhereisPageView{ listed, hitPage, pageLimit, pageOffset }, headDate, ex ); writeWhereisSelectorNotes( out, res, ex ); // The history lane, when it was asked for: what the probe did, then this symbol's own verdict. @@ -3059,42 +3124,7 @@ inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_ // row is printed unless the index itself already proved the symbol is defined on HEAD. writeWhereisFate( out, res, ex ); - // The contract, restated because it was false here: shown + dropped == the listed rows, ALWAYS. The - // count must be taken against the CAP, not against a loop variable `shown++ >= cap` has already pushed to - // cap+1 — that off-by-one under-reported every drop by exactly one row (81 hits, 60 shown, "20" dropped), - // and at exactly cap+1 hits `size > shown` went false, so the element vanished and a row disappeared - // unmarked. Nothing is dropped without a number is a headline claim; keep it arithmetically true. - std::size_t shownCount = 0; - for( std::size_t k = hitPage.begin; k < hitPage.end; ++k ) - { - const WhereHit& h = res.hits[ listed.rows[ k ] ]; - ++shownCount; - if( whereisRowOnHeadCommit( h, res.headSha, headDate ) ) - { - rw::emitTo( out, "", - ex( h.ref ).c_str(), ex( h.path ).c_str(), - h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); - continue; - } - rw::emitTo( out, "", - ex( h.ref ).c_str(), h.tip.c_str(), ex( h.date ).c_str(), ex( h.path ).c_str(), - h.line, h.isDef ? "def" : "ref", h.testLocal ? " test_local=\"1\"" : "", ex( h.text ).c_str() ); - } - ASSUME( shownCount == hitPage.end - hitPage.begin ); - // = the LISTED rows AFTER this page, so shown + more == the listed rows from this page's offset - // on. Un-paginated that is the historic "listed rows minus the 60 printed"; paged it is what the NEXT page holds - // (and next_offset= on the root says where to ask for it). - if( hitPage.end < listedCount ) - { - rw::emitTo( out, "", listedCount - hitPage.end ); - } - // The elided ref rows, COUNTED and one pasteable call away: count= is exact (every kind="ref" row of the hit list, - // whatever page this is), and next= lists exactly them. Not a element: is this listing's own - // page remainder, and a reader (or a gate) that reads " 0 ) - { - rw::emitTo( out, "", listed.refsElided, nextAttrXml( whereisRefsNext( res ) ) ); - } + writeWhereisRows( out, res, listed, hitPage, headDate, ex ); rw::emitRaw( out, "" ); } diff --git a/src/serialize.h b/src/serialize.h index 9b827113e..081d7706e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4192,6 +4192,36 @@ inline std::string lensRowPath( const IngestResult& ing, std::uint32_t fileId, s : std::string( rw::sarif::rootRelativeUri( ing.files[ fileId ], rw::sarif::rootPrefixOf( rootArg ) ) ); } +// The descriptive tail of one "" row, after p=/layer= and through the closing '>': cx=/ccx=/in= under facts.metrics +// (each omitted at 0 where sigRowElidesZero), the Q3 lens + pure, then r=. Split out of sigRowHead so the head stays a head. +inline void sigRowTail( char* tail, std::size_t cap, const Symbol& s, const SigRowFacts& facts, NodeId id, const char* rankAttr ) +{ + if( facts.metrics && sigRowElidesZero( facts, id ) ) + { + // lean-answers lane: the same three facts, each omitted at 0 (the lens legend: absent = 0) + char cxAttr[ 24 ]; cxAttr[ 0 ] = '\0'; + char ccxAttr[ 24 ]; ccxAttr[ 0 ] = '\0'; + char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; + if( s.cx != 0 ) { rw::formatTo( cxAttr, sizeof( cxAttr ), " cx=\"{}\"", s.cx ); } + if( s.ccx != 0 ) { rw::formatTo( ccxAttr, sizeof( ccxAttr ), " ccx=\"{}\"", s.ccx ); } + if( ( *facts.fanIn )[ id ] != 0 ) { rw::formatTo( inAttr, sizeof( inAttr ), " in=\"{}\"", ( *facts.fanIn )[ id ] ); } + rw::formatTo( tail, cap, "{}{}{}{}{}{}>", rw::cstr( cxAttr ), rw::cstr( ccxAttr ), rw::cstr( inAttr ), facts.lens, facts.pure, rankAttr ); + } + else if( facts.metrics ) + { + char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; + if( facts.fanIn && id < facts.fanIn->size() ) + { + rw::formatTo( inAttr, sizeof( inAttr ), " in=\"{}\"", ( *facts.fanIn )[ id ] ); + } + rw::formatTo( tail, cap, " cx=\"{}\" ccx=\"{}\"{}{}{}{}>", s.cx, s.ccx, rw::cstr( inAttr ), facts.lens, facts.pure, rankAttr ); + } + else + { + rw::formatTo( tail, cap, "{}{}{}>", facts.lens, facts.pure, rankAttr ); + } +} + // P2.3/P2.4 — the exact "" opening tag of ONE signature row, defined once so the two-phase (globally // budgeted) emitter and the streaming emitter can never drift by a byte: the budget ledger measures exactly // the string this returns. @@ -4237,30 +4267,7 @@ inline std::string sigRowHead( const IngestResult& ing, NodeId id, const SigRowF rw::formatTo( rankAttr, sizeof( rankAttr ), " r=\"{}\"", facts.rank ); } char tail[ 224 ]; - if( facts.metrics && sigRowElidesZero( facts, id ) ) - { - // lean-answers lane: the same three facts, each omitted at 0 (the lens legend: absent = 0) - char cxAttr[ 24 ]; cxAttr[ 0 ] = '\0'; - char ccxAttr[ 24 ]; ccxAttr[ 0 ] = '\0'; - char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; - if( s.cx != 0 ) { rw::formatTo( cxAttr, sizeof( cxAttr ), " cx=\"{}\"", s.cx ); } - if( s.ccx != 0 ) { rw::formatTo( ccxAttr, sizeof( ccxAttr ), " ccx=\"{}\"", s.ccx ); } - if( ( *facts.fanIn )[ id ] != 0 ) { rw::formatTo( inAttr, sizeof( inAttr ), " in=\"{}\"", ( *facts.fanIn )[ id ] ); } - rw::formatTo( tail, sizeof( tail ), "{}{}{}{}{}{}>", rw::cstr( cxAttr ), rw::cstr( ccxAttr ), rw::cstr( inAttr ), facts.lens, facts.pure, rw::cstr( rankAttr ) ); - } - else if( facts.metrics ) - { - char inAttr[ 24 ]; inAttr[ 0 ] = '\0'; - if( facts.fanIn && id < facts.fanIn->size() ) - { - rw::formatTo( inAttr, sizeof( inAttr ), " in=\"{}\"", ( *facts.fanIn )[ id ] ); - } - rw::formatTo( tail, sizeof( tail ), " cx=\"{}\" ccx=\"{}\"{}{}{}{}>", s.cx, s.ccx, rw::cstr( inAttr ), facts.lens, facts.pure, rw::cstr( rankAttr ) ); - } - else - { - rw::formatTo( tail, sizeof( tail ), "{}{}{}>", facts.lens, facts.pure, rw::cstr( rankAttr ) ); - } + sigRowTail( tail, sizeof( tail ), s, facts, id, rw::cstr( rankAttr ) ); head += tail; // extent honesty (kExtentSuspectRowLegend): after r=, before next=, absent when every check held — so every // pre-existing adjacency on an unflagged row is byte-stable and the budget ledger still measures this string. From 0b8e0f1838d2d32aa1d38f163f98eaa6e160c876 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 15:55:56 -0400 Subject: [PATCH 043/229] refactor(count-floor): split callerFloors and the safe-delete floor/legend work into small helpers quality-delta gated the first landing: emitSafeDeleteLegend and runSafeDelete grew in complexity, and the use-form switch, the armed-language test and the lego attribute probe read as clones of existing switch/or-chain shapes. The use form is now a declarative per-SymKind table (kRefRoleTagTable's shape, static_assert-pinned), the armed languages a constant array, the two unbound-spelling scans one helper (relatedByName + markUnboundSpellings), and the safe-delete floor clauses, risk sentence, uses floor and call-site pass helpers of their own. Output is byte-identical (countfloorcheck green). Co-Authored-By: Claude Opus 5.5 --- src/countfloor.h | 317 ++++++++++++++++++++----------------------- src/serialize.h | 3 +- src/verbs_navigate.h | 122 +++++++++++------ 3 files changed, 231 insertions(+), 211 deletions(-) diff --git a/src/countfloor.h b/src/countfloor.h index 2f0bc78fb..2b7a45995 100644 --- a/src/countfloor.h +++ b/src/countfloor.h @@ -35,8 +35,10 @@ #include "infra/Diagnostics.h" #include +#include #include #include +#include #include #include #include @@ -54,34 +56,36 @@ namespace rw // NotCode — a markdown heading or a file's module scope: no code calls or reads it, so a zero there is simply true. enum class UseForm : std::uint8_t { Calls, NotCalls, NotCode }; +// One reading per SymKind, in enum order (model.h's kRefRoleTagTable shape: a declarative table, and the static_assert +// is the guard a -Werror=switch would be — a NEW kind without a reading is a build error, never a silent default). +inline constexpr UseForm kUseFormOfKind[] = { + UseForm::Calls, // Function + UseForm::Calls, // Method + UseForm::NotCalls, // Class: a constructor call is one use; a type mention, a base clause, an isinstance are the rest + UseForm::NotCalls, // Struct + UseForm::NotCalls, // Interface + UseForm::NotCalls, // Var + UseForm::NotCode, // Section (a markdown heading) + UseForm::Calls, // Macro (a function-like invocation binds as a call) + UseForm::NotCalls, // Field + UseForm::NotCalls, // Other + UseForm::NotCode, // ModuleScope +}; +static_assert( std::size( kUseFormOfKind ) == kSymKindCount, "kUseFormOfKind: one reading per SymKind, in enum order" ); + inline UseForm useFormOf( const Symbol& s ) noexcept { - switch( s.kind ) - { - case SymKind::Function: - case SymKind::Method: - case SymKind::Macro: - return UseForm::Calls; - case SymKind::Section: - case SymKind::ModuleScope: - return UseForm::NotCode; - case SymKind::Class: // a constructor call is one use; a type mention, a base clause, an isinstance are the rest - case SymKind::Struct: - case SymKind::Interface: - case SymKind::Var: - case SymKind::Field: - case SymKind::Other: - return UseForm::NotCalls; - } - return UseForm::NotCalls; // a byte past the enum: the conservative answer (a floor), never a silent total + return enumTableAt( kUseFormOfKind, s.kind, UseForm::NotCalls ); // past the enum: the conservative reading (a floor) } -// Whether a read/write/type use of a symbol in `lang` reaches the use-site table in an ingest that captured value -// uses (ingest_sidecap.h arms that pass for C++, ObjC and Python only). Outside those, `--uses` lists calls and value -// references but never a read, so the honest follow-up for a NotCalls symbol is the literal scan. +// The languages whose read/write/type uses reach the use-site table in an ingest that captured value uses +// (ingest_sidecap.h arms that pass for C++, ObjC and Python only). Outside them `--uses` lists calls and value references +// but never a read, so the honest follow-up for a NotCalls symbol is the literal scan. +inline constexpr std::array kValueUsesArmedLangs = { Lang::Cpp, Lang::ObjC, Lang::Python }; + inline bool valueUsesArmedFor( Lang lang ) noexcept { - return lang == Lang::Cpp || lang == Lang::ObjC || lang == Lang::Python; + return std::ranges::find( kValueUsesArmedLangs, lang ) != kValueUsesArmedLangs.end(); } // The follow-up that finds what a floored count could not: the uses verb lists every call, value and (where armed) @@ -92,9 +96,8 @@ inline bool valueUsesArmedFor( Lang lang ) noexcept // uses verb never sees — so a header's NotCalls symbol gets the literal scan too (`struct cell` in a.h used in a.c). inline std::string countFloorNext( const IngestResult& ing, const Symbol& s ) { - const std::string_view path = s.fileId < ing.files.size() ? std::string_view( ing.files[ s.fileId ] ) : std::string_view(); - const bool cHeader = path.ends_with( ".h" ); - const bool literal = useFormOf( s ) == UseForm::NotCalls && ( !valueUsesArmedFor( s.lang ) || cHeader ); + const std::string_view path = s.fileId < ing.files.size() ? std::string_view( ing.files[ s.fileId ] ) : std::string_view(); + const bool literal = useFormOf( s ) == UseForm::NotCalls && ( !valueUsesArmedFor( s.lang ) || path.ends_with( ".h" ) ); return nextFlag( literal ? "--grep=" : "--uses=", s.name ); } @@ -102,146 +105,156 @@ inline std::string countFloorNext( const IngestResult& ing, const Symbol& s ) struct CallerFloor { bool isFloor = false; - bool unmodelled = false; // the NotCalls reason (no call form): the safe-delete verdict reads it std::string next; }; -// callers= floors for a batch of rows: sets[i] is one row's definitions (all one NAME — a grep row's ids, a -// safe-delete selector's defs). ONE pass over the symbols and ONE over the references, whatever the row count — -// never a per-row rescan of the reference table (editcheck.h's rule). `vri` may be null: then it is built here, once, -// and only when a Calls row is still undecided after the cheaper evidence. -inline std::vector callerFloors( const IngestResult& ing, const Graph& g, std::span> sets, - const ValueRefIndex* vri = nullptr ) +namespace countfloor { - std::vector out( sets.size() ); - if( sets.empty() ) + +// Row indices by the NAME their definitions share — a name may head several rows (two chains `A::f` and `B::f`), +// and the reference table is keyed by spelling, so every lookup below asks of the name. +using RowsOfName = HashMap>; +using IdsOfName = HashMap>; + +// The use form of a row: NotCalls if ANY definition is read/named (its count is blind to that one), else Calls if any is +// called, else NotCode (headings and module scopes only). +inline UseForm rowUseForm( const IngestResult& ing, std::span ids ) noexcept +{ + bool anyCalls = false; + for( const NodeId id : ids ) { - return out; + const UseForm form = id < ing.symbols.size() ? useFormOf( ing.symbols[id] ) : UseForm::NotCode; + if( form == UseForm::NotCalls ) + { + return UseForm::NotCalls; + } + anyCalls = anyCalls || form == UseForm::Calls; } - const std::size_t nodeCount = g.wOutDeg.size(); - const auto* inRo = g.inEdges.rowOffsets(); - const auto* inCi = g.inEdges.colIndices(); + return anyCalls ? UseForm::Calls : UseForm::NotCode; +} - // Row → its name; a name may head several rows (two chains `A::f` and `B::f`) — each row asks of the NAME. - HashMap> rowsOfName; - std::vector pending; // Calls rows not yet decided by the cheap evidence - for( std::uint32_t i = 0; i < sets.size(); ++i ) +// For every symbol whose name heads a row, `relatedOf( id )` appended under that name, then sorted and deduplicated so a +// membership test is a binary search: the callers of every same-named definition, or every implementor of one. +template +inline IdsOfName relatedByName( const IngestResult& ing, const RowsOfName& rowsOfName, std::size_t idLimit, RelatedOf&& relatedOf ) +{ + IdsOfName out; + for( NodeId id = 0; id < ing.symbols.size() && id < idLimit; ++id ) { - const std::vector& ids = sets[i]; - bool anyNotCalls = false; - bool anyCalls = false; - for( const NodeId id : ids ) + const auto it = rowsOfName.find( std::string_view( ing.symbols[id].name ) ); + if( it != rowsOfName.end() ) { - if( id >= ing.symbols.size() ) - { - continue; - } - const UseForm form = useFormOf( ing.symbols[id] ); - anyNotCalls = anyNotCalls || form == UseForm::NotCalls; - anyCalls = anyCalls || form == UseForm::Calls; + relatedOf( id, out[ it->first ] ); } - if( ids.empty() || ids[0] >= ing.symbols.size() ) + } + for( auto& [ name, ids ] : out ) + { + std::sort( ids.begin(), ids.end() ); + ids.erase( std::unique( ids.begin(), ids.end() ), ids.end() ); + } + return out; +} + +// ONE pass over the reference table: a reference `isKind` accepts, spelled like a row's name, from a symbol that is not +// in that name's `bound` set (or from file scope), marks every row of the name. `mark( row )` is the caller's verdict. +template +inline void markUnboundSpellings( const IngestResult& ing, const RowsOfName& rowsOfName, const IdsOfName& bound, IsKind&& isKind, Mark&& mark ) +{ + for( const Reference& r : ing.references ) + { + if( !isKind( r ) ) { continue; } - if( anyNotCalls ) + const auto it = rowsOfName.find( std::string_view( r.calleeName ) ); + if( it == rowsOfName.end() ) { - out[i].isFloor = true; - out[i].unmodelled = true; continue; } - if( !anyCalls ) + const auto boundIt = bound.find( it->first ); + const bool isBound = r.fromSymbol != kNoNode && boundIt != bound.end() + && std::binary_search( boundIt->second.begin(), boundIt->second.end(), r.fromSymbol ); + if( !isBound ) { - continue; // NotCode only: nothing calls or reads a heading, the zero is true + for( const std::uint32_t row : it->second ) + { + mark( row ); + } } - if( declinedCallsNaming( g, ids ) > 0 ) + } +} + +inline bool isCallSpelling( const Reference& r ) noexcept +{ + return ( r.role == RefRole::Call || r.role == RefRole::Macro ) && !r.isInherit && !r.isDocLink && !r.isCompose && r.lang != Lang::Markdown; +} + +// The value evidence for one row: a non-decorator value reference (a decorator row, into="@name", is a fact about the +// definition, not a path that calls it). +inline bool hasValueUse( const IngestResult& ing, const ValueRefIndex& vri, std::span ids ) +{ + for( const ValueRefRow& vr : valueRefCallerRows( ing, vri, ids ).rows ) + { + if( vr.ref < ing.references.size() && !ing.references[ vr.ref ].fieldName.starts_with( '@' ) ) { - out[i].isFloor = true; - continue; + return true; } - rowsOfName[ std::string_view( ing.symbols[ ids[0] ].name ) ].push_back( i ); - pending.push_back( i ); } + return false; +} - if( !pending.empty() ) +} // namespace countfloor + +// callers= floors for a batch of rows: sets[i] is one row's definitions (all one NAME — a grep row's ids, a +// safe-delete selector's defs). ONE pass over the symbols and ONE over the references, whatever the row count — +// never a per-row rescan of the reference table (editcheck.h's rule). `vri` may be null: then it is built here, once, +// and only when a Calls row is still undecided after the cheaper evidence. Evidence, cheapest first: the use form, the +// declined calls, the unbound same-name calls, the value uses. +inline std::vector callerFloors( const IngestResult& ing, const Graph& g, std::span> sets, + const ValueRefIndex* vri = nullptr ) +{ + std::vector out( sets.size() ); + countfloor::RowsOfName rowsOfName; + std::vector pending; // Calls rows the use form and the declines left undecided + for( std::uint32_t i = 0; i < sets.size(); ++i ) { - // Per pending NAME: every symbol with an edge into ANY definition of that name. A call from one of them was - // bound somewhere — to this row or to a same-named sibling — so it is not evidence of a miss. - HashMap> boundCallers; - for( NodeId id = 0; id < ing.symbols.size() && id < nodeCount; ++id ) + const std::vector& ids = sets[i]; + if( ids.empty() || ids[0] >= ing.symbols.size() ) { - const auto it = rowsOfName.find( std::string_view( ing.symbols[id].name ) ); - if( it == rowsOfName.end() ) - { - continue; - } - std::vector& bound = boundCallers[ it->first ]; - for( std::uint32_t k = inRo[id]; k < inRo[id + 1]; ++k ) - { - bound.push_back( inCi[k] ); - } + continue; } - for( auto& [ name, bound ] : boundCallers ) + const UseForm form = countfloor::rowUseForm( ing, ids ); + out[i].isFloor = form == UseForm::NotCalls || ( form == UseForm::Calls && declinedCallsNaming( g, ids ) > 0 ); + if( form == UseForm::Calls && !out[i].isFloor ) { - std::sort( bound.begin(), bound.end() ); - bound.erase( std::unique( bound.begin(), bound.end() ), bound.end() ); + rowsOfName[ std::string_view( ing.symbols[ ids[0] ].name ) ].push_back( i ); + pending.push_back( i ); } + } - // ONE pass: a call or macro site spelled like a pending name, from a symbol no edge of that name leaves. - for( const Reference& r : ing.references ) - { - if( ( r.role != RefRole::Call && r.role != RefRole::Macro ) || r.isInherit || r.isDocLink || r.isCompose || r.lang == Lang::Markdown ) - { - continue; - } - const auto it = rowsOfName.find( std::string_view( r.calleeName ) ); - if( it == rowsOfName.end() ) - { - continue; - } - const auto boundIt = boundCallers.find( it->first ); - const std::vector* bound = boundIt == boundCallers.end() ? nullptr : &boundIt->second; - const bool isBound = r.fromSymbol != kNoNode && bound != nullptr && std::binary_search( bound->begin(), bound->end(), r.fromSymbol ); - if( isBound ) - { - continue; - } - for( const std::uint32_t row : it->second ) - { - out[row].isFloor = true; - } - } + if( !pending.empty() ) + { + // A call from a symbol with an edge into ANY definition of the name was bound somewhere — to this row or to a + // same-named sibling — so it is not evidence of a miss. + const auto* inRo = g.inEdges.rowOffsets(); + const auto* inCi = g.inEdges.colIndices(); + const countfloor::IdsOfName boundCallers = countfloor::relatedByName( ing, rowsOfName, g.wOutDeg.size(), + [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), inCi + inRo[id], inCi + inRo[id + 1] ); } ); + countfloor::markUnboundSpellings( ing, rowsOfName, boundCallers, countfloor::isCallSpelling, + [ & ]( std::uint32_t row ) { out[row].isFloor = true; } ); - // value evidence, last and only where still undecided: a ValueRefIndex is one more pass over the table. - std::vector stillOpen; + std::optional own; // one more pass over the table: built only if a row is still open for( const std::uint32_t row : pending ) { - if( !out[row].isFloor ) + if( out[row].isFloor ) { - stillOpen.push_back( row ); + continue; } - } - if( !stillOpen.empty() ) - { - std::optional own; - const ValueRefIndex* index = vri; - if( index == nullptr ) + if( vri == nullptr && !own ) { own.emplace( ing ); - index = &*own; - } - for( const std::uint32_t row : stillOpen ) - { - for( const ValueRefRow& vr : valueRefCallerRows( ing, *index, sets[row] ).rows ) - { - // a decorator row (into="@name") is a fact about the definition, not a path that calls it - if( vr.ref < ing.references.size() && !ing.references[ vr.ref ].fieldName.starts_with( '@' ) ) - { - out[row].isFloor = true; - break; - } - } } + out[row].isFloor = countfloor::hasValueUse( ing, vri != nullptr ? *vri : *own, sets[row] ); } } @@ -263,8 +276,8 @@ inline std::vector callerFloors( const IngestResult& ing, const Gra inline std::vector implementorFloors( const IngestResult& ing, const std::vector>& graphImplementors, std::span ifaces ) { - std::vector out( ifaces.size(), 0 ); - HashMap> rowsOfName; + std::vector out( ifaces.size(), 0 ); + countfloor::RowsOfName rowsOfName; for( std::uint32_t i = 0; i < ifaces.size(); ++i ) { if( ifaces[i] < ing.symbols.size() ) @@ -276,44 +289,10 @@ inline std::vector implementorFloors( const IngestResult& ing, const std:: { return out; } - HashMap> boundDerived; // every implementor of any def of the name - for( NodeId id = 0; id < ing.symbols.size() && id < graphImplementors.size(); ++id ) - { - const auto it = rowsOfName.find( std::string_view( ing.symbols[id].name ) ); - if( it == rowsOfName.end() ) - { - continue; - } - std::vector& bound = boundDerived[ it->first ]; - bound.insert( bound.end(), graphImplementors[id].begin(), graphImplementors[id].end() ); - } - for( auto& [ name, bound ] : boundDerived ) - { - std::sort( bound.begin(), bound.end() ); - bound.erase( std::unique( bound.begin(), bound.end() ), bound.end() ); - } - for( const Reference& r : ing.references ) - { - if( !r.isInherit && r.role != RefRole::Extends ) - { - continue; - } - const auto it = rowsOfName.find( std::string_view( r.calleeName ) ); - if( it == rowsOfName.end() ) - { - continue; - } - const auto boundIt = boundDerived.find( it->first ); - const bool isBound = r.fromSymbol != kNoNode && boundIt != boundDerived.end() - && std::binary_search( boundIt->second.begin(), boundIt->second.end(), r.fromSymbol ); - if( !isBound ) - { - for( const std::uint32_t row : it->second ) - { - out[row] = 1; - } - } - } + const countfloor::IdsOfName boundDerived = countfloor::relatedByName( ing, rowsOfName, graphImplementors.size(), + [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), graphImplementors[id].begin(), graphImplementors[id].end() ); } ); + countfloor::markUnboundSpellings( ing, rowsOfName, boundDerived, []( const Reference& r ) { return r.isInherit || r.role == RefRole::Extends; }, + [ & ]( std::uint32_t row ) { out[row] = 1; } ); return out; } diff --git a/src/serialize.h b/src/serialize.h index 40733dd57..74433b14e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -7257,9 +7257,10 @@ inline constexpr std::string_view kForLegoCountLegend = // Whether a rendered ranked block carries an attribute kForLegoCountLegend defines (attribute values are escaped, // so ` name="` can only open an attribute). +inline constexpr std::string_view kLegoCountAttrNeedles[] = { " implementors_shown=\"", " implementors_floor=\"" }; inline bool legoCarriesCountAttrs( std::string_view legoXml ) noexcept { - return legoXml.find( " implementors_shown=\"" ) != std::string_view::npos || legoXml.find( " implementors_floor=\"" ) != std::string_view::npos; + return std::ranges::any_of( kLegoCountAttrNeedles, [ & ]( std::string_view needle ) { return legoXml.find( needle ) != std::string_view::npos; } ); } // L2 fault-injection fix (independent review, 2026-09-19): packLego/packCompose's out-params give the stub diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 907972da8..a46dd6c44 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -852,25 +852,14 @@ struct SafeDeleteLegendFlags bool hasSitesL = false; // a row is on this page }; -inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs, std::size_t ambiguousCallers, std::string_view risk, - const SafeDeleteLegendFlags& flags ) +// count-floor (countfloor.h) + CALLSITE-LINE: the safe-delete legend's clauses for callers_floor=, uses_floor=, next= and +// sites_l=, each exactly when its attribute rides the root/rows, each saying what it does NOT mean. +inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, std::string_view risk ) { - const bool singleRoot = flags.singleRoot; - const rw::GaugeClauses hasUnindexed = flags.hasUnindexed; - const bool hasModScope = flags.hasModScope; - const std::size_t declinedCalls = flags.declinedCalls; - // declined_calls=: its definition, and — beside risk=none-found — the sentence that keeps none-found from reading as - // a safety verdict about a definition some declined call may have meant (graph.h declinedCallsNaming). - std::string declinedClause = rw::declinedCallsLegendWithGate( declinedCalls > 0, flags.gateDeclined ); - if( declinedCalls > 0 && risk == "none-found" ) - { - declinedClause += "none-found beside declined_calls= is not a safety reading: those calls may reach this definition. "; - } - declinedClause += rw::valueRefsSafeDeleteLegend( flags.valueRefs > 0 ); // exactly when the root carries value_refs= - // count-floor (countfloor.h): each clause exactly when its attribute rides the root/rows, each saying what it does NOT mean. + std::string clause; if( flags.callersFloor ) { - declinedClause += "callers_floor=\"1\": callers= may be SHORT for this definition, because the index holds evidence of a miss " + clause += "callers_floor=\"1\": callers= may be SHORT for this definition, because the index holds evidence of a miss " "for it: a call the resolver declined to bind that named it, a call or macro site spelled like it that bound to " "no definition at all, a use as a value, or a kind that is read or named more than called (a variable, a " "class or struct, an interface) — impact_reaches= is walked over the same edges and is short the same way. It does NOT " @@ -878,25 +867,60 @@ inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs } if( flags.usesFloor ) { - declinedClause += "uses_floor=\"1\": uses= could not see this kind's reads, writes or type mentions in this run (only calls, values, " + clause += "uses_floor=\"1\": uses= could not see this kind's reads, writes or type mentions in this run (only calls, values, " "imports and extends sites are indexed for it), so a zero there is not a count of its uses. "; } if( flags.callersFloor && risk == "none-found" ) { - declinedClause += "none-found beside callers_floor= is not a safety reading: the calls it names may reach this definition. "; + clause += "none-found beside callers_floor= is not a safety reading: the calls it names may reach this definition. "; } if( flags.callersFloor || flags.usesFloor ) { - declinedClause += "next= is the call that lists what the floored counts could not: the uses verb (every call, value and, where " + clause += "next= is the call that lists what the floored counts could not: the uses verb (every call, value and, where " "indexed, read site spelled like sym=, bound or not), or the literal grep where this language's reads are never indexed. "; } if( flags.hasSitesL ) { - declinedClause += "sites_l= on a c row is the ascending LINE list of that caller's call sites spelled like sym= (the rows the uses " + clause += "sites_l= on a c row is the ascending LINE list of that caller's call sites spelled like sym= (the rows the uses " "verb prints) — the lines to open, beside p=, which is where the CALLER is defined. It does NOT mean each line " "binds to this definition (a same-named call to another definition in that caller is listed too); absent when no " "call there is spelled like sym= (a renamed import). Two calls on one line are one site. "; } + return clause; +} + +// The one risk= sentence in force (a reader needs their own value spelled out, not a glossary of four). +inline const char* safeDeleteRiskSentence( std::string_view risk ) noexcept +{ + if( risk == "unmodelled" ) + { + return "unmodelled: zero callers AND zero uses were READ, but this kind is used by reading or naming it and those " + "counts cannot see that — no reading either way, never a none-found. "; + } + if( risk == "none-found" ) + { + return "none-found: zero callers AND zero uses — an ABSENCE of evidence, never evidence of absence. "; + } + return risk == "untested-radius" ? "untested-radius: callers or uses exist, and NONE of the transitive blast radius is test-covered. " + : "uses-exist: callers or uses exist, and at least part of the radius is test-covered. "; +} + +inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs, std::size_t ambiguousCallers, std::string_view risk, + const SafeDeleteLegendFlags& flags ) +{ + const bool singleRoot = flags.singleRoot; + const rw::GaugeClauses hasUnindexed = flags.hasUnindexed; + const bool hasModScope = flags.hasModScope; + const std::size_t declinedCalls = flags.declinedCalls; + // declined_calls=: its definition, and — beside risk=none-found — the sentence that keeps none-found from reading as + // a safety verdict about a definition some declined call may have meant (graph.h declinedCallsNaming). + std::string declinedClause = rw::declinedCallsLegendWithGate( declinedCalls > 0, flags.gateDeclined ); + if( declinedCalls > 0 && risk == "none-found" ) + { + declinedClause += "none-found beside declined_calls= is not a safety reading: those calls may reach this definition. "; + } + declinedClause += rw::valueRefsSafeDeleteLegend( flags.valueRefs > 0 ); // exactly when the root carries value_refs= + declinedClause += safeDeleteFloorLegend( flags, risk ); // count-floor + CALLSITE-LINE: present-only clauses rw::emitTo( stdout, " +666 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **665 gate scripts** and is the authoritative list; +`test/regression.sh` names **666 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 30956ac0c..f8c5be3f8 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` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 666 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **665 gate scripts**, all of which exist on disk. +naming **666 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 665. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 666. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 3868b0a5c..aad58c82a 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 = [ - ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["666 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "666", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["666 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..d9b5cd2d1 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -705,6 +705,11 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // path: src/verbs_navigate.h (the root emit) { "from_p", "from_p=/to_p=: the definitions from= and to= were bound to", false, "path", MapHeaderRead::No, {}, "path" }, // also defines to_p= { "from_defs", "from_defs=/to_defs=: definitions of each name, all searched; above 1, qualify file:name", false, "path", MapHeaderRead::No, {}, "path" }, // also defines to_defs= + // PATH-GAP (src/pathgaps.h kPathGapsLegend), present only on a reachable=0 answer whose search met a gap. Each says what it does NOT + // mean in the same breath (checklist 3): not proof of no path, a row is never a hop, and which calls are not counted. + { "searched", "searched=N: reachable=0, but the search from from= (N symbols over resolved call edges) is INCOMPLETE: not proof of no path", false, "path", MapHeaderRead::No, {}, "path" }, + { "gaps", "gaps=kind:N,...: calls there with no edge (declined: nothing chose a candidate; unresolved), a function handed off as a value, a call through a slot; ambiguous and undefined-name calls not counted", true, {}, MapHeaderRead::No, {}, "path" }, + { "gap_syms", "gap_syms=N: searched symbols carrying gaps; rows, nearest from= first, are where the search could not see, never a hop; gap_syms_capped=1 more than shown", false, "path", MapHeaderRead::No, {}, "path" }, // quality-delta: src/verbs_quality.h (root emit) + src/quality.h identityDisclosure { "stale", "stale=N: ack ledger rows whose target no longer applies (sa rows); never gating", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "preexisting-worse", "preexisting-worse=N: regressions on symbols that existed at baseline; only these gate (when major)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/main.cpp b/src/main.cpp index 003a9b14b..4f7376c2d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "serialize.h" #include "pageview.h" // §P8: the ONE --limit/--offset window + root-element shown=/capped= disclosure #include "graphlegend.h" // §H4 §3.4: the ONE counts_floor= marker + the shared graph-count legend wording +#include "pathgaps.h" // PATH-GAP: --path's gap analysis + clause, shared with MCP path_between #include "columnar.h" // RESEARCH lever 1: opt-in columnar re-serialization for the flat list verbs (--format=columnar) #include "redact.h" // RedactCounts + reportRedactions for the emitted-body secret redaction #include "filter.h" diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..6b474a4b0 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -37,6 +37,7 @@ #include "slice.h" // lane/tc-sliceat: the shared --slice / MCP slice def-use core (sliceBundleText — ONE emitter, two surfaces) #include "fielduses.h" // the member-variable round: the ONE --uses=Owner.field renderer (renderFieldUses — CLI ≡ MCP) #include "testmap.h" // lane/t10-mcp-coverage: writeAffectedReport — the shared --affected / `affected` verb renderer +#include "pathgaps.h" // PATH-GAP: the shared gap analysis + clause of --path / path_between #include // §B6 M3: the shared root-path existence/directory check (mcpRootRefusal below) #include // mcpAnswerText / usesText: nullopt is an answer buffer that failed, never an empty answer @@ -3197,9 +3198,11 @@ inline std::optional pathText( const std::string& root, const std:: // H5: the same brief floor legend + marker the CLI --path prints (verbs_navigate.h) — one wording, two transports. // Reference-as-value round: the CLI --path's to_value_refs=, by the same call. const std::size_t ptToValueRefs = toValueRefsCount( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); + // PATH-GAP: the CLI --path's gap clause, by the same analysis and emitter (src/pathgaps.h). + const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; rw::emitTo( mem, "{}", unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ).c_str(), - toValueRefsLegend( ptToValueRefs > 0 ), + "graph holds none. {}{}{}{}-->{}", unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ).c_str(), + toValueRefsLegend( ptToValueRefs > 0 ), pathGapsLegend( ptGaps.any() ), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); rw::emitTo( mem, " pathText( const std::string& root, const std:: pth.empty() ? 0 : 1, pth.empty() ? std::size_t( 0 ) : pth.size() - 1, ptRootAttr.c_str(), graphCountFloorAttrXml( g ).c_str() ); // M15: gauge + marker rw::emitTo( mem, "{}", countAttrXmlOrEmpty( "to_value_refs", ptToValueRefs ) ); // absent at zero, as on the CLI - if( pth.empty() ) + std::string ptGapRows; + if( ptGaps.any() ) + { + const PathGapsXml gx = pathGapsXml( ing, ptGaps, ptSingleRoot, ptRootPrefix, "the connect verb on " + from + "," + to, "" ); + rw::emitTo( mem, "{}", gx.rootAttrs ); + ptGapRows = gx.rows; + } + else if( pth.empty() ) { rw::emitTo( mem, " hint=\"no directed call path — try the connect verb on {},{} (undirected: finds a shared caller), or uses/impact for non-call references\"", ex( from ).c_str(), ex( to ).c_str() ); } rw::emitRaw( mem, ">" ); + rw::emitRaw( mem, ptGapRows.c_str() ); for( NodeId n : pth ) { const Symbol& s = ing.symbols[n]; const std::string_view rp = ptSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], ptRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); diff --git a/src/model.h b/src/model.h index 693ee7fb0..0d2933f4f 100644 --- a/src/model.h +++ b/src/model.h @@ -71,7 +71,9 @@ inline constexpr NodeId kNoNode = 0xFFFFFFFFu; // interface keeps t="struct"/t="iface". Every other language's @definition.type capture still maps to Struct (C/C++ // typedef and enum, TS/Swift/Dart aliases, Java/C#/PHP/GDScript enums, Elixir @type) — a disclosed floor, not a claim // that those are structs. The three kinds BEHAVE as Struct did everywhere a predicate reads the kind -// (isStructOrNamedType below): only the label changes, so callable/owner/type-like semantics are byte-identical. +// (isStructOrNamedType below), so only the label changes: a label-normalised base-vs-head sample over Go fixtures and +// 26 argv found no other byte moved (2026-10-04, this lane). Dropping the helper moved nothing on +// those probes either, so it is a guard for predicates no Go fixture reaches today, not a proven behaviour. enum class SymKind : std::uint8_t { Function, Method, Class, Struct, Interface, Var, Section, Macro, Field, Other, ModuleScope, NamedType, Alias, FuncType }; // The number of SymKind enumerators, and the bound a cached def's kind byte is VALIDATED against on the way // back in (ingest_cache.h ByteR::enumU8). The static_assert is not a restatement: enumCountIsExact asks the diff --git a/src/pathgaps.h b/src/pathgaps.h new file mode 100644 index 000000000..52957df07 --- /dev/null +++ b/src/pathgaps.h @@ -0,0 +1,238 @@ +#pragma once +// pathgaps.h — what a reachable="0" path answer's search MET that the call graph holds no edge for (PATH-GAP honesty). +// +// THE DEFECT. --path=A,B (and the MCP path_between twin) answered `reachable="0" hint="no directed call path"` whenever +// the BFS over resolved call edges did not reach B, although that search had walked past calls the graph keeps NO edge +// for: a call the resolver DECLINED (two or more same-language candidates, none local, nothing chose one), a call left +// UNRESOLVED (an in-repo name whose every definition was filtered out), a function handed off as a VALUE (stored or +// passed, then invoked by whoever holds it) and a call made THROUGH a parameter or slot. Any of those can be the hop +// that joins A to B, so "no path" there was a wholeness claim the search could not make. +// +// THE CONTRACT. pathSearchGaps walks the same directed cone the BFS walked (every symbol reachable from the from= +// definitions over resolved call edges) and counts those four populations inside it. When the count is non-zero the +// answer never says "no directed call path": it says the search was incomplete, gives the counts (searched=, gaps=), +// and names the cone symbols that carry them ( rows, nearest to from= first, capped at kPathGapRows and +// disclosed) with a next= that reads their bodies. When the count is zero the old wording stands, byte for byte. +// +// What it does NOT mean: a row is not a hop and not evidence that a path exists — it is where the search could +// not see. AMBIGUOUS calls are deliberately NOT a gap: the resolver gives every candidate of a k-way split its own edge +// (graph.h buildGraph's splitPick), and the BFS follows all of them, so ambiguity never hides a path (it can only make +// a FOUND path a guess, which prov="split" / amb= already disclose). Calls to names with no in-repo definition at all +// (Undefined) are not counted either: the graph keeps no per-caller tally of them, and nearly all are library calls. +// Both are floors the legend sentence states. +// +// ONE analysis and ONE emitter for both transports (verbs_navigate.h runPath, mcpverbs.h pathText): the two used to +// carry their own copies of the hint and must not drift on this clause. + +#include "graph.h" // Graph — out-edges (the cone), declinedOut / unresolvedOut (the per-caller gap counts) +#include "model.h" +#include "nextverb.h" // nextAttrXml — the one pasteable follow-up +#include "sarif.h" // rootRelativeUri — p= relative to root=, as every other row +#include "serialize.h" // escapeXml +#include "valuerefindex.h" // ValueRefIndex::targetsOf — what a value reference may hand off + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rw +{ + +inline constexpr std::size_t kPathGapRows = 3; // rows printed; gap_syms= counts them all, gap_syms_capped="1" says so + +// The four gap populations, in the order gaps= lists them. A declarative table (CONTRIBUTING §3): the spelling lives once. +enum class PathGapKind : std::uint8_t { Declined, Unresolved, Value, Through }; +inline constexpr std::size_t kPathGapKindCount = 4; +inline constexpr std::array kPathGapKindNames = { "declined", "unresolved", "value", "through" }; +using PathGapCounts = std::array; + +struct PathGapSym +{ + NodeId id = kNoNode; + std::uint32_t depth = 0; // hops from the nearest from= definition + PathGapCounts counts{}; +}; + +struct PathSearchGaps +{ + std::size_t searched = 0; // symbols the directed search reached, the from= definitions included + PathGapCounts totals{}; + std::vector syms; // cone symbols carrying >= 1 gap: nearest first, then more gaps, then id + + bool any() const noexcept + { + return std::ranges::any_of( totals, []( std::size_t n ) { return n > 0; } ); + } +}; + +inline std::size_t pathGapSum( const PathGapCounts& c ) noexcept +{ + std::size_t s = 0; + for( const std::size_t n : c ) + { + s += n; + } + return s; +} + +// The directed cone of `srcs` (the same out-edges shortestPathAny walks, every one of them — no early stop at a target, +// because this runs only when there was none) and the gap populations inside it. Deterministic: BFS in id-ascending +// out-edge order, a reference pass in index order, and a total order on the rows. +inline PathSearchGaps pathSearchGaps( const IngestResult& ing, const Graph& g, std::span srcs, const ValueRefIndex& values ) +{ + constexpr std::uint32_t kUnseen = std::numeric_limits::max(); + const std::size_t N = g.wOutDeg.size(); + EXPECTS( g.outOff.size() == N + 1, "the out-edge CSR has one offset per node plus the end" ); + std::vector depth( N, kUnseen ); + std::vector queue; + queue.reserve( 64 ); + for( const NodeId s : srcs ) + { + if( s < N && depth[s] == kUnseen ) + { + depth[s] = 0; + queue.push_back( s ); + } + } + for( std::size_t head = 0; head < queue.size(); ++head ) + { + const NodeId u = queue[head]; + for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) + { + const NodeId v = g.outTargets[k]; + if( v < N && depth[v] == kUnseen ) + { + depth[v] = depth[u] + 1; + queue.push_back( v ); + } + } + } + + PathSearchGaps out; + out.searched = queue.size(); + std::vector per( N ); // only cone entries are ever written + const auto inCone = [ & ]( NodeId n ) noexcept { return n < N && depth[n] != kUnseen; }; + for( const NodeId u : queue ) + { + per[u][ std::size_t( PathGapKind::Declined ) ] = u < g.declinedOut.size() ? g.declinedOut[u] : 0; + per[u][ std::size_t( PathGapKind::Unresolved ) ] = u < g.unresolvedOut.size() ? g.unresolvedOut[u] : 0; + } + for( std::uint32_t i = 0; i < ing.references.size(); ++i ) + { + const Reference& r = ing.references[i]; + if( !inCone( r.fromSymbol ) ) + { + continue; + } + if( r.role == RefRole::Through ) + { + ++per[ r.fromSymbol ][ std::size_t( PathGapKind::Through ) ]; + } + else if( r.role == RefRole::Value ) + { + // A function handed off as a value is a gap only when the search did not reach it by a call anyway. + const std::vector targets = values.targetsOf( i ); + if( std::ranges::any_of( targets, [ & ]( NodeId t ) { return t < N && !inCone( t ); } ) ) + { + ++per[ r.fromSymbol ][ std::size_t( PathGapKind::Value ) ]; + } + } + } + for( const NodeId u : queue ) + { + if( pathGapSum( per[u] ) == 0 ) + { + continue; + } + for( std::size_t k = 0; k < kPathGapKindCount; ++k ) + { + out.totals[k] += per[u][k]; + } + out.syms.push_back( PathGapSym{ u, depth[u], per[u] } ); + } + std::ranges::sort( out.syms, []( const PathGapSym& a, const PathGapSym& b ) + { + if( a.depth != b.depth ) + { + return a.depth < b.depth; + } + const std::size_t sa = pathGapSum( a.counts ), sb = pathGapSum( b.counts ); + if( sa != sb ) + { + return sa > sb; + } + return a.id < b.id; + } ); + ENSURES( out.any() == !out.syms.empty(), "a non-zero total is carried by at least one cone symbol, and only then" ); + return out; +} + +// gaps="declined:D,unresolved:U,value:V,through:T" — zero entries left out, table order. +inline std::string pathGapCountsValue( const PathGapCounts& c ) +{ + std::string v; + for( std::size_t k = 0; k < kPathGapKindCount; ++k ) + { + if( c[k] > 0 ) + { + v += ( v.empty() ? "" : "," ) + std::string( kPathGapKindNames[k] ) + ":" + std::to_string( c[k] ); + } + } + return v; +} + +// The legend sentence, carried only by an answer that prints the clause (absent otherwise: byte-identical). +inline constexpr std::string_view kPathGapsLegend = + "searched=N gaps= gap_syms=: reachable=0 but the search is INCOMPLETE — the N symbols reachable from from= over resolved call edges hold calls the graph " + "keeps no edge for (gaps= counts them: declined = several candidates and nothing chose one, unresolved = an in-repo name every definition of which was " + "filtered out, value = a function stored or passed as a value and not otherwise reached, through = a call through a parameter or slot), so this is NOT " + "proof that no path exists. rows are the symbols that carry them, nearest to from= first, gap_syms_capped=1 = more than shown; a row " + "is where the search could not see, never a hop. Not counted: ambiguous calls (every candidate has an edge the search followed) and calls to names " + "defined nowhere in the tree. next= reads the rows' bodies. "; +inline std::string_view pathGapsLegend( bool on ) noexcept +{ + return on ? kPathGapsLegend : std::string_view(); +} + +struct PathGapsXml +{ + std::string rootAttrs; // searched= gaps= gap_syms= [gap_syms_capped=] hint= next= — replaces the old hint + std::string rows; // the rows, the path element's only children when reachable="0" +}; + +// `rootPrefix` empty ⇒ p= stays as indexed (multi-root), exactly as the hop rows spell it. `connectSuggestion` is the +// transport's own spelling of the undirected follow-up ("--connect=A,B" on the CLI, "the connect verb on A,B" on MCP); +// `hintTail` is the CLI's existing several-defs clause (already escaped text), "" on MCP — both as the no-gap hint has them. +inline PathGapsXml pathGapsXml( const IngestResult& ing, const PathSearchGaps& gaps, bool singleRoot, std::string_view rootPrefix, + std::string_view connectSuggestion, std::string_view hintTail ) +{ + EXPECTS( gaps.any(), "the gap clause is emitted only for a search that met a gap" ); + std::vector esc; + const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; + const std::size_t shown = std::min( gaps.syms.size(), kPathGapRows ); + + PathGapsXml out; + std::string next = "--expand="; + for( std::size_t i = 0; i < shown; ++i ) + { + const Symbol& s = ing.symbols[ gaps.syms[i].id ]; + const std::string_view rp = singleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], rootPrefix ) : std::string_view( ing.files[ s.fileId ] ); + out.rows += ""; + // file:name, the selector spelling --expand resolves to this one definition (the same p= the row prints) + next += ( i == 0 ? "" : "," ) + std::string( rp ) + ":" + s.name; + } + out.rootAttrs = " searched=\"" + std::to_string( gaps.searched ) + "\" gaps=\"" + pathGapCountsValue( gaps.totals ) + "\" gap_syms=\"" + + std::to_string( gaps.syms.size() ) + "\"" + ( gaps.syms.size() > shown ? " gap_syms_capped=\"1\"" : "" ) + + " hint=\"no path through resolved call edges, but the search is incomplete: it met calls with no edge (gaps=) — read the gap rows, or try " + + ex( connectSuggestion ) + " (undirected: finds a shared caller)" + std::string( hintTail ) + "\"" + nextAttrXml( next ); + return out; +} + +} // namespace rw diff --git a/src/serialize.h b/src/serialize.h index 90182aa9a..72a257975 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2368,11 +2368,11 @@ inline constexpr std::string_view kModScopeMapLegend = // honesty-small-068: the three Go named-type kinds' reading, appended ONLY when the corpus has one (the modscope clause's // byte-identity rule: a corpus without such a row keeps every byte of this legend). What it does NOT mean: t=type is not // "a struct" (its underlying form is not a struct/interface/func, or is another named type the parse cannot see into), -// and only Go is split — another language's typedef/alias/enum still reads t=struct. +// and only Go is split — another language's typedef/alias/enum still reads t=struct (said in model.h and the gate's +// arm F, not in every Go answer: the clause rides each Go map, so it carries only what a Go row needs). inline constexpr std::string_view kNamedTypeMapLegend = - ""; + ""; // EXTENT HONESTY (src/extentsuspect.h, gate test/extentcheck.sh) — the ONE reading of extent_suspect= on every ROW // surface: the map's , a bundle's and . Written only into a document that carries the attribute, right diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 62d43e5d4..f3719902d 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2155,10 +2155,18 @@ std::optional runPath( const MainDispatch& d ) // same brief sentence, on both transports (mcpverbs.h path_between mirrors this line). // Reference-as-value round: with no directed call path, how often to= is used as a VALUE — a run through such a // slot is not a hop the graph can show, so the count is the clue (the callers verb lists the binding sites). - const std::size_t pthToValueRefs = rw::toValueRefsCount( ing, path.empty(), dstDefs ); + // PATH-GAP (src/pathgaps.h): an unreached to= is "no path" only when the search met no call the graph holds no edge + // for; otherwise the answer says the search was incomplete and names where. One ValueRefIndex serves both counts. + std::optional pthValueIdx; + if( path.empty() ) + { + pthValueIdx.emplace( ing ); + } + const std::size_t pthToValueRefs = path.empty() ? rw::toValueRefsCount( ing, true, dstDefs, &*pthValueIdx ) : 0; + const rw::PathSearchGaps pthGaps = path.empty() ? rw::pathSearchGaps( ing, g, srcDefs, *pthValueIdx ) : rw::PathSearchGaps{}; rw::emitTo( stdout, "{}", rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ).c_str(), - rw::toValueRefsLegend( pthToValueRefs > 0 ), + "graph holds none. {}{}{}{}-->{}", rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ).c_str(), + rw::toValueRefsLegend( pthToValueRefs > 0 ), rw::pathGapsLegend( pthGaps.any() ), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); rw::emitTo( stdout, " runPath( const MainDispatch& d ) rw::emitTo( stdout, "{}", rw::countAttrXmlOrEmpty( "to_value_refs", pthToValueRefs ) ); // absent at zero: byte-identical otherwise // P2.10: a dead end is exactly the moment to name the next verb. --path is DIRECTED; --connect searches // undirected and finds the shared-caller join a directed walk can never see. - if( path.empty() ) + const char* const pthDefsTail = ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : ""; + std::string pthGapRows; + if( pthGaps.any() ) + { + const rw::PathGapsXml gx = rw::pathGapsXml( ing, pthGaps, pthSingleRoot, pthRootPrefix, + "--connect=" + std::string( srcN ) + "," + std::string( dstN ), pthDefsTail ); + rw::emitTo( stdout, "{}", gx.rootAttrs ); + pthGapRows = gx.rows; + } + else if( path.empty() ) { rw::emitTo( stdout, " hint=\"no directed call path — try --connect={},{} (undirected: finds a shared caller), or --uses/--impact for non-call references{}\"", - ex( srcN ).c_str(), ex( dstN ).c_str(), - ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : "" ); + ex( srcN ).c_str(), ex( dstN ).c_str(), pthDefsTail ); } - rw::emitTo( stdout, ">" ); + rw::emitTo( stdout, ">{}", pthGapRows ); for( NodeId n : path ) { const Symbol& s = ing.symbols[n]; diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh new file mode 100755 index 000000000..fa4a78eb2 --- /dev/null +++ b/test/pathgapcheck.sh @@ -0,0 +1,309 @@ +#!/usr/bin/env bash +# pathgapcheck.sh — a reachable="0" path answer never claims "no path" when its search met a call the graph holds no +# edge for (src/pathgaps.h). +# +# THE DEFECT. --path=A,B answered `reachable="0" hint="no directed call path"` whenever the BFS over resolved call edges +# did not reach B — also when that search had walked past a DECLINED call (several same-language candidates, none +# local, nothing chose one), an UNRESOLVED call, a function handed off as a VALUE (`.then( handleResponse )`) or a call +# THROUGH a parameter. Any of those may be the hop that joins A to B, so the answer's wholeness claim was false. +# +# THE CONTRACT. When the search cone met such a call: searched= gaps= gap_syms= on the root, rows +# (nearest from= first, at most 3, gap_syms_capped="1" past that), a hint that says the search is incomplete, a next= +# that reads the rows' bodies — and never the text "no directed call path". When the cone met none, the old answer +# stands byte for byte. Ambiguous calls are NOT a gap (every candidate has an edge the search follows). The MCP twin +# (path_between) carries the same clause. +# +# ARMS +# (P1) declined: a platform-split callee (two same-language defs in sibling dirs, none in the caller's) — gaps=declined. +# (P2) value: a callback handed to .then() — gaps=value, the row names the function that hands it off. +# (P3) through: a call through a parameter — gaps=through. +# (P4) unresolved: a call whose only same-named definition is in another language — gaps=unresolved. +# (P5) runaway guard: 6 gap symbols — 3 rows, gap_syms="6", gap_syms_capped="1", nearest first. +# (P6) next= is pasteable: running it serves the gap row's body. +# (N1) a fully resolved graph with truly no path keeps "no directed call path" and no gap attribute. +# (N2) a gap OUTSIDE the search cone (a declined call reachable only from elsewhere) does not count. +# (N3) ambiguous-only cone (a k-way split the search followed) with no path keeps "no directed call path". +# (N4) a found path (reachable="1") through a cone with gaps carries no gap clause. +# (N5) a function passed as a value that the search ALSO reaches by a call is not a gap. +# (M) MCP path_between carries the same searched/gaps/gap_syms/rows/next as the CLI, and the same no-gap hint shape. +# +# Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } + +# ── fixtures ──────────────────────────────────────────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/decl/core" "$TMP/decl/linux" "$TMP/decl/darwin" "$TMP/val" "$TMP/thr" "$TMP/unr" "$TMP/clean" \ + "$TMP/out/core" "$TMP/out/linux" "$TMP/out/darwin" "$TMP/amb" "$TMP/many/core" "$TMP/many/linux" "$TMP/many/darwin" \ + "$TMP/found" "$TMP/valin" +cat > "$TMP/decl/core/machine.c" <<'EOF' +void Machine_scan( void ); +void Machine_run( void ) { Machine_scan(); } +EOF +cat > "$TMP/decl/linux/scan.c" <<'EOF' +void Process_update( void ) { } +void Machine_scan( void ) { Process_update(); } +EOF +cat > "$TMP/decl/darwin/scan.c" <<'EOF' +void Machine_scan( void ) { } +EOF +cat > "$TMP/val/app.js" <<'EOF' +function respond(ctx) { return ctx; } +function handleResponse(ctx) { return respond(ctx); } +function runMiddleware(ctx) { return Promise.resolve(ctx); } +function handleRequest(ctx) { return runMiddleware(ctx).then(handleResponse); } +function listen() { return handleRequest({}); } +module.exports = { listen }; +EOF +cat > "$TMP/thr/t.py" <<'EOF' +def target(): + return 1 + +def apply(fn): + return fn() + +def start(): + return apply(None) + +def wire(): + return apply(target) +EOF +cat > "$TMP/unr/main.py" <<'EOF' +def finish(): + return 0 + +def begin(): + return render_page() +EOF +cat > "$TMP/unr/render.rb" <<'EOF' +def render_page + finish +end +EOF +cat > "$TMP/clean/c.c" <<'EOF' +static int d( void ) { return 1; } +static int c( void ) { return d(); } +static int b( void ) { return 2; } +int a( void ) { return b(); } +int z( void ) { return c(); } +EOF +# N2: the declined call lives in other(), which only elsewhere() reaches; start() -> helper() is fully resolved. +cat > "$TMP/out/core/m.c" <<'EOF' +void scan( void ); +static int sink( void ) { return 1; } +static int helper( void ) { return 2; } +int start( void ) { return helper(); } +int other( void ) { scan(); return 0; } +int elsewhere( void ) { return other() + sink(); } +EOF +cat > "$TMP/out/linux/s.c" <<'EOF' +void scan( void ) { } +EOF +cat > "$TMP/out/darwin/s.c" <<'EOF' +void scan( void ) { } +EOF +# N3: two same-file definitions of one method name: obj.process() splits over both (ambiguous, every arm an edge). +cat > "$TMP/amb/a.py" <<'EOF' +class A: + def process(self): + return 1 + +class B: + def process(self): + return 2 + +def unrelated(): + return 3 + +def run(obj): + return obj.process() +EOF +# P5: six functions each with a declined call (f1..f4, mid, root), all reachable from root(). +cat > "$TMP/many/core/m.c" <<'EOF' +void scan( void ); +static int end_( void ) { return 0; } +int f1( void ) { scan(); return 1; } +int f2( void ) { scan(); return 2; } +int f3( void ) { scan(); return 3; } +int f4( void ) { scan(); return 4; } +int mid( void ) { scan(); return f3() + f4(); } +int root( void ) { scan(); return f1() + f2() + mid(); } +EOF +cat > "$TMP/many/linux/s.c" <<'EOF' +void scan( void ) { } +EOF +cat > "$TMP/many/darwin/s.c" <<'EOF' +void scan( void ) { } +EOF +# N4: a real path through a cone that also holds a declined call. +mkdir -p "$TMP/found/core" "$TMP/found/linux" "$TMP/found/darwin" +cat > "$TMP/found/core/m.c" <<'EOF' +void scan( void ); +static int goal( void ) { return 1; } +int step( void ) { scan(); return goal(); } +int go( void ) { return step(); } +EOF +cat > "$TMP/found/linux/s.c" <<'EOF' +void scan( void ) { } +EOF +cat > "$TMP/found/darwin/s.c" <<'EOF' +void scan( void ) { } +EOF +# N5: handleResponse is passed as a value AND called directly, so the search reaches it anyway. +cat > "$TMP/valin/app.js" <<'EOF' +function other() { return 1; } +function handleResponse(ctx) { return ctx; } +function runMiddleware(ctx) { return Promise.resolve(ctx); } +function handleRequest(ctx) { handleResponse(ctx); return runMiddleware(ctx).then(handleResponse); } +module.exports = { handleRequest, other }; +EOF + +# One --path answer, run unpiped so rc is ripwire's; the head and the rest are read off the saved file. +path(){ # $1 dir, $2 FROM,TO, $3 out file + "$BIN" "$1" --no-cache --path="$2" > "$3" 2>&1 +} +attr(){ # $1 file, $2 attribute name -> its value on the root (empty if absent) + python3 - "$1" "$2" <<'PY' +import re, sys +t = open(sys.argv[1], encoding="utf-8", errors="replace").read() +m = re.search(r']*>', t) +if not m: sys.exit(0) +a = re.search(r'\s' + re.escape(sys.argv[2]) + r'="([^"]*)"', m.group(0)) +print(a.group(1) if a else "") +PY +} +rows(){ grep -o ']*/>' "$1"; } +premise(){ # $1 label, $2 rc, $3 file — the answer ran and printed a root (checklist 14: an absence is read only off a real answer) + if [ "$2" -ne 0 ] || ! grep -q ' root: $( head -c 300 "$3" )"; return 1; fi + return 0 +} +gapArm(){ # $1 label, $2 dir, $3 FROM,TO, $4 expected gaps=, $5 expected first row name + local f="$TMP/$( echo "$1" | tr -cd 'A-Za-z0-9' ).xml" rc + path "$2" "$3" "$f"; rc=$? + premise "$1" "$rc" "$f" || return + local g; g="$( attr "$f" gaps )" + local first; first="$( rows "$f" | head -1 | sed 's/.* n="\([^"]*\)".*/\1/' )" + if [ "$( attr "$f" reachable )" = "0" ] && [ "$g" = "$4" ] && ! grep -q 'no directed call path' "$f" \ + && [ -n "$( attr "$f" searched )" ] && [ "$first" = "$5" ] && grep -q 'search is incomplete' "$f"; then + ok "$1 reachable=0, gaps=\"$g\", first row $first, no \"no directed call path\"" + else + no "$1 expected gaps=\"$4\" first row $5 and no 'no directed call path'; got gaps=\"$g\" first=[$first]: $( grep -o ' "$TMP/next.xml" 2>&1; rc=$? + if [ "$rc" -eq 0 ] && grep -q 'then(handleResponse)' "$TMP/next.xml"; then + ok "(P6) next=\"$nx\" serves handleRequest's body, where the value hand-off is" + else + no "(P6) next=\"$nx\" rc=$rc did not serve the body: $( head -c 300 "$TMP/next.xml" )" + fi +else + no "(P6) next= on the value arm is [$nx], expected --expand=app.js:handleRequest" +fi + +echo "(N) near-miss negatives" +cleanArm "(N1) fully resolved, no path" "$TMP/clean" "a,d" +cleanArm "(N2) a gap outside the cone" "$TMP/out" "start,sink" +f="$TMP/ambprobe.xml"; "$BIN" "$TMP/amb" --no-cache > "$f" 2>&1 +if grep -q ']*n="run"[^>]*amb="1"' "$f"; then + ok "(N3) premise: run's obj.process() is an ambiguous split (amb=1 on its row)" + cleanArm "(N3) ambiguous-only cone" "$TMP/amb" "run,unrelated" +else + no "(N3) premise: run's call is not an ambiguous split here, the arm would assert nothing: $( grep -o ']*n="run"[^>]*>' "$f" )" +fi +f="$TMP/found.xml"; path "$TMP/found" "go,goal" "$f"; rc=$? +if premise "(N4)" "$rc" "$f"; then + if [ "$( attr "$f" reachable )" = "1" ] && [ -z "$( attr "$f" gaps )" ] && [ -z "$( attr "$f" hint )" ] && [ -z "$( rows "$f" )" ]; then + ok "(N4) a found path carries no gap clause and no hint" + else + no "(N4) found path: $( grep -o ' 0: + resp[ o[ "id" ] ] = "".join( c.get( "text", "" ) for c in o.get( "result", {} ).get( "content", [] ) ) +def head( t ): + m = re.search( r']*>', t ) + return m.group( 0 ) if m else "" +def pick( t, name ): + m = re.search( r'\s' + name + r'="([^"]*)"', head( t ) ) + return m.group( 1 ) if m else None +bad = 0 +for i, ( d, a, b, gap ) in enumerate( cases ): + mcp = resp.get( i + 1, "" ) + cli = subprocess.run( [ binp, tmp + "/" + d, "--no-cache", "--path=%s,%s" % ( a, b ) ], capture_output = True, text = True ).stdout + if not head( mcp ): + print( " MCP gave no root for", d, repr( mcp[:300] ), p.stderr[:300] ); bad = 1; continue + for name in ( "searched", "gaps", "gap_syms", "gap_syms_capped", "next", "reachable" ): + if pick( mcp, name ) != pick( cli, name ): + print( " %s: %s= MCP %r vs CLI %r" % ( d, name, pick( mcp, name ), pick( cli, name ) ) ); bad = 1 + if re.findall( r']*/>', mcp ) != re.findall( r']*/>', cli ): + print( " %s: gap rows differ" % d ); bad = 1 + if gap and ( "no directed call path" in mcp or "search is incomplete" not in mcp ): + print( " %s: MCP hint still claims no path" % d ); bad = 1 + if not gap and "no directed call path" not in mcp: + print( " %s: MCP lost the plain no-path hint" % d ); bad = 1 +sys.exit( bad ) +PY + +if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAIL"; fi +exit "$fail" diff --git a/test/regression.sh b/test/regression.sh index 2addbf6d8..79564c397 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 26a3abff575fe5ca207a57e56064deb5ca258daa Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 17:21:21 -0400 Subject: [PATCH 050/229] test: rubysettercheck counts symbol rows, not a map callee row Co-Authored-By: Claude Opus 5.5 --- test/rubysettercheck.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/rubysettercheck.sh b/test/rubysettercheck.sh index 80a51f88e..3de567ff5 100755 --- a/test/rubysettercheck.sh +++ b/test/rubysettercheck.sh @@ -119,7 +119,9 @@ if grep -q 'n="count=" sc="W"' "$SPLIT"; then ok "def count=(v) → W::count="; if grep -q 'n="limit=" sc="W"' "$SPLIT"; then ok "def self.limit=(v) → W::limit= (singleton setter)"; else no "def self.limit=(v) not indexed"; fi if grep -q 'n="name=" sc="Other"' "$SPLIT"; then ok "Other#name= indexed separately"; else no "Other#name= not indexed"; fi if grep -q ' callee row also has `n="name" ` followed by a space, +# and it is a call, not a definition — the count this arm makes is of definitions named `name`. +if [ "$( grep -c '^]*n="name" ' "$SPLIT" )" -eq 1 ]; then ok "exactly one symbol named name (the getter)"; else no "expected one getter row, got $( grep -c '^]*n="name" ' "$SPLIT" )"; fi echo "=== a setter CALL edges to the setter, never to the getter ===" WR="$( rowOf 'n="writer" ' )" From 8409b451c691fd5d60784a90c6092ee4bbee85f2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 17:27:55 -0400 Subject: [PATCH 051/229] docs: CHANGELOG for the Go kinds and the path gaps; dictv from the binary CHANGELOG [Unreleased] gains the two Fixed entries. The session legend dictionary is dictv=cbadf4aa6acda0c2 entries=787 (build/ripwire --legend-dict: 78390 B on 788 raw lines): the CHANGELOG sentence, the showcase capture's dictv and display lines by hand, docs/COMMANDS.md via docs/docs_commands_build.py. docscommandscheck, legenddriftcheck. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 26 ++++++++++++++++++- docs/COMMANDS.md | 2 +- docs/captures/COMMANDS_showcase_2026-09-14.md | 4 +-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..96eb3d8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,30 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — `--path` no longer says "no directed call path" when its search met a call the graph has no edge for + +`--path=A,B` and the MCP `path_between` verb answered `reachable="0" hint="no directed call path"` whenever the search over +resolved call edges did not reach `B`, even when that search had passed a call the graph keeps no edge for: a call the +resolver declined (several candidates, none local, nothing chose one), an unresolved call, a function handed off as a +value (`.then( handleResponse )`), or a call through a parameter. Any of those can be the missing hop. Such an answer now +says the search was incomplete: `searched=` (the symbols it reached), `gaps="declined:D,unresolved:U,value:V,through:T"`, +`gap_syms=` with up to three `` rows (nearest `from=` first, `gap_syms_capped="1"` past that) and a +`next=` that expands their bodies. A `` row is where the search could not see, never a hop. Ambiguous calls are not +counted (every candidate has an edge the search follows), nor are calls to names defined nowhere in the tree. When the +search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. Gate: +`test/pathgapcheck.sh`. + +### Fixed — a Go named type's `t=` says what it is: `type`, `functype` or `alias`, not `struct` + +Every Go `type_spec` was labelled `t="struct"`, so `type TestName string` and `type F func(...)` read as structs, and +`type A = B` was not indexed at all. The kind now follows the written form: a struct is `t="struct"`, an interface +`t="iface"`, a func type `t="functype"`, an alias `t="alias"` (now indexed), and every other defined type (`string`, +`[]T`, `map`, `chan`, a pointer, an array, another named type, a generic over any of these) `t="type"`. Grouped +`type ( … )` blocks and generic types follow the same rule. The three new kinds behave as `struct` did everywhere else (a +conversion `TestName( s )` keeps its caller edge), `--graph-query`'s `kind()` accepts them, and the map's `--legend=full` +gains a clause for them only on a tree that has one. Only Go is split: other languages' typedefs, aliases and enums still +read `t="struct"`. `kParserVer` 143 → 145, so every ingest cache is re-indexed once. Gate: `test/gokindcheck.sh`. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) The MCP `grep` hit row was `{file, line, in}`: no matched text, so an agent re-read every file it had just searched, and @@ -521,7 +545,7 @@ above 141's full-use file tag 142, so no cache such a build wrote is read as thi (the function-literal fix's record changes, then the false-edge fix's member-call fields) and `kQSnapCacheScheme` 15 → 17 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions held as values). Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=04d7833c60d2d5bd entries=784`. +cached quality snapshot is recomputed. The session legend dictionary is `dictv=cbadf4aa6acda0c2 entries=787`. ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..44de47dad 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4398,7 +4398,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=cbadf4aa6acda0c2 entries=787 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..dc34f9c8f 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -5351,7 +5351,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=cbadf4aa6acda0c2 entries=787 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5381,7 +5381,7 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [755 more display lines; full output is 77894 bytes on 785 raw line(s)] +… [758 more display lines; full output is 78390 bytes on 788 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` From 4a1bbc82783b4a106cf5ed787f735b0246410995 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:07:48 -0400 Subject: [PATCH 052/229] fix(count-floor): present-only full grep floor clause; shorter safe-delete readings; re-pin safe-delete compact legend compactlegendcheck (A-PIN) pinned the full --grep legend byte for byte: the callers_floor= sentence rode every full answer. It is now spliced only when a row on the page carries the marker (rows and floors computed once, before the legend), so a page without one is byte-identical to before. compactlegendcheck (U): the safe-delete compact legend grew past its 720 B pin because every caller row now carries sites_l= and its reading must ride with it. The four new readings are shortened; the probe measures 805 B (715 on the base). Re-pinned 720 -> 820 with the attribution comment the table's rule asks for; no other schema moved. Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 8 +++--- src/verbs_grep.h | 51 +++++++++++++++++++++++--------------- test/compactlegendcheck.sh | 8 +++++- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index d331d43aa..104b30465 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -837,10 +837,10 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "callers_floor", "callers_floor=1: callers= may be short for this row: a declined or unbound call spelled like it, a value use, or a kind used by reading/naming it (var, class, struct, interface); not proof more callers exist; absent = no such evidence", true, "enc", MapHeaderRead::No, {}, "grep" }, { "floor_next", "floor_next=: the call that lists what that count could not see (the uses verb; the literal scan where reads are not indexed)", true, "enc", MapHeaderRead::No, {}, "grep" }, // count-floor + CALLSITE-LINE on safe-delete (src/verbs_navigate.h runSafeDelete): root markers beside the counts they qualify. - { "callers_floor", "callers_floor=1: callers= and impact_reaches= may be short: a declined or unbound call spelled like sym=, a value use, or a kind used by reading/naming it; not proof more exist", false, {}, MapHeaderRead::No, {}, "safe-delete" }, - { "uses_floor", "uses_floor=1: uses= cannot see this kind's reads or type mentions in this run; a zero there is not a count of its uses", false, {}, MapHeaderRead::No, {}, "safe-delete" }, - { "risk", "risk=unmodelled: nothing found, but the counts are blind to this kind's uses; no reading either way, next= lists them", false, "safe-delete", MapHeaderRead::No, "unmodelled", "safe-delete" }, - { "sites_l", "c sites_l=: that caller's call-site LINES spelled like sym= (p= is where the caller is defined); not proof each binds here", true, "c", MapHeaderRead::No, {}, "safe-delete" }, + { "callers_floor", "callers_floor=1: callers= and impact_reaches= may be short (a declined, unbound or value use, or a kind read not called); not proof of more", false, {}, MapHeaderRead::No, {}, "safe-delete" }, + { "uses_floor", "uses_floor=1: this kind's reads and type mentions are not indexed in this run; uses= does not count them", false, {}, MapHeaderRead::No, {}, "safe-delete" }, + { "risk", "risk=unmodelled: nothing found, but the counts cannot see this kind's uses; next= lists them", false, "safe-delete", MapHeaderRead::No, "unmodelled", "safe-delete" }, + { "sites_l", "c sites_l=: its call-site lines (p= is the caller's def line); not proof each binds here", true, "c", MapHeaderRead::No, {}, "safe-delete" }, // count-floor on the targeted lego verb (serialize.h packLego, countfloor.h implementorFloors): present-only. { "implementors_floor", "implementors_floor=1: an extends clause spelled like this interface bound nowhere; implementors= may be short (not proof of another)", true, "iface", MapHeaderRead::No, {}, "lego" }, { "floor_next", "floor_next=: lists every extends site of the name, bound or not", true, "iface", MapHeaderRead::No, {}, "lego" }, diff --git a/src/verbs_grep.h b/src/verbs_grep.h index ecb260520..ee9285689 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -102,22 +102,12 @@ void emitGrepHandleLegend( bool enabled ) "means no content hash could be proven. -->" ); } -void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span hits, - const GrepEncOptions& opt ) +void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span rows, + std::span floors, const GrepEncOptions& opt ) { using namespace rw; GrepHandleAttrs handleAttrs( ing, g, opt.handles, opt.root ); - const std::vector rows = grepEnclosingRows( ing, g, hits ); - // count-floor: callers= beside evidence of a miss for THAT row is a floor (countfloor.h); the MCP twin calls the same - // function over the same rows (mcpverbs.h grepEnclosingJson), so the two surfaces cannot disagree. - std::vector> rowIds; - rowIds.reserve( rows.size() ); - for( const GrepEncRow& row : rows ) - { - rowIds.push_back( row.ids ); - } - const std::vector floors = callerFloors( ing, g, rowIds ); - ENSURES( floors.size() == rows.size() ); + EXPECTS( floors.size() == rows.size() ); // emitGrepReport computes both from the same page (grepEncFloors) for( std::size_t r = 0; r < rows.size(); ++r ) { const GrepEncRow& row = rows[r]; @@ -160,6 +150,28 @@ void emitGrepEncRows( const rw::IngestResult& ing, const rw::Graph& g, std::span } } +// count-floor: callers= beside evidence of a miss for THAT row is a floor (countfloor.h). Computed once per page, before +// the legend, so the full dialect defines callers_floor= exactly when a row carries it; the MCP twin calls the same +// function over the same rows (mcpverbs.h grepEnclosingJson), so the two surfaces cannot disagree. +std::vector grepEncFloors( const rw::IngestResult& ing, const rw::Graph& g, std::span rows ) +{ + std::vector> rowIds; + rowIds.reserve( rows.size() ); + for( const rw::GrepEncRow& row : rows ) + { + rowIds.push_back( row.ids ); + } + return rw::callerFloors( ing, g, rowIds ); +} + +// count-floor (countfloor.h): the full dialect's reading of a row's own floor marker — no attribute=value literal (this +// legend's rule) — spliced only when a row on the page carries it, so a page without one keeps every byte. +inline constexpr const char* kGrepEncFloorClause = + "callers_floor= (value 1, present only on such a row) marks a count the index holds evidence of a miss for: a declined or " + "unbound call spelled like the name, a use as a value, or a kind used by reading or naming it (a variable, a class, an " + "interface); it is not proof more callers exist, and its absence means the index holds no such evidence. floor_next= is " + "the call that lists what that count could not see (the uses verb, or this literal scan where reads are never indexed); "; + // R1a emission, lifted out of emitGrepReport for the same reason. What to suggest is // search.h's grepZeroHitSuggestions (shared with the MCP twin); this is pure serialization. void emitGrepSuggest( const rw::IngestResult& ing, const std::string& pat, bool regex, std::vector& esc ) @@ -684,6 +696,9 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw const PageWindow grepPage = pageWindow( hitCount, rowCap, cfg.pageOffset ); const std::vector hits = grepEnrich( ing, std::span( found.raw ).subspan( grepPage.begin, grepPage.end - grepPage.begin ), cfg.grepBefore, cfg.grepAfter ); + const std::vector encRows = grepEnclosingRows( ing, g, std::span( hits ) ); // R1b, before the legend + const std::vector encFloors = grepEncFloors( ing, g, encRows ); + const bool anyEncFloor = std::ranges::any_of( encFloors, []( const CallerFloor& f ) { return f.isFloor; } ); std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; // CDATA-safe a context block: split any ]]> (would prematurely close the CDATA) and scrub XML-illegal @@ -831,11 +846,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // parse this verb's header counters by grep, and a quoted numeric example here would be matched first — the quality-delta legend's rule. "After the hit rows, rows list each DISTINCT enclosing symbol NAME of THIS page (first-appearance order, bounded by the page) with callers= its 1-hop DISTINCT-caller count, " "unioned across same-named defs like the callers verb (a FLOOR — dynamic dispatch contributes no edge), defs= how many defs the name grouped (only when more than one), cx= complexity; " - // count-floor (countfloor.h): the row's own marker, no attribute=value literal (this legend's rule). - "callers_floor= (value 1, present only on such a row) marks a count the index holds evidence of a miss for: a declined or " - "unbound call spelled like the name, a use as a value, or a kind used by reading or naming it (a variable, a class, an " - "interface); it is not proof more callers exist, and its absence means the index holds no such evidence. floor_next= is " - "the call that lists what that count could not see (the uses verb, or this literal scan where reads are never indexed); " + "{}" // count-floor: kGrepEncFloorClause, present-only (a row on this page carries callers_floor=) "amp=/tested= join only when a metrics co-run already computed that lens. On a zero-hit answer a element may follow: SUGGESTIONS, never matches — near= the nearest indexed " "symbol name (did-you-mean), next= a ready-to-paste conceptual fallback; absent for regex/non-word-like patterns or when nothing plausible exists. " // T1: the completeness claim, defined where it appears (no attribute=value literal in these @@ -888,7 +899,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // P3 (L7): next= on the root, defined where the reader meets it "next= is the one pasteable follow-up: the at verb on the top hit; the next page (compact legend) when cut; " "the conceptual lens on a zero-hit answer. " - "{} -->", rw::kPageRaiseCapClause ); + "{} -->", anyEncFloor ? kGrepEncFloorClause : "", rw::kPageRaiseCapClause ); } emitGrepHandleLegend( cfg.grepHandles ); // G3: terms=/scope=/suppressed= — only when AND/NOT was actually given, so a plain --grep answer @@ -1029,7 +1040,7 @@ int emitGrepReport( const rw::Config& cfg, const rw::IngestResult& ing, const rw // ── R1b: the block — the map's context on the answer, no second call (helper above) ────── const GrepEncOptions encOpt{ amp, tested, cfg.grepHandles, cfg.roots.size() == 1 ? cfg.roots[0] : std::string_view(), esc }; - emitGrepEncRows( ing, g, std::span( hits ), encOpt ); + emitGrepEncRows( ing, g, encRows, encFloors, encOpt ); // ── R1a: the zero-hit follow-up — suggestions, labeled as such, never matches (helper above) ─── // §R-J: also suppressed when the AUX block found the pattern — a real hit in queries/cpp/tags.scm is not diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 9a103f962..efb51e3ed 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -500,6 +500,12 @@ probeFor() # --impact=distance probe; 797 on the base binary b343b988). The answer now carries by_depth= on the root and d= on its # first row (the listing runs nearest first, graph.h orderByDepthThenRank), and the compact legend reads both: the # present-only row (+64 B with its separator) and by_depth= (+63 B). Both are absent at reaches="0". No other schema moved. +# RE-PINNED 2026-10-04 (lane count-floor-068, CALLSITE-LINE): ripwire.safe-delete/v1 720 -> 820 (measured 805, the +# --safe-delete=distance probe; 715 on the base binary 255dc199). Each caller row now carries sites_l=, its call-site LINES +# beside p= (the caller's definition line): an edit decision needs the lines to open, and graded answers that named every +# caller were scored wrong for printing only definition lines. The compact legend reads it (+90 B with its separator: what +# it is, and that it is not proof each line binds to this definition). callers_floor=/uses_floor=/risk=unmodelled are +# present-only and ride neither this probe nor any other pinned one. No other schema moved. # RE-PINNED BACK 2026-09-30 (train22 fixups, review M2): ripwire.scan-skills/v1 520 -> 380 (measured 369). An earlier commit on # this branch pinned 520 from the developer's own HOME (a Codex install's .py helpers made the answer carry # code_not_flow_scanned=); the gate now runs under an empty HOME of its own, so the probe measures the same bytes everywhere. @@ -566,7 +572,7 @@ ripwire.callers/v1 330 317 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 ripwire.batch/v1 250 238 -ripwire.safe-delete/v1 720 708 +ripwire.safe-delete/v1 820 805 ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 From 09c49f0d095d92f990b593bd1e2ab2038d22f343 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:10:01 -0400 Subject: [PATCH 053/229] map: withdraw the comma-joined callee list (deferred); bundleidcheck reads the --for zero spelling The map calls= join is withdrawn from this lane: about thirty language-resolution gates read map edges as rows and the map goldens pin them, and FE-B is adding a per-callee via= to the same rows, which changes which rows could join. It lands better after FE-B with a shared gate reader. Map output is byte-identical to base again; the compactlegendcheck map-family pins and the map.xml A-PIN are restored. bundleidcheck: on the --for answer an r= row without in= reads 0 (its legend says absent = 0, asserted); the in= agreement arms are unchanged. Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 2 -- src/serialize.h | 34 ++--------------------- test/bundleidcheck.sh | 13 ++++++++- test/compactlegendcheck.sh | 27 ++++++++---------- test/compactlegendfix/pre_l1_full/map.xml | 2 +- 5 files changed, 26 insertions(+), 52 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 38fe00584..09e79f54e 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -566,8 +566,6 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // ctxRootOpen) rides a bundle whose task was routed to a ranker: --pack-task always, MCP explore unless no_route; it is // ELEMENT-qualified on . compactlegendcheck (D36)/(D37). { "r", ": rank N in this ranking, rows in r= order", true, "d" }, - // lean-answers lane (serialize.h, the map's callee rows): present only where a row joined its callees. - { "calls", ": its rows, comma-joined", true, "s" }, { "ccx", ": cyclomatic/cognitive complexity", true, "d" }, { "in", ": N callers in the index (absent: not measured)", true, "d" }, { "amp", ": direct callers + files sharing a commit with its file (absent at 0)", true, "d" }, diff --git a/src/serialize.h b/src/serialize.h index 081d7706e..96bbdcaa3 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2766,8 +2766,8 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // written once the document it describes has been measured (PHASE 2 below) and the legend's own bytes // are part of what it describes. std::string legend = outProv - ? "" - : ""; + ? "" + : ""; // EXTENT HONESTY (src/extentsuspect.h): how many definitions carry extent_suspect= corpus-wide — the header's // extent_suspect_syms= — and the row + header readings, appended ONLY when that is non-zero, so a corpus with // nothing flagged keeps every byte of this legend. @@ -3383,36 +3383,6 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto std::string evRun = " ev=\"" + std::to_string( s.ev ) + "\" ev_floor=\"1\" ev_why=\"" + evWhyString( s ) + "\""; w.write( evRun ); } - // lean-answers lane: a symbol whose every callee row would be a BARE (no per-edge attribute, no - // comma in a name) says them once, comma-joined, as calls="a,b,c" on the row itself — the same names in - // the same order, ~11 B a callee less. One attributed edge (prov=, or any later per-callee hedge) keeps - // the rows for the whole symbol, so no per-callee attribute ever has to be dropped to join. - const bool joinCallees = outOff[id] < outOff[id + 1] && [ & ] - { - for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) - { - const bool attributed = outProv && e < outProv->size() && ( *outProv )[e]; - if( attributed || ing.symbols[ outTargets[e] ].name.find( ',' ) != std::string::npos ) - { - return false; - } - } - return true; - }(); - if( joinCallees ) - { - w.write( " calls=\"" ); - for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) - { - if( e > outOff[id] ) - { - w.write( "," ); - } - w.write( escapeXml( ing.symbols[ outTargets[e] ].name, esc ) ); - } - w.write( "\"/>" ); - continue; - } w.write( ">" ); for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) diff --git a/test/bundleidcheck.sh b/test/bundleidcheck.sh index 7ab9b500e..b99fb460e 100755 --- a/test/bundleidcheck.sh +++ b/test/bundleidcheck.sh @@ -107,10 +107,19 @@ def rows( path ): for m in re.finditer( r']*)>', text ): attrs = dict( re.findall( r'(\w+)="([^"]*)"', m.group( 1 ) ) ) if 'n' not in attrs or 'l' not in attrs: continue + # lean-answers lane: the --for lens omits a zero in= on an r= row and its legend says so ("absent cx/ccx/in = 0"), + # so on THAT answer an absent in= reads 0 — the same fact in the new spelling; pack-task still prints every in=. + if zeroAbsent and 'r' in attrs and 'in' not in attrs: attrs[ 'in' ] = '0' out[ ( attrs.get( 'p', '' ), attrs['l'], attrs['n'] ) ] = attrs.get( 'in' ) return out -a, b = rows( sys.argv[1] ), rows( sys.argv[2] ) +forText = open( sys.argv[1], encoding = 'utf-8' ).read() +if 'absent cx/ccx/in = 0' not in forText and 'each absent when 0' not in forText: + print( 'MISMATCH legend: the --for answer omits zeros without saying absent = 0' ) +zeroAbsent = True +a = rows( sys.argv[1] ) +zeroAbsent = False +b = rows( sys.argv[2] ) shared = sorted( set( a ) & set( b ) ) bad = [ ( k, a[k], b[k] ) for k in shared if a[k] != b[k] ] print( 'shared=%d' % len( shared ) ) @@ -150,6 +159,8 @@ else while read -r nm fp val; do [ -n "$nm" ] || continue ref="$( drows "$TMP/for.xml" | grep -oE "n=\"$nm\"[^>]* p=\"$fp\"[^>]* in=\"[0-9]+\"" | grep -oE 'in="[0-9]+"' | head -1 | tr -cd '0-9' )" + # lean-answers lane: a --for r= row with no in= is a 0 (its legend: absent cx/ccx/in = 0), not "no row" + [ -n "$ref" ] || { drows "$TMP/for.xml" | grep -qE "n=\"$nm\"[^>]* p=\"$fp\"[^>]* r=\"[0-9]+\"" && ref=0; } [ -n "$ref" ] || continue [ "$val" = "$ref" ] || { mism=$(( mism + 1 )); echo " mismatch $nm ($fp): trace=$val for=$ref"; } done <<< "$( drows "$TMP/trace.xml" | grep -oE 'n="[A-Za-z_][A-Za-z0-9_]*"[^>]* p="[^"]*"[^>]* in="[0-9]+"' \ diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 5c628af18..72af595d8 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -288,11 +288,9 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # outside-import/use,super-past-the-tree;...)" (was "(builtin/stdlib-name-...,external-import,...)"), +106 B — and the # est_tokens= that prices them, 858 -> 901 (header and root). Checked: the new output differs from the pin by exactly # those two clauses and that figure; the six other pins are unchanged. -# RE-ANCHORED BY HAND 2026-10-04 (lane/lean-answers-068), for.xml and map.xml: two lossless row spellings and the legend -# sentences that define them. for.xml: a --for lens row omits a zero cx=/ccx=/in= and the v1 legend says "each absent when -# 0;"; map.xml: a row whose callees are all bare carries them as calls="a,b" and the v1 header legend defines calls=. -# Checked mechanically: restoring the zeros (on every r= row) and the rows, and removing those two legend clauses, -# gives the previous pins byte for byte with at= and est_tokens= masked (est_tokens= prices the shorter bytes). The five +# RE-ANCHORED BY HAND 2026-10-04 (lane/lean-answers-068), for.xml only: a --for lens row omits a zero cx=/ccx=/in= and the +# v1 legend says "each absent when 0;". Checked mechanically: restoring the zeros on every r= row and removing that clause +# gives the previous pin byte for byte with at= and est_tokens= masked (est_tokens= prices the shorter bytes). The six # other pins are unchanged. PIN_DIR="$ROOT/test/compactlegendfix/pre_l1_full" # the one normalisation, in python on BOTH sides so no sed dialect decides it (BSD sed appends a final newline, GNU @@ -516,21 +514,18 @@ probeFor() # its definition as two contracts — so the schema had no XML answer to pin. It now answers about the definition. # the pins follow the definitions, measured + 10 rounded up to 10. # schema pin measured -# RE-PINNED 2026-10-04 (lane/lean-answers-068), three new definitions, each measured on this gate's fixture against the +# RE-PINNED 2026-10-04 (lane/lean-answers-068), two new definitions, each measured on this gate's fixture against the # base binary (255dc199) — no prose was added to any reading that already rode: -# the map family (map 892->935 at --top-k=3, map-diff 885->939, metrics 1021->1066, query 707->761, around 760->814, -# pack-top-n 745->799, pack-signatures 759->813): the present-only reading (+49 B with its separator) — a -# map row whose callees are all bare now carries them as one comma-joined attribute (~11 B a callee less in the rows). # ripwire.for/v1 738->761: " (absent cx/ccx/in = 0)" (+23 B) — the --for lens omits a zero cx=/ccx=/in=. # ripwire.whereis/v1 611->887: the default listing's readings — listing=, the element (and the # generic next= reading it pulls in), head_date= (the tip/date hoist) and complete='s "(of its listing=)" (+276 B). The # answer they ride is the lean one: its rows shrink from every reference to the definitions alone. PIN_TABLE=' ripwire.edit-check/v1 610 595 -ripwire.map/v1 950 935 -ripwire.map-diff/v1 955 939 -ripwire.pack-signatures/v1 830 813 -ripwire.metrics/v1 1080 1066 +ripwire.map/v1 910 892 +ripwire.map-diff/v1 900 885 +ripwire.pack-signatures/v1 770 759 +ripwire.metrics/v1 1040 1021 ripwire.deps/v1 780 764 ripwire.hotspots/v1 530 518 ripwire.clones/v1 810 799 @@ -571,12 +566,12 @@ ripwire.mentions/v1 260 243 ripwire.affected/v1 840 826 ripwire.verify/v1 440 421 ripwire.help-task/v1 440 429 -ripwire.query/v1 775 761 +ripwire.query/v1 720 707 ripwire.grep/v1 1380 1368 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 -ripwire.around/v1 830 814 +ripwire.around/v1 770 760 ripwire.callers/v1 330 317 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 @@ -590,7 +585,7 @@ ripwire.whereis/v1 900 887 ripwire.community/v1 730 719 ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 -ripwire.pack-top-n/v1 815 799 +ripwire.pack-top-n/v1 760 745 ripwire.expand/v1 280 265 ripwire.expand-file/v1 360 342 ripwire.for/v1 775 761 diff --git a/test/compactlegendfix/pre_l1_full/map.xml b/test/compactlegendfix/pre_l1_full/map.xml index 81b630175..01d2de816 100644 --- a/test/compactlegendfix/pre_l1_full/map.xml +++ b/test/compactlegendfix/pre_l1_full/map.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 173c947545d6b13a236a286f51cecb2beb9b8297 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:14:03 -0400 Subject: [PATCH 054/229] refactor: one unreached-path clause for both transports; kind tables over switches --quality-delta flagged the lane's own additions: pathText's complexity and verbosity grew past their bars (15 -> 17, 56 -> 65), and the three new kinds turned lsp.h's lspKindWord switch into a near-clone of model.h's langTag. Now: - pathgaps.h pathUnreachedXml writes the whole reachable="0" clause, the gap clause or the plain hint (byte-identical to before), for the CLI and MCP alike, from each transport's spelling of its follow-ups; - lspKindWord reads a table indexed by SymKind (sized by kSymKindCount); - isStructOrNamedType reads a bit table, as kCallableKindBits does; - PathSearchGaps::any and pathGapSum lean on std::accumulate. No output change: pathgapcheck and gokindcheck green. Co-Authored-By: Claude Opus 5.5 --- src/lsp.h | 30 ++++++++++--------------- src/mcpverbs.h | 18 ++++----------- src/model.h | 5 ++++- src/pathgaps.h | 53 ++++++++++++++++++++++++++------------------ src/verbs_navigate.h | 21 +++++------------- 5 files changed, 58 insertions(+), 69 deletions(-) diff --git a/src/lsp.h b/src/lsp.h index 114738178..be37adbda 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -35,6 +35,7 @@ #include "infra/os.h" // rw::os::realpath / getcwd / stat — the root-is-a-directory check at initialize, the launch-cwd base for relative index paths #include +#include #include #include #include @@ -366,26 +367,19 @@ inline int lspKindOf( rw::SymKind k ) noexcept return 13; // Variable — the neutral bucket } +// The hover word per SymKind, a declarative table in enum order (CONTRIBUTING §3's table-over-switch rule; it was a +// switch, and the three Go named-type kinds made it a near-clone of model.h's langTag switch). Sized by kSymKindCount, +// so an appended kind without a word does not compile. +inline constexpr std::array kLspKindWords = { + "function", "method", "class", "struct", "interface", "variable", "section", "macro", "field", + "symbol", // Other + "module scope", // ModuleScope + "type", "type alias", "function type" // NamedType, Alias, FuncType +}; inline const char* lspKindWord( rw::SymKind k ) noexcept { - switch( k ) - { - case rw::SymKind::Function: return "function"; - case rw::SymKind::Method: return "method"; - case rw::SymKind::Class: return "class"; - case rw::SymKind::Struct: return "struct"; - case rw::SymKind::Interface: return "interface"; - case rw::SymKind::Var: return "variable"; - case rw::SymKind::Section: return "section"; - case rw::SymKind::Macro: return "macro"; - case rw::SymKind::Field: return "field"; - case rw::SymKind::ModuleScope: return "module scope"; - case rw::SymKind::NamedType: return "type"; - case rw::SymKind::Alias: return "type alias"; - case rw::SymKind::FuncType: return "function type"; - case rw::SymKind::Other: break; - } - return "symbol"; + const std::size_t i = std::size_t( k ); + return i < kLspKindWords.size() ? kLspKindWords[i] : "symbol"; } // ─── file identity: a client URI → an index fileId ────────────────────────────────────────────────── diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 6b474a4b0..914130cc6 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3210,20 +3210,10 @@ inline std::optional pathText( const std::string& root, const std:: pth.empty() ? 0 : 1, pth.empty() ? std::size_t( 0 ) : pth.size() - 1, ptRootAttr.c_str(), graphCountFloorAttrXml( g ).c_str() ); // M15: gauge + marker rw::emitTo( mem, "{}", countAttrXmlOrEmpty( "to_value_refs", ptToValueRefs ) ); // absent at zero, as on the CLI - std::string ptGapRows; - if( ptGaps.any() ) - { - const PathGapsXml gx = pathGapsXml( ing, ptGaps, ptSingleRoot, ptRootPrefix, "the connect verb on " + from + "," + to, "" ); - rw::emitTo( mem, "{}", gx.rootAttrs ); - ptGapRows = gx.rows; - } - else if( pth.empty() ) - { - rw::emitTo( mem, " hint=\"no directed call path — try the connect verb on {},{} (undirected: finds a shared caller), or uses/impact for non-call references\"", - ex( from ).c_str(), ex( to ).c_str() ); - } - rw::emitRaw( mem, ">" ); - rw::emitRaw( mem, ptGapRows.c_str() ); + const PathGapsXml ptUnreached = pth.empty() + ? pathUnreachedXml( ing, ptGaps, ptSingleRoot, ptRootPrefix, { "the connect verb on " + from + "," + to, "uses/impact", "" } ) + : PathGapsXml{}; + rw::emitTo( mem, "{}>{}", ptUnreached.rootAttrs, ptUnreached.rows ); for( NodeId n : pth ) { const Symbol& s = ing.symbols[n]; const std::string_view rp = ptSingleRoot ? sarif::rootRelativeUri( ing.files[ s.fileId ], ptRootPrefix ) : std::string_view( ing.files[ s.fileId ] ); diff --git a/src/model.h b/src/model.h index 0d2933f4f..cdae9c691 100644 --- a/src/model.h +++ b/src/model.h @@ -108,9 +108,12 @@ inline const char* symTag( SymKind k ) noexcept // admitted Struct as "a named type" (callable as a conversion, a method owner, a type-like row) admits these too, so // the split changes the t= LABEL and nothing else. Read this, not `== SymKind::Struct`, wherever the question is // "is this a struct-or-named-type definition". +inline constexpr std::uint32_t kStructOrNamedTypeBits = ( 1u << unsigned( SymKind::Struct ) ) | ( 1u << unsigned( SymKind::NamedType ) ) + | ( 1u << unsigned( SymKind::Alias ) ) | ( 1u << unsigned( SymKind::FuncType ) ); +static_assert( kSymKindCount <= 32, "kStructOrNamedTypeBits holds one bit per SymKind" ); inline bool isStructOrNamedType( SymKind k ) noexcept { - return k == SymKind::Struct || k == SymKind::NamedType || k == SymKind::Alias || k == SymKind::FuncType; + return ( ( kStructOrNamedTypeBits >> unsigned( k ) ) & 1u ) != 0u; } // #324: a ModuleScope owner is SYNTHETIC (comment above the enum) — a CALLER minted after the tags.scm pass, diff --git a/src/pathgaps.h b/src/pathgaps.h index 52957df07..920623cff 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -65,20 +66,17 @@ struct PathSearchGaps PathGapCounts totals{}; std::vector syms; // cone symbols carrying >= 1 gap: nearest first, then more gaps, then id - bool any() const noexcept - { - return std::ranges::any_of( totals, []( std::size_t n ) { return n > 0; } ); - } + bool any() const noexcept; }; inline std::size_t pathGapSum( const PathGapCounts& c ) noexcept { - std::size_t s = 0; - for( const std::size_t n : c ) - { - s += n; - } - return s; + return std::accumulate( c.begin(), c.end(), std::size_t( 0 ) ); +} + +inline bool PathSearchGaps::any() const noexcept +{ + return pathGapSum( totals ) > 0; } // The directed cone of `srcs` (the same out-edges shortestPathAny walks, every one of them — no early stop at a target, @@ -202,22 +200,35 @@ inline std::string_view pathGapsLegend( bool on ) noexcept struct PathGapsXml { - std::string rootAttrs; // searched= gaps= gap_syms= [gap_syms_capped=] hint= next= — replaces the old hint - std::string rows; // the rows, the path element's only children when reachable="0" + std::string rootAttrs; // searched= gaps= gap_syms= [gap_syms_capped=] hint= next=, or the plain no-path hint alone + std::string rows; // the rows, the path element's only children when reachable="0" ("" with the plain hint) }; -// `rootPrefix` empty ⇒ p= stays as indexed (multi-root), exactly as the hop rows spell it. `connectSuggestion` is the -// transport's own spelling of the undirected follow-up ("--connect=A,B" on the CLI, "the connect verb on A,B" on MCP); -// `hintTail` is the CLI's existing several-defs clause (already escaped text), "" on MCP — both as the no-gap hint has them. -inline PathGapsXml pathGapsXml( const IngestResult& ing, const PathSearchGaps& gaps, bool singleRoot, std::string_view rootPrefix, - std::string_view connectSuggestion, std::string_view hintTail ) +// The transport's own spellings of the follow-ups, so one function writes both dialects: `connect` is the undirected verb +// ("--connect=A,B" on the CLI, "the connect verb on A,B" on MCP), `usesImpact` the non-call one ("--uses/--impact" / +// "uses/impact"), `tail` the CLI's several-defs clause (plain text, "" on MCP) — exactly as the no-gap hint had them. +struct PathHintSpelling +{ + std::string connect; + std::string_view usesImpact; + std::string_view tail; +}; + +// The reachable="0" clause: the gap clause when the search met a gap, else the plain hint, byte-identical to before. +// `rootPrefix` empty ⇒ p= stays as indexed (multi-root), exactly as the hop rows spell it. +inline PathGapsXml pathUnreachedXml( const IngestResult& ing, const PathSearchGaps& gaps, bool singleRoot, std::string_view rootPrefix, + const PathHintSpelling& say ) { - EXPECTS( gaps.any(), "the gap clause is emitted only for a search that met a gap" ); std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; - const std::size_t shown = std::min( gaps.syms.size(), kPathGapRows ); - PathGapsXml out; + if( !gaps.any() ) + { + out.rootAttrs = " hint=\"no directed call path — try " + ex( say.connect ) + " (undirected: finds a shared caller), or " + + std::string( say.usesImpact ) + " for non-call references" + std::string( say.tail ) + "\""; + return out; + } + const std::size_t shown = std::min( gaps.syms.size(), kPathGapRows ); std::string next = "--expand="; for( std::size_t i = 0; i < shown; ++i ) { @@ -231,7 +242,7 @@ inline PathGapsXml pathGapsXml( const IngestResult& ing, const PathSearchGaps& g out.rootAttrs = " searched=\"" + std::to_string( gaps.searched ) + "\" gaps=\"" + pathGapCountsValue( gaps.totals ) + "\" gap_syms=\"" + std::to_string( gaps.syms.size() ) + "\"" + ( gaps.syms.size() > shown ? " gap_syms_capped=\"1\"" : "" ) + " hint=\"no path through resolved call edges, but the search is incomplete: it met calls with no edge (gaps=) — read the gap rows, or try " - + ex( connectSuggestion ) + " (undirected: finds a shared caller)" + std::string( hintTail ) + "\"" + nextAttrXml( next ); + + ex( say.connect ) + " (undirected: finds a shared caller)" + std::string( say.tail ) + "\"" + nextAttrXml( next ); return out; } diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index f3719902d..a9ee6425e 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2176,21 +2176,12 @@ std::optional runPath( const MainDispatch& d ) rw::emitTo( stdout, "{}", rw::countAttrXmlOrEmpty( "to_value_refs", pthToValueRefs ) ); // absent at zero: byte-identical otherwise // P2.10: a dead end is exactly the moment to name the next verb. --path is DIRECTED; --connect searches // undirected and finds the shared-caller join a directed walk can never see. - const char* const pthDefsTail = ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : ""; - std::string pthGapRows; - if( pthGaps.any() ) - { - const rw::PathGapsXml gx = rw::pathGapsXml( ing, pthGaps, pthSingleRoot, pthRootPrefix, - "--connect=" + std::string( srcN ) + "," + std::string( dstN ), pthDefsTail ); - rw::emitTo( stdout, "{}", gx.rootAttrs ); - pthGapRows = gx.rows; - } - else if( path.empty() ) - { - rw::emitTo( stdout, " hint=\"no directed call path — try --connect={},{} (undirected: finds a shared caller), or --uses/--impact for non-call references{}\"", - ex( srcN ).c_str(), ex( dstN ).c_str(), pthDefsTail ); - } - rw::emitTo( stdout, ">{}", pthGapRows ); + const rw::PathGapsXml pthUnreached = path.empty() + ? rw::pathUnreachedXml( ing, pthGaps, pthSingleRoot, pthRootPrefix, + { "--connect=" + std::string( srcN ) + "," + std::string( dstN ), "--uses/--impact", + ( srcDefs.size() > 1 || dstDefs.size() > 1 ) ? "; several defs share these names — qualify as file:name to pick one" : "" } ) + : rw::PathGapsXml{}; + rw::emitTo( stdout, "{}>{}", pthUnreached.rootAttrs, pthUnreached.rows ); for( NodeId n : path ) { const Symbol& s = ing.symbols[n]; From 743d3061f009c7089a65e4e057dc011698e95e53 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:24:12 -0400 Subject: [PATCH 055/229] CHANGELOG: FE-B states its measured cost, not a pointer to a report Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 353e6db22..7a4851147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,8 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. false. Rule 3's include-file narrow no longer decides a member call: the file a caller imports says nothing about its receiver. Ranking keeps the old ladder's pick (the same-file, else same-directory rung): the extra candidates are listed at edge weight zero, so a guess never moves PageRank. -- Measured on the round-1 comparison-table calls for this tool: see the lane report; the gate is +- Cost, measured over 210 calls on eight public repositories (`--no-cache`): +8.55% bytes in total; 32% of the + ``/``/`` rows carry `via="name"`, most of them the map's `` rows (C trees: no change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses for every evidence rule). Ingest records the receiver chain (`kParserVer` 146, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. From d14e3fdf6e7173d3bcb714fd8af0f2d752ac9638 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:27:27 -0400 Subject: [PATCH 056/229] refactor: the gap sum is carried, not recomputed; the split kinds are one range --quality-delta's clone lens matched the lane's two-line helpers (pathGapSum, the bit-table isStructOrNamedType, the bounded table read in lspKindWord) against unrelated small functions. The per-symbol and total gap sums are now carried in PathGapSym/PathSearchGaps; the three split kinds are pinned as the last contiguous enumerators and read as a range; lspKindWord states its invariant (a cached kind byte is validated on load) instead of re-checking it. No output change. Co-Authored-By: Claude Opus 5.5 --- src/lsp.h | 4 ++-- src/model.h | 8 ++++---- src/pathgaps.h | 32 ++++++++++++-------------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/lsp.h b/src/lsp.h index be37adbda..25f796dd9 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -378,8 +378,8 @@ inline constexpr std::array kLspKindWords = { }; inline const char* lspKindWord( rw::SymKind k ) noexcept { - const std::size_t i = std::size_t( k ); - return i < kLspKindWords.size() ? kLspKindWords[i] : "symbol"; + EXPECTS( std::size_t( k ) < kLspKindWords.size(), "a symbol's kind is a SymKind enumerator (the cache validates the byte)" ); + return kLspKindWords[ std::size_t( k ) ]; } // ─── file identity: a client URI → an index fileId ────────────────────────────────────────────────── diff --git a/src/model.h b/src/model.h index cdae9c691..9e01d03d6 100644 --- a/src/model.h +++ b/src/model.h @@ -108,12 +108,12 @@ inline const char* symTag( SymKind k ) noexcept // admitted Struct as "a named type" (callable as a conversion, a method owner, a type-like row) admits these too, so // the split changes the t= LABEL and nothing else. Read this, not `== SymKind::Struct`, wherever the question is // "is this a struct-or-named-type definition". -inline constexpr std::uint32_t kStructOrNamedTypeBits = ( 1u << unsigned( SymKind::Struct ) ) | ( 1u << unsigned( SymKind::NamedType ) ) - | ( 1u << unsigned( SymKind::Alias ) ) | ( 1u << unsigned( SymKind::FuncType ) ); -static_assert( kSymKindCount <= 32, "kStructOrNamedTypeBits holds one bit per SymKind" ); +// The three split kinds are the LAST three enumerators, so they are one range; the asserts pin that. +static_assert( SymKind::Alias == SymKind( unsigned( SymKind::NamedType ) + 1 ) && SymKind::FuncType == SymKind( unsigned( SymKind::NamedType ) + 2 ) + && kSymKindCount == unsigned( SymKind::FuncType ) + 1, "NamedType..FuncType are the last, contiguous kinds" ); inline bool isStructOrNamedType( SymKind k ) noexcept { - return ( ( kStructOrNamedTypeBits >> unsigned( k ) ) & 1u ) != 0u; + return k == SymKind::Struct || k >= SymKind::NamedType; } // #324: a ModuleScope owner is SYNTHETIC (comment above the enum) — a CALLER minted after the tags.scm pass, diff --git a/src/pathgaps.h b/src/pathgaps.h index 920623cff..9bee3ac7a 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -36,7 +36,6 @@ #include #include #include -#include #include #include #include @@ -57,28 +56,20 @@ struct PathGapSym { NodeId id = kNoNode; std::uint32_t depth = 0; // hops from the nearest from= definition + std::size_t calls = 0; // the sum of counts — the row order's second key PathGapCounts counts{}; }; struct PathSearchGaps { std::size_t searched = 0; // symbols the directed search reached, the from= definitions included + std::size_t calls = 0; // every gap in the cone: the sum of totals PathGapCounts totals{}; std::vector syms; // cone symbols carrying >= 1 gap: nearest first, then more gaps, then id - bool any() const noexcept; + bool any() const noexcept { return calls > 0; } }; -inline std::size_t pathGapSum( const PathGapCounts& c ) noexcept -{ - return std::accumulate( c.begin(), c.end(), std::size_t( 0 ) ); -} - -inline bool PathSearchGaps::any() const noexcept -{ - return pathGapSum( totals ) > 0; -} - // The directed cone of `srcs` (the same out-edges shortestPathAny walks, every one of them — no early stop at a target, // because this runs only when there was none) and the gap populations inside it. Deterministic: BFS in id-ascending // out-edge order, a reference pass in index order, and a total order on the rows. @@ -144,15 +135,17 @@ inline PathSearchGaps pathSearchGaps( const IngestResult& ing, const Graph& g, s } for( const NodeId u : queue ) { - if( pathGapSum( per[u] ) == 0 ) - { - continue; - } + std::size_t here = 0; for( std::size_t k = 0; k < kPathGapKindCount; ++k ) { + here += per[u][k]; out.totals[k] += per[u][k]; } - out.syms.push_back( PathGapSym{ u, depth[u], per[u] } ); + if( here > 0 ) + { + out.calls += here; + out.syms.push_back( PathGapSym{ u, depth[u], here, per[u] } ); + } } std::ranges::sort( out.syms, []( const PathGapSym& a, const PathGapSym& b ) { @@ -160,10 +153,9 @@ inline PathSearchGaps pathSearchGaps( const IngestResult& ing, const Graph& g, s { return a.depth < b.depth; } - const std::size_t sa = pathGapSum( a.counts ), sb = pathGapSum( b.counts ); - if( sa != sb ) + if( a.calls != b.calls ) { - return sa > sb; + return a.calls > b.calls; } return a.id < b.id; } ); From a6cb97421248b939d579944da8975fdde90b314b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:31:42 -0400 Subject: [PATCH 057/229] quality: ack five lexical clone matches on one-line helpers --ack-only=duplication,new-clone-of-reused-helper: isStructOrNamedType and lspKindWord share only the shape of a one-line boolean / indexed return with unrelated predicates (routematch::methodsCompatible, localsCountedLang, ConfigScopes::scope). The real findings of this lane (pathText complexity and verbosity, the lspKindWord switch clone, the sum/any helpers) were fixed in the two refactor commits before this. Co-Authored-By: Claude Opus 5.5 --- .ripwire_quality_acks | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index fff1468da..203ebc863 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -547,7 +547,9 @@ ack duplication 1cd9e6b6ec0829c1 75 now=75 was=0 prov=recon R2 pattern lane: the ack duplication 1d18dab8dc3fcc99 37 now=37 was=0 prov=recon incidental shape similarity of small JSON/string builders across unrelated protocols (#279 LSP wire helpers vs MCP/CLI/quality code); sharing a helper would couple the LSP wire format to them ack duplication 1f949c8839bb857e 19 now=19 was=0 prov=recon lane/diag-single-write: test/diagnotice_harness.cpp expectedDegraded is the test's own independent spelling of the degraded notice text, one return std::format( literal, four args ) — the 19-token pair with handoff.h handoffSymsCapClause is that one-statement shape and nothing else: a legend clause and a test expectation share no meaning, so merging them would name neither ack duplication 1fbb92d43e45d577 59 now=59 was=0 prov=recon Integration train 2b (PR #244, TS/JS literal receivers): isJsTsLitRecv (a five-case RecvKind membership test), jsLitCtorName (a five-case RecvKind -> constructor-name switch) and isJsTsBuiltinCtor (a five-string membership test) are each the smallest spelling of one fact about the Lit* enumerators appended in the same PR. Their token runs match refRoleTag, styleTag, votingStyle, langUsesHashComment, elixirTypedAttribute and a clone-idiom fixture by SHAPE (a short enum switch, an OR of equality tests), not by shared logic: no helper exists they could call, and folding unrelated enums into one table would couple them. Acked at the train that first measured them on the merged tree; the three stay adjacent in model.h beside the enum they describe. +ack duplication 202edf39c263e3af 37 now=37 was=0 p=src/lsp.h:379 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack duplication 206efc5a490a0d83 27 now=27 was=0 prov=recon P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) +ack duplication 20777160f7cd4594 26 now=26 was=0 p=src/graph.h:469 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack duplication 22018148ed87cd0e 58 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack duplication 2374106e320946a5 50 now=50 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 23b67d4e4935395f 28 by=src/* now=28 was=0 prov=recon A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. @@ -701,6 +703,7 @@ ack duplication d6d46219be2f34de 44 now=44 was=0 prov=recon DISCLOSE sinks: a re ack duplication d74386e39efcdffa 36 by=src/extentsuspect.h now=36 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. ack duplication d77ef6ec7d956c38 46 now=46 was=0 prov=recon audit pass 2026-08-06: per-cause as_* disclosure blocks in writeFieldAffinity follow the sibling one-if-per-attr pattern; chaseTypeCanPoint/isChaseRhsRow/looksLikePath are 2-call string predicates whose shared helper would be a worse abstraction; walks.cpp fixture twins are deliberate discriminating traps; short-horizon-churn is the audit editing just-landed code; runDefaultMap growth is the mapCtxOpenBytes fix + its measurement comment ack duplication d842417635ec5627 38 by=src/* now=38 was=0 prov=recon A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. +ack duplication da2b082324acc9b0 20 now=20 was=0 p=src/model.h:114 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack duplication daefdef824b8a96b 40 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. ack duplication dc449422d3ac892c 26 now=26 was=0 prov=recon D3 Windows port (lane/win32-d3-onto-os): four short token clones between os.h's Windows half and unrelated code, each a generic idiom rather than a shared rule — a one-line std::ranges::any_of membership lambda (win32TableProduces | cli.h shipsViewFlag, 34 tokens), a case-folding endsWith (oswin::endsWithAsciiCaseless | darkflags endsWithView, 45 tokens; os_win32_logic.h is a leaf infra header with no project includes, so it cannot call darkflags), an RAII reset (os_win32.cpp Unique::reset | pathguard OwnedFd::reset, 26 tokens; a HANDLE/SOCKET/HLOCAL owner cannot be an fd owner), and a three-term suffix OR (isWslLauncher | two unrelated predicates, 26 tokens). The larger clones (table scans, the PATHEXT reader) were refactored away instead of acked. ack duplication dc5ec483298d7df6 214 now=214 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) @@ -762,6 +765,7 @@ ack new-clone-of-reused-helper 0e9742973611fd71 3 T3 disclosure-gap fix 2026-08- ack new-clone-of-reused-helper 1096a473dedb29cf 4 now=4 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff ack new-clone-of-reused-helper 15ba8af502e0d24e 3 assignment types (narrowcheck arms 44-51): api-surface buildFieldNarrowTables gains the class-name set (1 to 2 params) because its local-name set must drop an assignment's callee-read type that no class is called, the same rule Rule 2's varType table applies, and the set is built once in buildGraph and once in collectFieldUseSites rather than re-derived per call. duplication/new-clone-of-reused-helper classNameSet ~ layout.h buildAggIndex (93 tokens): classNameSet is the Class/Struct/Interface loop buildGraph already carried inline for Rule 2c, hoisted into resolve.h so collectFieldUseSites can share it; buildAggIndex maps a name to its ids in layout.h, which includes graph.h, so resolve.h cannot reuse it and a common helper would invert the include order for a 7-line loop with a different value type. ack new-clone-of-reused-helper 1cd9e6b6ec0829c1 3 now=3 was=0 prov=recon R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. +ack new-clone-of-reused-helper 202edf39c263e3af 3 now=3 was=0 p=src/lsp.h:379 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack new-clone-of-reused-helper 340861e6b3c185e4 3 now=3 was=0 prov=recon Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. ack new-clone-of-reused-helper 36e29c8cfeef798b 120 now=120 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper 395f3040c6dd8ee1 5 now=5 was=0 prov=recon a probe TU deliberately mirrors layout declarations (#224 structlayout_probe.cpp) @@ -805,6 +809,7 @@ ack new-clone-of-reused-helper c8b32435256b863e 4 by=src/* Phase 5 (docs/EVALS.m ack new-clone-of-reused-helper ca79cc414dda8197 5 now=5 was=0 prov=recon Integration train 2b (PR #244, TS/JS literal receivers): isJsTsLitRecv (a five-case RecvKind membership test), jsLitCtorName (a five-case RecvKind -> constructor-name switch) and isJsTsBuiltinCtor (a five-string membership test) are each the smallest spelling of one fact about the Lit* enumerators appended in the same PR. Their token runs match refRoleTag, styleTag, votingStyle, langUsesHashComment, elixirTypedAttribute and a clone-idiom fixture by SHAPE (a short enum switch, an OR of equality tests), not by shared logic: no helper exists they could call, and folding unrelated enums into one table would couple them. Acked at the train that first measured them on the merged tree; the three stay adjacent in model.h beside the enum they describe. ack new-clone-of-reused-helper cf6c83caaec68565 10 now=10 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff ack new-clone-of-reused-helper d11c67611bc73d49 3 mcpgrepdegradedcheck round (2026-08-30): the gate's mcp_text/batch_sub/call shell harness is the 7th deliberate copy of mcpclidiffcheck's — gate scripts are self-contained by design (each runs standalone under RIPWIRE_BIN with no shared lib to source), the same trade every earlier MCP gate made; a shared test/lib refactor is its own round, not this diff's +ack new-clone-of-reused-helper da2b082324acc9b0 4 now=4 was=0 p=src/model.h:114 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack new-clone-of-reused-helper daefdef824b8a96b 3 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. ack new-clone-of-reused-helper dc5ec483298d7df6 16 now=16 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper ddce9efe392269e6 4 now=4 was=0 prov=recon 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. From c3bcc1aeea1318ee1d1298910334e6b1035c2557 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 05:57:12 -0400 Subject: [PATCH 058/229] chore(quality): ack emitGrepEncRows signature change (single same-file caller) emitGrepEncRows now takes the page's enc rows and their floors, computed once by emitGrepReport before the legend, instead of the raw hits. Its one caller is emitGrepReport in the same file, updated in the same commit (edit-check: callers=1 incompatible=0). Co-Authored-By: Claude Opus 5.5 --- .ripwire_quality_acks | 1 + 1 file changed, 1 insertion(+) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index fff1468da..7bf3a30d3 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -83,6 +83,7 @@ ack api-surface 5c2c4a2b311b8dba 4 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the l ack api-surface 5e5cc30bcbc1fb63 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 5ec38fbd414fa4d4 7 cid=6f20993f1b475898 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 5f7ae7ac9bbef3c5 5 cid=8662c6730ed46798 now=5 was=4 facet=contract-change p=src/darkflags.h:522 0.6.6 D5 review B1: forEachCodeByte gains the per-file TemplateStack (JS/TS template literals span lines); its one caller, harvestEnvReads, is updated in the same commit +ack api-surface 610cc771fd6f10c1 5 cid=86df6969a0d891fb now=5 was=4 facet=contract-change p=src/verbs_grep.h:105 lane count-floor-068 (4a1bbc82): emitGrepEncRows takes the page's enc rows and their floors (computed once by emitGrepReport before the legend, so the full dialect defines callers_floor= only when a row carries it) instead of the raw hits; its one caller, emitGrepReport in the same file, updated in the same commit ack api-surface 61e5df9e1e40ff70 5 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 6483016e50837e70 3 cid=4c53bf4b5b7d4a0c degrade disclosure: each read seam and the ref pool now take the DISCLOSE sink whose field the document reads From 36bc144a2471476ef6ee22cd48bc5a01548b5d76 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 06:10:22 -0400 Subject: [PATCH 059/229] test(donelegendcheck): re-pin sd_uses 3800 -> 4100 for sites_l=, with a twin holding the rest at 3800 Every --safe-delete caller row now carries sites_l= (its call-site lines beside p=, the caller's definition line), and the full legend defines it exactly when a row carries it. On this gate's fixture the sd_uses legend measures 3662 -> 4086 B; the 424 B are that one sentence. sd_none, which has no caller row, does not move (3659 B). The old ceiling is kept as a twin arm: the sd_uses legend with the sites_l= sentence cut out must still fit 3800 B (it measures 3662 B, the base binary's whole legend). The cut is taken only when the row carries sites_l= and the sentence is found once; any other premise fails. Two emit-on-presence arms pin the sentence to sd_uses and keep it off sd_none. Co-Authored-By: Claude Opus 5.5 --- test/donelegendcheck.sh | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/test/donelegendcheck.sh b/test/donelegendcheck.sh index 5ab33f1a3..1efd56a10 100755 --- a/test/donelegendcheck.sh +++ b/test/donelegendcheck.sh @@ -175,8 +175,46 @@ printf '%s' "$DIRT" >> "$FX/src/base.cpp" # 213 there, so the ceiling moves by less than the growth (45 B of headroom left) — a ratchet, not an allowance. run_budget qd_dirty 3900 8574 "$FX" --quality-delta run_budget qd_dirty_scope 5200 10512 "$FX" --quality-delta "--scope=src/*" -run_budget sd_uses 3800 4112 "$FX" --safe-delete=classifyWidth +# sd_uses 3800 -> 4100 (2026-10-07, lane count-floor-068, CALLSITE-LINE): every safe-delete row now carries +# sites_l=, the caller's call-site LINES beside p= (the caller's definition line) — graded answers that named every +# caller were scored wrong for printing only definition lines — and the law this gate pins (no emitted name is +# undefined) puts its one defining sentence in the full legend exactly when a row carries it (verbs_navigate.h, +# flags.hasSitesL). Measured on this fixture: 3662 -> 4086 B (+424, the sentence alone); sd_none (no caller row, no +# sites_l=) does not move (3659). The pin is the next multiple of 100 over the measured total. The OLD ceiling is +# not dropped: the twin arm below holds everything EXCEPT that sentence at 3800, so the rest of the essay cannot +# re-inflate under the new headroom. +run_budget sd_uses 4100 4112 "$FX" --safe-delete=classifyWidth run_budget sd_none 3800 4112 "$FX" --safe-delete=tangle +# twin of the pre-2026-10-07 sd_uses ceiling: the sd_uses legend with the sites_l= sentence cut out still fits 3800 B. +# The cut is only taken when the row really carries sites_l= AND the sentence is found once, start to end; +# any other premise FAILs (a cut that silently removes nothing, or removes the wrong text, would prove nothing). +sdRest="$( python3 - "$WORK/sd_uses.xml" <<'PY' +import re, sys +doc = open( sys.argv[1], 'rb' ).read().decode( 'utf-8', 'replace' ) +m = re.match( r'\A(?:\s*)+', doc, re.S ) +leg = m.group( 0 ) if m else '' +body = doc[ len( leg ): ] +if not leg or 'sites_l="' not in body: + print( 'NOPREMISE row-without-sites_l' ); sys.exit( 0 ) +a, z = 'sites_l= on a c row', 'Two calls on one line are one site. ' +i = leg.find( a ); j = leg.find( z, i + 1 ) if i >= 0 else -1 +if i < 0 or j < 0 or leg.count( a ) != 1: + print( 'NOPREMISE sentence-not-found-once' ); sys.exit( 0 ) +rest = leg[ :i ] + leg[ j + len( z ): ] +print( len( rest.encode() ), len( leg.encode() ) - len( rest.encode() ) ) +PY +)" +case "$sdRest" in + NOPREMISE*) no "(a) sd_uses twin (old 3800 ceiling minus the sites_l= sentence) cannot measure: ${sdRest#NOPREMISE }" ;; + *) read -r restB cutB </dev/null && [ "$restB" -le 3800 ]; then + ok "(a) sd_uses twin: legend minus the sites_l= sentence ($cutB B) is $restB B <= 3800 B (the pre-CALLSITE-LINE ceiling)" + else + no "(a) sd_uses twin: legend minus the sites_l= sentence ($cutB B) is ${restB:-?} B > 3800 B — the rest of the essay re-inflated" + fi ;; +esac # tg_empty 1200 -> 1400 (2026-09-04, capture-audit wave-1 close, lane L4 M15): the zero-row root now carries # graph_ambiguous=/graph_unresolved= (the resolver gauge every graph-floored root spells, floormarkcheck arm 11), # and two emitted attributes must be DEFINED in this document (arm (d) below) — the shortest honest sentence is @@ -243,6 +281,9 @@ present sd_uses 'untested-radius: callers or uses exist' absent sd_uses 'none-found: zero callers AND zero uses' present sd_none 'none-found: zero callers AND zero uses' absent sd_none 'untested-radius: callers or uses exist' +# sites_l= (CALLSITE-LINE, 2026-10-07) is defined exactly where a caller row carries it: sd_uses has one caller row, sd_none none. +present sd_uses 'sites_l= on a c row' +absent sd_none 'sites_l=' echo echo "=== (d) COVERAGE — every attribute the root emits is DEFINED in that document's own legend ==========" From 5e08bf814f6373ced9dd38943c44ca407cf4558a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:17:00 -0400 Subject: [PATCH 060/229] =?UTF-8?q?test(countfloorcheck):=20L2=20=E2=80=94?= =?UTF-8?q?=20a=20bound=20Rust=20impl=20and=20a=20reopened=20Ruby=20class?= =?UTF-8?q?=20do=20not=20floor=20implementors=3D=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graph's inheritance pass re-keys the derived type in two languages: a Rust `impl Trait for T` header lies outside T's span (T's name rides in the reference's qualifier), and a Ruby class opened twice is one implementor. The implementors floor compared the reference's enclosing symbol id against the implementor list, so both bound clauses read as misses. A Rust impl for a type the tree never defines is the near miss that must still floor. Co-Authored-By: Claude Opus 5.5 --- test/countfloorcheck.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index a6c7e702b..3b1369759 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -37,6 +37,8 @@ # implementors_shown= + implementors_next=; (negative) all rows listed => no shown/next attribute. # L implementors_floor: an inherit clause naming the interface that bound nowhere (a cross-language base) # floors the count on the bundle AND the targeted verb; (negative) without it, no marker. +# L2 (negatives) a bound Rust `impl Trait for T` and a twice-opened Ruby class do not floor (the graph re-keys +# their derived type); (near miss) a Rust impl for a type the tree never defines still floors. # M MCP twins: grep's `enclosing` JSON carries callers_floor/floor_next exactly where the CLI does; the MCP # `for` lego block equals the CLI's. # N legend: every new attribute is defined in the answer that carries it (compact and full dialects). @@ -289,6 +291,27 @@ IF_LB="$( run tsl '--for=which router classes implement the Router interface' | if [ "$( attr "$IF_LB" implementors_floor )" = 1 ]; then ok "(L) the --for bundle carries the same floor"; else no "(L) bundle: $IF_LB"; fi case "$( first_iface "$( run ts --lego=Router )" )" in *implementors_floor=*) no "(L) a fully bound interface carries implementors_floor" ;; *) ok "(L) no floor when every inherit clause bound" ;; esac +# ── L2: the derived type is the one the GRAPH bound, not the reference's enclosing symbol ───────────────── +# graph.h's inheritance pass re-keys two languages' derived type: a Rust `impl Trait for T` header sits outside T's +# span (the ref carries T's NAME in `qualifier`; fromSymbol is whatever encloses the impl), and a reopened Ruby class +# is ONE implementor (rubyBases.canonicalClass), so the clause in the second `class Foo < Base` comes from a symbol id +# the implementor list does not hold. Both are BOUND clauses and must not floor; a Rust impl for a type the tree +# never defines is the near miss that still must. +echo "=== L2: a bound Rust impl / a reopened Ruby class is not evidence of a miss; an impl for an undefined type is ===" +mkdir -p "$W/rs" "$W/rsg" "$W/rb" +printf 'pub trait Shape { fn area(&self) -> u32; }\npub struct Widget { n: u32 }\nimpl Shape for Widget { fn area(&self) -> u32 { self.n } }\n' > "$W/rs/lib.rs" +cp "$W/rs/lib.rs" "$W/rsg/lib.rs"; printf 'impl Shape for Ghost { fn area(&self) -> u32 { 0 } }\n' >> "$W/rsg/lib.rs" +printf 'class Base\nend\nclass Foo < Base\n def a; end\nend\nclass Foo < Base\n def b; end\nend\n' > "$W/rb/a.rb" +IF_RS="$( first_iface "$( run rs --lego=Shape )" )" +[ "$( attr "$IF_RS" implementors )" = 1 ] || no "(L2) premise: Rust Shape must have its one implementor: $IF_RS" +case "$IF_RS" in *implementors_floor=*) no "(L2) a bound Rust impl floors implementors=: $IF_RS" ;; *) ok "(L2) Rust impl Shape for Widget (bound): no floor" ;; esac +IF_RB="$( first_iface "$( run rb --lego=Base )" )" +[ "$( attr "$IF_RB" implementors )" = 1 ] || no "(L2) premise: the reopened Ruby class must be one implementor: $IF_RB" +case "$IF_RB" in *implementors_floor=*) no "(L2) a reopened Ruby class floors implementors=: $IF_RB" ;; *) ok "(L2) Ruby class Foo < Base opened twice (bound): no floor" ;; esac +IF_RSG="$( first_iface "$( run rsg --lego=Shape )" )" +[ "$( attr "$IF_RSG" implementors )" = 1 ] && [ "$( attr "$IF_RSG" implementors_floor )" = 1 ] \ + && ok "(L2) Rust impl Shape for an undefined Ghost (unbound) still floors" || no "(L2) near miss: $IF_RSG" + # ── M: MCP twins ───────────────────────────────────────────────────────────────────────────────────────── echo "=== M: the MCP grep and for twins say the same thing ===" cat > "$W/mcptext.py" <<'PY' From 09b38b013ee9949fba486462860c0ab86680d6d1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:31:42 -0400 Subject: [PATCH 061/229] fix(count-floor): safeDeleteRiskSentence's ASSUME names the end iterator instead of calling std::end selfcheckcheck (C) keeps calls out of promises: a self-check states values, and Release may evaluate it as an assumption. The end iterator is taken once outside the ASSUME; the degrade test after it reads the same value. No output change. Co-Authored-By: Claude Opus 5.5 --- src/verbs_navigate.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 41a4b5c78..927610808 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -904,9 +904,10 @@ inline constexpr SafeDeleteRiskReading kSafeDeleteRiskReadings[] = { }; inline const char* safeDeleteRiskSentence( std::string_view risk ) noexcept { - const auto it = std::ranges::find( kSafeDeleteRiskReadings, risk, &SafeDeleteRiskReading::risk ); - ASSUME( it != std::end( kSafeDeleteRiskReadings ), "safeDeleteRiskSentence: runSafeDelete assigns only the four values listed" ); - return it != std::end( kSafeDeleteRiskReadings ) ? it->sentence : ""; + const auto it = std::ranges::find( kSafeDeleteRiskReadings, risk, &SafeDeleteRiskReading::risk ); + const auto end = std::end( kSafeDeleteRiskReadings ); // outside the promise: a self-check names values, it calls nothing + ASSUME( it != end, "safeDeleteRiskSentence: runSafeDelete assigns only the four values listed" ); + return it != end ? it->sentence : ""; } inline void emitSafeDeleteLegend( std::size_t defCount, std::size_t unprovenDefs, std::size_t ambiguousCallers, std::string_view risk, From 9fe6018a0a2cd61b0a825b44e4d56f9623c8a87e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:31:42 -0400 Subject: [PATCH 062/229] fix(count-floor): an inherit clause is bound when the graph listed its derived type, read as graph.h reads it implementors_floor= compared each extends/implements reference's enclosing symbol id against the implementor list. The graph's inheritance pass names the derived type two other ways: a Rust `impl Trait for T` header lies outside T's span, so T's name rides in the reference's qualifier; and a reopened Ruby class is listed once, under its canonical opening. Both bound clauses therefore read as misses, and a fully bound Rust trait or Ruby base printed implementors_floor="1" (rustqualcheck's pinned --lego=Shape answer moved). The bound test is now a predicate markUnboundSpellings takes: callers keep the plain enclosing-symbol test; the implementors floor also accepts a clause whose derived name (the qualifier on a Rust inherit ref, else the enclosing symbol's name) names a listed implementor. By name, like every floor here. C++, C#, Java, Python and TS take the enclosing-symbol path unchanged. Co-Authored-By: Claude Opus 5.5 --- src/countfloor.h | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/countfloor.h b/src/countfloor.h index 1db67053f..273a616be 100644 --- a/src/countfloor.h +++ b/src/countfloor.h @@ -163,9 +163,11 @@ inline IdsOfName relatedByName( const IngestResult& ing, const RowsOfName& rowsO } // ONE pass over the reference table: a reference `isKind` accepts, spelled like a row's name, from a symbol that is not -// in that name's `bound` set (or from file scope), marks every row of the name. `mark( row )` is the caller's verdict. -template -inline void markUnboundSpellings( const IngestResult& ing, const RowsOfName& rowsOfName, const IdsOfName& bound, IsKind&& isKind, Mark&& mark ) +// in that name's `bound` set (or from file scope), marks every row of the name. `isBound( r, ids )` decides whether the +// reference is accounted for by that name's sorted `bound` ids (empty when the name has none); `mark( row )` is the verdict. +template +inline void markUnboundSpellings( const IngestResult& ing, const RowsOfName& rowsOfName, const IdsOfName& bound, IsKind&& isKind, + IsBound&& isBound, Mark&& mark ) { for( const Reference& r : ing.references ) { @@ -178,10 +180,9 @@ inline void markUnboundSpellings( const IngestResult& ing, const RowsOfName& row { continue; } - const auto boundIt = bound.find( it->first ); - const bool isBound = r.fromSymbol != kNoNode && boundIt != bound.end() - && std::binary_search( boundIt->second.begin(), boundIt->second.end(), r.fromSymbol ); - if( !isBound ) + const auto boundIt = bound.find( it->first ); + const std::span ids = boundIt != bound.end() ? std::span( boundIt->second ) : std::span(); + if( !isBound( r, ids ) ) { for( const std::uint32_t row : it->second ) { @@ -191,6 +192,31 @@ inline void markUnboundSpellings( const IngestResult& ing, const RowsOfName& row } } +// The plain bound test: the reference's own enclosing symbol is one of the name's bound ids (sorted). +inline bool fromIsBound( const Reference& r, std::span ids ) noexcept +{ + return r.fromSymbol != kNoNode && std::binary_search( ids.begin(), ids.end(), r.fromSymbol ); +} + +// An inherit clause is bound when the graph listed its DERIVED type as an implementor. graph.h's inheritance pass names +// that type two ways besides the enclosing symbol, and this must read it the same way or a bound clause reads as a miss: +// a Rust `impl Trait for T` header lies outside T's span, so T's NAME rides in `qualifier` (empty for every other +// language's inherit ref); and a reopened Ruby class is listed once, under its canonical opening, so the clause in +// another opening comes from a symbol id the list does not hold but a symbol NAME it does. By name, like every floor +// here: a same-named derived type elsewhere that the graph bound answers for this clause too ("may", not "does"). +inline bool inheritIsBound( const IngestResult& ing, const Reference& r, std::span ids ) noexcept +{ + if( fromIsBound( r, ids ) ) + { + return true; + } + const std::string_view derived = r.isInherit && !r.qualifier.empty() ? std::string_view( r.qualifier ) + : r.fromSymbol != kNoNode && r.fromSymbol < ing.symbols.size() ? std::string_view( ing.symbols[ r.fromSymbol ].name ) + : std::string_view(); + return !derived.empty() + && std::ranges::any_of( ids, [ & ]( NodeId id ) { return id < ing.symbols.size() && ing.symbols[id].name == derived; } ); +} + inline bool isCallSpelling( const Reference& r ) noexcept { return ( r.role == RefRole::Call || r.role == RefRole::Macro ) && !r.isInherit && !r.isDocLink && !r.isCompose && r.lang != Lang::Markdown; @@ -247,7 +273,7 @@ inline std::vector callerFloors( const IngestResult& ing, const Gra const auto* inCi = g.inEdges.colIndices(); const countfloor::IdsOfName boundCallers = countfloor::relatedByName( ing, rowsOfName, g.wOutDeg.size(), [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), inCi + inRo[id], inCi + inRo[id + 1] ); } ); - countfloor::markUnboundSpellings( ing, rowsOfName, boundCallers, countfloor::isCallSpelling, + countfloor::markUnboundSpellings( ing, rowsOfName, boundCallers, countfloor::isCallSpelling, countfloor::fromIsBound, [ & ]( std::uint32_t row ) { out[row].isFloor = true; } ); std::optional own; // one more pass over the table: built only if a row is still open @@ -299,6 +325,7 @@ inline std::vector implementorFloors( const IngestResult& ing, const std:: const countfloor::IdsOfName boundDerived = countfloor::relatedByName( ing, rowsOfName, graphImplementors.size(), [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), graphImplementors[id].begin(), graphImplementors[id].end() ); } ); countfloor::markUnboundSpellings( ing, rowsOfName, boundDerived, []( const Reference& r ) { return r.isInherit || r.role == RefRole::Extends; }, + [ & ]( const Reference& r, std::span ids ) { return countfloor::inheritIsBound( ing, r, ids ); }, [ & ]( std::uint32_t row ) { out[row] = 1; } ); return out; } From 668c6a00e1113e209a010af2430709aba4fa3d28 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:37:58 -0400 Subject: [PATCH 063/229] CHANGELOG: drop the withdrawn map callee join; totals at the current head The map calls= join was withdrawn from this lane (it moves to a later map-rows lane). The [Unreleased] entry no longer describes it, and the measured totals are the ones of this build: 631,732 -> 611,785 B lossless (-3.2%), 567,512 B with the --whereis default (-10.2%), on the same 90 answers. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcde1d4d4..db44026d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,10 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] -### Changed — leaner answers: `--whereis` lists definitions, three lossless row spellings, the MCP legend once per session +### Changed — leaner answers: `--whereis` lists definitions, two lossless row spellings, the MCP legend once per session Measured on 90 rung-0 answers of a comparison table (eight repos, three in-sample and five held out), base vs this -build: 631,732 → 603,482 B for the lossless changes alone (−4.5%), 559,209 B with the `--whereis` default (−11.5%). +build: 631,732 → 611,785 B for the lossless changes alone (−3.2%), 567,512 B with the `--whereis` default (−10.2%). Every gold item the base answers supplied is still supplied (502 of 502), and decoding the lossless spellings gives the base answer back element for element on all 90. @@ -30,9 +30,8 @@ the base answer back element for element on all 90. window the listed rows; `hits=` counts every row; `complete=` reads the listing. - **Lossless:** a `--whereis` row on HEAD's commit omits `tip=`/`date=` (they read `at=` and the new root `head_date=`); a `--for` lens row omits `cx=`/`ccx=`/`in=` when 0 (the legend says an absent one is 0; the rows - kept by the byte budget are unchanged); a map row whose callees are all bare carries them as `calls="a,b,c"` - (any per-edge attribute keeps the `` rows). Median per class: how-it-works −0.24 KB, orient-for −0.26 KB, - orient-map −0.96 KB, where-defined −1.95 KB. + kept by the byte budget are unchanged). Median per class: how-it-works −0.24 KB, orient-for −0.26 KB, + where-defined −1.95 KB; map answers are unchanged. - **MCP: the legend session opens on the first answer.** A stdio session no longer waits for a read of `ripwire://legend-dict`: its first answer carries its legend inline, later answers take the `legend="ref"` posture (rows first, each definition sent once per session, the core carried by the first ref answer). From a433bd46514a27bd022701d9310606d8d47cc747 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:42:30 -0400 Subject: [PATCH 064/229] test(printf parity): re-pin safe_delete and help_all for sites_l= and the count-floor help text UPDATE_GOLDEN_EXPECT="safe_delete help_all" matched; the other 40 verbs keep their pins. safe_delete (--safe-delete=distance on test/fixture): each caller row gains sites_l= (lines 16 and 10) and the compact legend gains its one reading. help_all: the --safe-delete entry gains the unmodelled risk value, callers_floor=/uses_floor=/next= and sites_l=. Both are this lane's intended output; no printf-family conversion is involved. Co-Authored-By: Claude Opus 5.5 --- test/printf_parity.manifest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 41c8cfe8b..549d158e5 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -34,9 +34,9 @@ uses_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 96c grep_json 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 278827e2a7740aeeefa653b87ec47f74c98ec97a1758ce2e5426855a845a80c4 expand_missing 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 10f2dda880ec34c8d5a3f5bc50bd1bf50d0abe20c308de11ca8a16714f87b496 callers_missing 1 39a4cf4b2702863152edcc6c270255bac015a17d25e834dbdff181d809181326 0770e05552f4baf1c25d213e35c5f273c6f341e3c518e8233a83ff849abcd1c9 -safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +safe_delete 0 d010f9ca7e45d5bedcde9be49875bbc705632b3011cb9c8c4185f47957d51504 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 5e82bb6e129da4086c1e64f5939be191964e5db6e7c49154edfee9d1cfdc44e6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 81990af777932e931853a3867e50479a46e89a2dbf4cdef4c1493ab36428023f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 5d65c187ee4848653f294f4c1b15501fcef995c7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 07:45:10 -0400 Subject: [PATCH 065/229] test: docdemotecheck re-pins (f)/(h) goldens for the --for zero spelling Both goldens drift by exactly this lane's --for change: a lens row omits cx=/ccx=/in= when 0 and the legend reads an absent one as 0. With the zeros put back, the reading removed and est_tokens=/at= masked, the live answers equal the previous goldens byte for byte; the arms' assertions are unchanged. Co-Authored-By: Claude Opus 5.5 --- test/docdemotecheck.sh | 6 ++++++ test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 31bc84c78..c1afc2760 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,12 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-07 (lane/lean-answers-068, --for zero elision): docdemotegolden_for.xml 5821 -> 5505 B +# (est_tokens "2328" -> "2202") and docdemotegolden_noroute.xml 9423 -> 9092 B (est_tokens "3333" -> "3200"). +# ONE identified change: a lens row omits cx=/ccx=/in= when 0 and the legend says an absent one is 0. +# Verified before re-pinning: putting cx="0"/ccx="0"/in="0" back on every r= row, deleting the zero reading +# from the legend comment and masking est_tokens=/at=, live and previous goldens are byte-identical on BOTH +# fixtures — no ranking, demotion, route or row moved. (f)/(h)'s own assertions are unchanged. # RE-PIN 2026-09-13 (merge of lane/sc-legend and lane/for-widen): docdemotegolden_for.xml RE-MEASURED on the # MERGED tree at 5,809 B (est_tokens "2328"), from 5,887 on for-widen's tree and 5,425 on sc-legend's. Neither # lane's own number is the merged one, so this is measured, not summed. Three identified changes, and the golden diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 06e898765..1759cbf7b 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/docdemotegolden_noroute.xml b/test/docdemotegolden_noroute.xml index 9a699e69c..8c21b2dd2 100644 --- a/test/docdemotegolden_noroute.xml +++ b/test/docdemotegolden_noroute.xml @@ -1,4 +1,4 @@ -# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ): Date: Wed, 7 Oct 2026 08:07:10 -0400 Subject: [PATCH 066/229] cli: count the two new closed-value flag arms in the tripwire --whereis-listing= and --mcp-legend= are hand-written closed-value arms (the --grep-in= shape), so kHandWrittenFlagArms 24 -> 26 and kTotalFlagArms 215 -> 217; flagtablecheck reads both from parseArgs. Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.h b/src/cli.h index 5efc7bdc7..8d5e18f9f 100644 --- a/src/cli.h +++ b/src/cli.h @@ -3499,8 +3499,8 @@ inline constexpr IntFlag kIntFlags[] = // aliases that warn once per RUN, not per flag — state a BoolFlag row has // nowhere to keep) // • a bare no-op / bare pair --route, --quality-ack (the =REASON form is a kViewFlags row) -inline constexpr std::size_t kHandWrittenFlagArms = 24; // +1 #350 (2026-09-28): --max-memory= (byte-size arm, same shape as --max-file-size=); +1: --color-by= (enum-value arm); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (repeatable-value arms, same shape as --exclude=); +1 R-H: --grep-in= (closed-value arm, same shape as --grep-scope=); +1 lane/flag-biggest-first (2026-09-23): --readability (deprecation-warning alias, same shape as --stable/--most-important-last/--no-auto-order) — its kBoolFlags row is renamed to --biggest-first (same Config::readability member), so the old spelling moves OUT of the table and becomes a hand-written arm, per the house rule that deprecation-warning aliases stay hand-written -inline constexpr std::size_t kTotalFlagArms = 215; // +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it +inline constexpr std::size_t kHandWrittenFlagArms = 26; // +2 lane/lean-answers-068 (2026-10-07): --whereis-listing= and --mcp-legend= (closed-value arms, same shape as --grep-in=); +1 #350 (2026-09-28): --max-memory= (byte-size arm, same shape as --max-file-size=); +1: --color-by= (enum-value arm); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (repeatable-value arms, same shape as --exclude=); +1 R-H: --grep-in= (closed-value arm, same shape as --grep-scope=); +1 lane/flag-biggest-first (2026-09-23): --readability (deprecation-warning alias, same shape as --stable/--most-important-last/--no-auto-order) — its kBoolFlags row is renamed to --biggest-first (same Config::readability member), so the old spelling moves OUT of the table and becomes a hand-written arm, per the house rule that deprecation-warning aliases stay hand-written +inline constexpr std::size_t kTotalFlagArms = 217; // +2 lane/lean-answers-068 (2026-10-07): --whereis-listing= and --mcp-legend= (hand-written closed-value arms, see kHandWrittenFlagArms); +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it static_assert( std::size( kBoolFlags ) + std::size( kViewFlags ) + std::size( kIntFlags ) + kHandWrittenFlagArms == kTotalFlagArms, "a --flag arm was added or removed without updating the ledger above — count the arms in parseArgs and fix the counter" ); From 6542c18135bfe3cd01c71b4c18aeb200cf7dc782 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:07:10 -0400 Subject: [PATCH 067/229] test: forlenscheck reads the --for zero spelling; strict ccx arm on a nonzero row A lens row omits a zero cx=/ccx=/in= and the legend reads an absent one as 0. The strict "carries ccx" arm now reads a row whose ccx is not 0, and the zero row must carry ccx= or omit it under that reading (red when the reading is missing). Green on the base binary as well. Co-Authored-By: Claude Opus 5.5 --- test/forlenscheck.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/forlenscheck.sh b/test/forlenscheck.sh index 7dd58ccce..f88a88113 100755 --- a/test/forlenscheck.sh +++ b/test/forlenscheck.sh @@ -36,7 +36,14 @@ dline(){ printf '%s' "$1" | sed 's/>\n]*>$2" | head -1; # ── 1) the --for bundle carries the quality lens on its ranked blocks (ccx + amp) ───────────────── FOR1="$( "$BIN" test/fixture --no-cache --for="compute perimeter distance" 2>/dev/null )" DL="$( dline "$FOR1" 'double distance' )" -if printf '%s' "$DL" | grep -q ' ccx='; then ok "--for carries ccx (cognitive complexity): $DL"; else no "--for missing ccx: $DL"; fi +# lean-answers lane (by design): a lens row omits a zero cx=/ccx=/in= and the legend reads an absent one as 0. The strict +# "carries ccx" arm reads a row whose ccx is NOT 0 (perimeter, ccx=1); the distance row (ccx 0) carries ccx= or omits it +# under the "absent = 0" reading — never omits it silently. +DLP="$( dline "$FOR1" 'double perimeter' )" +if printf '%s' "$DLP" | grep -q ' ccx="[1-9]'; then ok "--for carries ccx (cognitive complexity) on a nonzero row: $DLP"; else no "--for missing a nonzero ccx: $DLP"; fi +if printf '%s' "$DL" | grep -q ' ccx='; then ok "--for carries ccx (cognitive complexity): $DL" +elif [ -n "$DL" ] && printf '%s' "$FOR1" | grep -qE 'absent cx/ccx/in = 0|each absent when 0'; then ok "--for omits a zero ccx and the legend reads absent as 0: $DL" +else no "--for missing ccx with no absent = 0 reading: $DL"; fi if printf '%s' "$DL" | grep -q ' amp='; then ok "--for carries amp (change-amplification)"; else no "--for missing amp: $DL"; fi # ── 2) GOLDEN NEUTRALITY — plain --pack-signatures must NOT carry any lens attr; default map unchanged ─ From 2d28d6d9cd0972a49a82e84a77d731d32f166c66 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:21:30 -0400 Subject: [PATCH 068/229] fix(impact): a long selector's cut import tier keeps its importers_next= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sizeImportTier offered importers_next= only while `--impact=SYM --limit=N` fit kNextAttrMaxBytes (120 B), so a cut tier whose symbol name was long lost its only continuation with no marker (importers_capped="1" and nothing that serves the rest). That was the last producer still dropping a next= for length after the 2026-09-25 ruling (138b383c: next= is emitted in full, never truncated or dropped). The invocation is now always emitted in full. kNextAttrMaxBytes stays, re-documented as what it is: the typical size the short-fixture gates hold their own next= to, not a runtime ceiling. Stale include comments that named it are corrected. Gate: impactimportcheck.sh #9d — a 110-character selector (a 130 B call): the XML root, --json and the MCP impact twin carry the full importers_next=, pasting it serves all 48 importers, and an uncut tier carries none. RED on 255dc199 (4 FAIL), GREEN here. Co-Authored-By: Claude Opus 5.5 --- src/flipimpact.h | 2 +- src/forpage.h | 2 +- src/graph.h | 15 +++++++------- src/nextverb.h | 8 +++++++- test/impactimportcheck.sh | 43 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/flipimpact.h b/src/flipimpact.h index 448aa5483..cb00fe138 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -71,7 +71,7 @@ #include "serialize.h" // escapeXml #include "testmap.h" // M21(b): TestRunnerIndex / runAttrDisclosed — the ONE run= hint the tests_to_run family shares #include "pageview.h" // §P8: pageWindow / effectiveRowCap / secondaryCutAttrs — the ONE paging contract -#include "nextverb.h" // P3: nextAttrXml / kNextAttrMaxBytes — the ONE pasteable follow-up +#include "nextverb.h" // P3: nextAttrXml — the ONE pasteable follow-up #include "infra/Diagnostics.h" // DISCLOSE #include "btree.hpp" // gtl::btree_map — sorted iteration (house rule: never std::map) diff --git a/src/forpage.h b/src/forpage.h index 192902ba1..859149442 100644 --- a/src/forpage.h +++ b/src/forpage.h @@ -41,7 +41,7 @@ #include "lexical.h" // LexTermEvidence — the term masks + df the BM25 pass already accumulated #include "model.h" -#include "nextverb.h" // nextFlag / nextAttrXml / kNextAttrMaxBytes — the ONE next= spelling +#include "nextverb.h" // nextFlag / nextAttrXml — the ONE next= spelling #include "pageview.h" // pageWindow / pageDisclosure — the shared --limit/--offset vocabulary #include "serialize.h" // escapeXml, lensRowPath, ctxRootOpen, radixSortByScoreDescId #include "testmap.h" // splitChangedFilesOfSymbols — the ONE "distinct files of a symbol set" walk (--affected's) diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..099ebe67c 100644 --- a/src/graph.h +++ b/src/graph.h @@ -7,7 +7,7 @@ #include "elixir_resolve.h" // lexical module/name/arity resolution; reuses cached Binding records #include "filter.h" // isTestPath — for the Q2 tested= post-pass #include "pageview.h" // LB-H: kImportReachRowCap — the import tier's display cap lives with the rest of the truncation vocabulary -#include "nextverb.h" // cut-fix E: nextFlag / nextAttrXml / kNextAttrMaxBytes — the import tier's importers_next= +#include "nextverb.h" // cut-fix E: nextFlag / nextAttrXml — the import tier's importers_next= #include "graphlegend.h" // M15: graphGaugeAttrXml/Json + kGraphCountFloorAttrXml/Json — graphCountFloorAttrXml( g ) below #include "lintrules.h" // §P9.4: langOfPath / dependencyCapable — the file-language classification // restrictDependencyHealth() needs (owns the extension table, kept in sync @@ -7904,8 +7904,11 @@ struct ImportTier // cut-fix E: `sym` is the answer's own selector. A cut tier was a DEAD-END cut (docs/research/answer-completeness.md // §1.3): counted, but naming no call that serves the rest, although one exists — the tier is sized by limit, so // `--impact=SYM --limit=` lists all of it. That call rides as importers_next= (the root's next= is taken -// by --safe-delete), only on a cut, and not when the invocation would pass kNextAttrMaxBytes (a hint that pastes -// wrong is worse than none — forpage.h's rule). Both surfaces spell the CLI flag, as their root next= already does. +// by --safe-delete), only on a cut, and IN FULL whatever its length (knob-honesty-068): it used to be dropped past +// kNextAttrMaxBytes, so a long selector's cut tier lost its only continuation with no marker — the silent drop the +// 2026-09-25 ruling (nextverb.h: next= is never truncated or dropped for length) removed from every other producer. +// The invocation is never truncated, so it never pastes wrong. Both surfaces spell the CLI flag, as their root next= +// already does. Gate: impactimportcheck.sh #9d (a 130 B call). inline void sizeImportTier( ImportTier& t, int pageLimit, std::string_view sym = {} ) { t.shown = std::min( t.files.size(), std::size_t( rw::effectiveRowCap( pageLimit, rw::kImportReachRowCap ) ) ); @@ -7913,11 +7916,7 @@ inline void sizeImportTier( ImportTier& t, int pageLimit, std::string_view sym = t.next.clear(); if( t.capped && !sym.empty() ) { - std::string inv = rw::nextFlag( "--impact=", sym ) + " --limit=" + std::to_string( t.files.size() ); - if( inv.size() <= rw::kNextAttrMaxBytes ) - { - t.next = std::move( inv ); - } + t.next = rw::nextFlag( "--impact=", sym ) + " --limit=" + std::to_string( t.files.size() ); } // Emitted UNCONDITIONALLY, zero included: an absent importers= reads as "this build cannot measure it", // and a shown_ without its capped= is the missing-attribute ambiguity pageview.h rule 3 forbids. diff --git a/src/nextverb.h b/src/nextverb.h index ff193bf86..3aab07603 100644 --- a/src/nextverb.h +++ b/src/nextverb.h @@ -6,7 +6,7 @@ // --test-gate/--safe-delete/--situ handed the agent nothing, so "contract-change → find the sites → open the // file" was three calls with the middle one guessed. The owner's frame for this lane: an answer should TERMINATE // the search in one compact shot — so every root in P3's enumeration carries exactly one next= holding a -// pasteable invocation of ≤ kNextAttrMaxBytes: --edit-check contract-change → --uses=SYM; --impact → +// pasteable invocation, typically ≤ kNextAttrMaxBytes and never dropped past it: --edit-check contract-change → --uses=SYM; --impact → // --safe-delete=SYM; a gating --quality-delta row → --expand=FILE:NAME; --test-gate → its first run= command; // --situ → --test-gate; --from-trace/--run-trace → --slice=@FILE:LINE (the innermost in-corpus frame); // --callers → --uses=SELECTOR (bare NAME for a narrowed selector with declined calls; otherwise the spelling is mirrored); @@ -24,6 +24,12 @@ namespace rw { +// NOT A RUNTIME CEILING (knob-honesty-068). No producer drops or truncates a next= for its length: nextAttrXml has no +// length branch (138b383c, 2026-09-25 ruling: a dropped next= leaves a cut with no way back, and an untruncated one never +// pastes wrong), and the last pre-filter — graph.h's importers_next= — went with this lane. What remains is the TYPICAL +// size the short-fixture gates hold their own next= to (nextverbcheck.sh, forsectioncollapsecheck.sh (5a)): a compactness +// target for the common case, never a reason to omit a continuation. A producer that wants a shorter next= must spell +// a shorter equivalent call, not drop it. inline constexpr std::size_t kNextAttrMaxBytes = 120; // the ONE definition every legend that meets next= splices (legendcoveragecheck: an attribute is defined where diff --git a/test/impactimportcheck.sh b/test/impactimportcheck.sh index 74d0d73ac..ea9e08830 100755 --- a/test/impactimportcheck.sh +++ b/test/impactimportcheck.sh @@ -316,6 +316,49 @@ LCUT="$( attr lens "$( ri --format=columnar )" )"; LALL="$( attr lens "$( ri --l || no "next: --format=columnar lens='$LCUT' on the cut tier (want shown_importers,importers_capped,importers_next), lens='$LALL' uncut (want shown_importers,importers_capped)" rm -rf "$RS" +# ── #9d A LONG SELECTOR KEEPS ITS CALL (knob-honesty-068) ─────────────────────────────────────────────── +# sizeImportTier offered importers_next= only while the invocation was <= kNextAttrMaxBytes (120 B), so a cut tier +# whose symbol name is long lost its ONLY continuation with no marker: importers_capped="1" and nothing that serves +# the rest — the silent drop the 2026-09-25 ruling (138b383c: "emit the FULL next=") removed everywhere else. Same +# 48-importer shape as #9b with a 110-character function name: `--impact=NAME --limit=48` is 130 B. RED on 255dc199 +# (no importers_next= on the XML root, the --json dialect, or the MCP twin). +LS="$( mktemp -d )"; mkdir -p "$LS/pkg" +LONGN="importHubFn_$( printf 'x%.0s' $( seq 1 98 ) )" +python3 - "$LS" "$LONGN" <<'PY' +import os, sys +d, n = os.path.join( sys.argv[1], "pkg" ), sys.argv[2] +open( os.path.join( d, "__init__.py" ), "w" ).write( "" ) +open( os.path.join( d, "hub.py" ), "w" ).write( "def %s( x ):\n return x\n" % n ) +for i in range( 48 ): + open( os.path.join( d, "a_%02d.py" % i ), "w" ).write( "from pkg.hub import %s\n\ndef use_a_%02d( ):\n return %s( %d )\n" % ( n, i, n, i ) ) +PY +rl(){ perl -e 'alarm 30; exec @ARGV' "$BIN" "$LS" --impact="$LONGN" --no-cache "$@" 2>/dev/null; } +L_DEF="$( rl )" +WANT_L="--impact=$LONGN --limit=48" +{ [ "${#LONGN}" = 110 ] && [ "${#WANT_L}" -gt 120 ] && [ "$( attr importers_capped "$L_DEF" )" = 1 ]; } \ + && ok "long: presence guard — a ${#LONGN}-char selector, a ${#WANT_L} B call (> 120), importers_capped=1" \ + || no "long: fixture broken — name ${#LONGN} chars, call ${#WANT_L} B, importers_capped='$( attr importers_capped "$L_DEF" )'" +[ "$( attr importers_next "$L_DEF" )" = "$WANT_L" ] \ + && ok "long: the cut tier still names its call (importers_next= is ${#WANT_L} B, never dropped for length)" \ + || no "long: importers_next='$( attr importers_next "$L_DEF" )' on a cut tier (want the full ${#WANT_L} B call)" +L_NX="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$LS" $( attr importers_next "$L_DEF" ) --no-cache 2>/dev/null )" +{ [ "$( attr shown_importers "$L_NX" )" = 48 ] && [ "$( attr importers_capped "$L_NX" )" = 0 ]; } \ + && ok "long: pasting the long importers_next= serves all 48 importers" \ + || no "long: pasting importers_next= gave shown_importers='$( attr shown_importers "$L_NX" )'" +rl --json | grep -qF "\"importers_next\":\"$WANT_L\"" \ + && ok "long: the --json dialect carries the same full importers_next" \ + || no "long: --json lacks the full \"importers_next\" on the cut tier" +L_MCP="$( printf '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"impact","arguments":{"path":"%s","symbol":"%s"}}}\n' "$LS" "$LONGN" \ + | perl -e 'alarm 60; exec @ARGV' "$BIN" --mcp 2>/dev/null | tail -1 )" +printf '%s' "$L_MCP" | grep -qF "importers_next=\\\"$WANT_L\\\"" \ + && ok "long: the MCP impact twin carries the same full importers_next" \ + || no "long: MCP impact lacks importers_next on the cut tier: $( printf '%s' "$L_MCP" | grep -oE 'importers_[a-z]+=[^ ]+' | tr '\n' ' ' | cut -c1-200 )" +# negative twin: an UNCUT long tier carries no importers_next= (the follow-up rides a cut only) +[ -z "$( attr importers_next "$( rl --limit=100 )" )" ] \ + && ok "long: the uncut tier (--limit=100) carries no importers_next=" \ + || no "long: an uncut tier carries importers_next=" +rm -rf "$LS" + # ── #10 determinism + well-formedness ───────────────────────────────────────────────────────────────── A="$( i Widget )"; B="$( i Widget )" if [ "$A" = "$B" ]; then ok "determinism: --impact=Widget byte-identical run-to-run"; else no "non-deterministic --impact output"; fi From c70fc81ed3d753be25070558d0e660cb2dfe530c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:21:41 -0400 Subject: [PATCH 069/229] docs(skills): ripwire-handoff passes --for the flag it reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 1 recommended `--for="SUBSYSTEM" --top-k=20`. --for does not read --top-k: the binary warns on stderr and serves the whole bundle, so the 20 narrowed nothing. The step now spells --signatures-only (README's own replacement for that pattern), which skips the inline bodies the next step expands anyway, and says why --top-k is gone. Checked every other shipped command line the same way: all 159 `ripwire --…` lines in skills/ (and the wrap.h recipes) run through the binary with no "not read by" warning; this was the only one. Co-Authored-By: Claude Opus 5.5 --- skills/ripwire-handoff/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skills/ripwire-handoff/SKILL.md b/skills/ripwire-handoff/SKILL.md index 2b27e2bcb..1f4427c43 100644 --- a/skills/ripwire-handoff/SKILL.md +++ b/skills/ripwire-handoff/SKILL.md @@ -20,9 +20,11 @@ fast, accurate brief — not a wall of source code. `` = repo root. `SUBSYSTEM` = the area in plain words (e.g. "ingest pipeline", "graph ranking", "MCP server loop"). -1. **Task-relevant symbols** — `ripwire --for="SUBSYSTEM" --top-k=20` +1. **Task-relevant symbols** — `ripwire --for="SUBSYSTEM" --signatures-only` Output: `` ranked by relevance. The top 10 are the symbols the recipient most needs to know. Note their file paths, complexity (`cx=`), and reuse count (`in=`). + (`--top-k` does not narrow `--for` — the binary says so on stderr and serves the whole list; + `--signatures-only` skips the inline bodies, since step 2 expands the ones you pick.) 2. **Expand the key symbols** — `ripwire --expand=SYM1,SYM2,SYM3 --legend=compact` (Pick the top 3 by rank from step 1.) From 0c429a244b4795dcd6c474e844f1b25cde33389e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:21:41 -0400 Subject: [PATCH 070/229] feat(lint): a floored rule names the call that counts the rest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rule that spent its per-rule match budget (kLintMaxPerRule = 5000) was disclosed — count_capped="1" on its row, findings_capped="1" on the root, findingsCapped in SARIF — but the budget had no flag, so the floor was a dead end on the CLI and in the SARIF a CI reads. - --lint-max-per-rule=N (with --lint / --lint-rules) sets the budget; the default is unchanged (5000), refused alone and at 0. The atoms/cache packs keep their engine budget at 20x the per-rule one (the default ratio), so a raised budget lifts their floors too instead of leaving them floored by a budget the flag cannot reach. - A floored answer carries findings_next= on the root (SARIF: run properties findingsNext): the floored rules only (--lint-select), under a 10x budget, keeping --lint-rules=DIR and --naming-locals when given. A re-run still floored names its own. Present-only full-dialect clause; compact term registered. Gate: lintbudgetcheck.sh arm 5 — a 6000-goto fixture floors at 5000 with the call named; pasting it counts 6000 and carries neither marker; --lint-max-per-rule=4 floors 10 at 4; refusals; SARIF twin; user rules keep their directory. RED on 255dc199, GREEN here. Co-Authored-By: Claude Opus 5.5 --- src/atoms.h | 8 ++- src/cachelint.h | 7 ++- src/cli.h | 19 ++++++- src/compactlegend.h | 4 ++ src/lintrules.h | 23 ++++++-- src/sarif.h | 8 +++ src/verbs_lint.h | 116 ++++++++++++++++++++++++++++++++++------ test/lintbudgetcheck.sh | 69 ++++++++++++++++++++++++ 8 files changed, 229 insertions(+), 25 deletions(-) diff --git a/src/atoms.h b/src/atoms.h index ae43b412d..31263de57 100644 --- a/src/atoms.h +++ b/src/atoms.h @@ -509,12 +509,16 @@ using atomdetail::atomsSpecs; // and text tests over the captures. Deterministic: astQuery returns (file path, startByte, endByte, // tag) order and every filter below is order-preserving, so no hash iteration, thread arrival order, // or wall clock can reach a row. -inline AtomsRun atomsOfConfusionFromCaptures( const IngestResult& ing, std::size_t maxPerRule, std::vector ms ) +// knob-honesty-068: `queryBudget` is the engine budget the captures were collected under — the default kAtomsQueryBudget, +// or the scaled one a raised --lint-max-per-rule asks for (lintPackQueryBudget) — so the exclusion stream's saturation is +// judged against the budget that actually cut it. +inline AtomsRun atomsOfConfusionFromCaptures( const IngestResult& ing, std::size_t maxPerRule, std::vector ms, + std::size_t queryBudget = kAtomsQueryBudget ) { using namespace atomdetail; AtomsRun run; - const Exclusions ex = collectExclusions( ing, ms, kAtomsQueryBudget ); + const Exclusions ex = collectExclusions( ing, ms, queryBudget ); std::vector kept = applyExclusions( ing, ms, ex ); std::vector rows = collapseNestedTernaries( ing, kept ); diff --git a/src/cachelint.h b/src/cachelint.h index 864535321..df4acb5bc 100644 --- a/src/cachelint.h +++ b/src/cachelint.h @@ -292,7 +292,10 @@ using cachedetail::cacheSpecs; // built-in [AST] checks and the atoms pack (astQueryGrouped, src/ingest.h). The pack does not run its own // pass: `--lint` is its only caller, and a second full read+parse of the corpus to ask this pack's // questions of trees the same run already built was most of what made the verb slow. -inline CacheRun cacheFriendliness( const IngestResult& ing, std::size_t maxPerRule, std::vector ms ) +// knob-honesty-068: `queryBudget` is the engine budget the captures were collected under (kCacheQueryBudget by default; scaled +// with a raised --lint-max-per-rule, lintPackQueryBudget), so the loop stream's saturation is judged against the budget that cut it. +inline CacheRun cacheFriendliness( const IngestResult& ing, std::size_t maxPerRule, std::vector ms, + std::size_t queryBudget = kCacheQueryBudget ) { using namespace cachedetail; @@ -306,7 +309,7 @@ inline CacheRun cacheFriendliness( const IngestResult& ing, std::size_t maxPerRu // Loop containment universe, then the fence: the loop stream is stripped and the three in-loop // rules keep only rows a loop region contains. const LoopIndex loops( ing, ms ); - const bool loopStreamSaturated = loops.loopRows >= kCacheQueryBudget; + const bool loopStreamSaturated = loops.loopRows >= queryBudget; std::vector eligible; eligible.reserve( ms.size() ); for( AstMatch& m : ms ) diff --git a/src/cli.h b/src/cli.h index 373fd9ed9..aef45193f 100644 --- a/src/cli.h +++ b/src/cli.h @@ -154,6 +154,9 @@ struct Config // these (or "*" for all) — modifies --lint / --lint-rules; refused alone. std::string_view lintIgnore; // --lint-ignore=PREFIX[,...]: drop rules whose name starts with one of these // (or "*" to drop everything) — applied AFTER --lint-select narrows the set. + int lintMaxPerRule = 0; // --lint-max-per-rule=N: each rule's raw-capture budget (0 = kLintMaxPerRule, the + // default runaway guard). knob-honesty-068: the floored answer's findings_next= names + // it, so a count_capped="1" floor is one pasteable call from its total. std::vector expand; // --expand=NAME,...: emit full def bodies for these symbols; NAME:START-END slices to 1-based lines START..END of the def (octocode partial-fetch) std::vector outline; // --outline=NAME,...: control-flow skeletons (L3 scoped snippet) std::string_view forTask; // --for=TASK: the task lens — ranked signatures inventory + metrics, framed for reuse @@ -1647,6 +1650,12 @@ inline constexpr char kHelpHead[] = " (with --lint / --lint-rules) DROP rules whose name starts with one of these PREFIXes (or '*' to\n" " drop everything, e.g. paired with --lint-select elsewhere to isolate one family) — applied AFTER\n" " --lint-select narrows the set; same unresolvable-PREFIX refusal and root disclosure as --lint-select\n" + " --lint-max-per-rule=N (with --lint) raise each rule's match budget above the default 5000\n" + " (with --lint / --lint-rules) each rule's raw-capture budget; the default (5000) is a runaway\n" + " guard, not a target. A rule that spends it carries count_capped=\"1\" (its count= is a FLOOR)\n" + " and the root names the call that counts the rest: findings_next= (SARIF: run properties\n" + " findingsNext) re-runs only the floored rules under a 10x budget; a re-run still floored\n" + " names its own. Raising it costs time and memory in proportion to the matches kept\n" " --sarif (with --lint / --lint-rules) the SAME findings as SARIF 2.1.0 instead of the native XML\n" " block — the shape github/codeql-action/upload-sarif consumes for code scanning.\n" " Pure re-serialization (zero new analysis); results count == the native run's findings\n" @@ -3430,6 +3439,9 @@ inline constexpr IntFlag kIntFlags[] = { "--pack-top-n=", &Config::packTopN, false, kIntFlagMax, "a positive integer", "--pack-top-n=10", "ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchanged behavior for now)\n" }, { "--detail=", &Config::detail, true, kIntFlagMax, "a non-negative integer (0 = off)", "--detail=2" }, + // knob-honesty-068: the per-rule lint budget (kLintMaxPerRule is the default); a modifier of --lint/--lint-rules, + // refused alone (validateLintSelectionModifierGuards) — the floored answer's findings_next= is the call that spells it. + { "--lint-max-per-rule=", &Config::lintMaxPerRule, false, kIntFlagMax, "a positive integer", "--lint-max-per-rule=50000" }, // --cochange-recur=K: K is a count of SUB-WINDOWS, so it is bounded by kCoRecurSubWindows in practice; // the parser accepts any positive integer and the verb reports zero pairs above the ceiling rather than // refusing — an empty result under a disclosed min_recur= is a truthful answer, not an error. @@ -3483,7 +3495,7 @@ inline constexpr IntFlag kIntFlags[] = // nowhere to keep) // • a bare no-op / bare pair --route, --quality-ack (the =REASON form is a kViewFlags row) inline constexpr std::size_t kHandWrittenFlagArms = 24; // +1 #350 (2026-09-28): --max-memory= (byte-size arm, same shape as --max-file-size=); +1: --color-by= (enum-value arm); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (repeatable-value arms, same shape as --exclude=); +1 R-H: --grep-in= (closed-value arm, same shape as --grep-scope=); +1 lane/flag-biggest-first (2026-09-23): --readability (deprecation-warning alias, same shape as --stable/--most-important-last/--no-auto-order) — its kBoolFlags row is renamed to --biggest-first (same Config::readability member), so the old spelling moves OUT of the table and becomes a hand-written arm, per the house rule that deprecation-warning aliases stay hand-written -inline constexpr std::size_t kTotalFlagArms = 215; // +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it +inline constexpr std::size_t kTotalFlagArms = 216; // +1 knob-honesty-068 (2026-10-07): --lint-max-per-rule= (kIntFlags row) — the per-rule lint budget a floored answer's findings_next= names; +1 lane/mcp-tool-profile-065 (2026-09-26): --mcp-tools= (kViewFlags row) — list only a subset of the MCP tools (names and/or the core/full profiles); +1 #350 (2026-09-28): --max-memory= (hand-written byte-size arm, see kHandWrittenFlagArms); +1 lane/flag-biggest-first (2026-09-23): --readability renamed to --biggest-first, kept working as a hand-written deprecated alias (see kHandWrittenFlagArms); +1 lane/r1-for-sections-stub (2026-09-19, L2/B1): --sections= (kViewFlags row) — the closed-set opt-in that restores the / sections --for collapses to a counted stub by default; +1 --lsp (kBoolFlags row, 2026-09-15): the navigation LSP server stdio entry point — Phase 1 PoC, docs/LSP.md; +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it static_assert( std::size( kBoolFlags ) + std::size( kViewFlags ) + std::size( kIntFlags ) + kHandWrittenFlagArms == kTotalFlagArms, "a --flag arm was added or removed without updating the ledger above — count the arms in parseArgs and fix the counter" ); @@ -4240,6 +4252,11 @@ inline void validateLintSelectionModifierGuards( Config& c ) noexcept rw::emitRaw( stderr, "ripwire: --lint-ignore=PREFIX modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-ignore=naming-)\n" ); c.ok = false; } + if( c.lintMaxPerRule > 0 && !c.lint && c.lintRulesDir.empty() ) + { + rw::emitRaw( stderr, "ripwire: --lint-max-per-rule=N modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-max-per-rule=50000)\n" ); + c.ok = false; + } } // ── THE LEGEND POSTURES (L1, round 1 of the answer-size loop, 2026-09-19) ──────────────────────────────── diff --git a/src/compactlegend.h b/src/compactlegend.h index 4daa36d97..ec8aafb69 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -227,6 +227,8 @@ inline constexpr std::string_view kCompactProsePrefixes[] = "" ); + if( !findingsNext.empty() ) + { + // present-only (absent ⇒ nothing floored), like the nest_refused= clause below; compactlegend.h restates it + lintPrintOut( "" ); + } if( lintNestRefused ) { lintPrintOut( "" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare @@ -2252,7 +2258,8 @@ inline std::optional forTaskText( const std::string& root, const st } // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). - if( const std::string cutNotes = rw::sigsCutLegendNotes( mcpSigsCut.isCapped, mcpSigsCut.shown, mcpSigsCut.total, mcpSigsCut.docsDropped ); + if( const std::string cutNotes = rw::sigsCutLegendNotes( mcpSigsCut.isCapped, mcpSigsCut.shown, mcpSigsCut.total, mcpSigsCut.docsDropped, + !mcpSigsCut.next.empty() ); !cutNotes.empty() ) { const std::size_t closeAt = headerStr.rfind( " -->" ); diff --git a/src/nextverb.h b/src/nextverb.h index 3aab07603..d52d7635e 100644 --- a/src/nextverb.h +++ b/src/nextverb.h @@ -79,11 +79,14 @@ inline std::string nextAttrXml( std::string_view invocation, std::string_view at return a; } -// the JSON twin: `,"next":"…"` with the JSON escapes a flag value can need -inline std::string nextFieldJson( std::string_view invocation ) +// the JSON twin: `,"next":"…"` with the JSON escapes a flag value can need; `key` names a SECONDARY listing's own +// follow-up, as nextAttrXml's `attr` does (knob-honesty-068: --for --json's "sigs_next") +inline std::string nextFieldJson( std::string_view invocation, std::string_view key = "next" ) { if( invocation.empty() ) { return {}; } - std::string a = ",\"next\":\""; + std::string a = ",\""; + a += key; + a += "\":\""; for( const char c : invocation ) { switch( c ) diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..6a37d3e92 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -865,14 +865,23 @@ inline constexpr std::string_view kForSigsShrunkNote = static_assert( kForDocExcerptRankCount == 24 && kForDocFullRankCount == 12, "kForDocsDroppedNote spells the tier thresholds (r>24; the ladder's r5..24): re-word it with these constants" ); -// The clauses a cut report owes, concatenated in a fixed order ("" when it owes none). -inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped ) +// • next= on (knob-honesty-068): the call that serves the block uncut (sigsCutNextInvocation). Not the r=1 row's +// next= (a body or the widening page), so it says which rows it serves. +inline constexpr std::string_view kForSigsNextNote = " [sigs next=: one call serving all total= rows uncut]"; + +// The clauses a cut report owes, concatenated in a fixed order ("" when it owes none). `hasNext`: the tag carries +// the cut's continuation (SigsCutReport::next). +inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped, bool hasNext = false ) { std::string notes; if( isCapped && shown == total ) { notes += kForSigsShrunkNote; } + if( isCapped && hasNext ) + { + notes += kForSigsNextNote; + } if( docsDropped > 0 ) { notes += kForDocsDroppedNote; @@ -4476,8 +4485,71 @@ struct SigsCutReport std::size_t total = 0; std::size_t docsDropped = 0; bool isCapped = false; + std::string next; // knob-honesty-068: the cut's continuation (sigsCutNextInvocation), set only on a capped + // block whose caller asked for one; the tag's next= / the JSON root's "sigs_next" }; +// ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── +// A capped said shown=/total= and named no call that serves the rows it cut: the only next= in the bundle was the +// r=1 row's (a body, or the file-grain widening page), so the trimmed tail was a dead end. The block now carries its own +// next= — the SAME ranked lens re-run with the sig side widened until nothing is cut: +// --for=TASK --signatures-only [RANKING FLAGS] [--pack-top-n=N] --token-budget=T [--json] +// --signatures-only gives the signatures the WHOLE explicit ceiling (verbs_for.h forSigSideCeiling: no auto-body share), +// and T is sized from THIS block's untrimmed bytes plus the bytes the caller says ride beside it (`fixedBytes`), at the +// budget conversion's own rate, rounded up to a 100-token step — a ceiling, so over-sizing it costs nothing (the re-run is +// as big as its content, bounded by the same candidate pool). --pack-top-n rides only when the caller set it (it sets +// total=). The value depends on the task and the untrimmed rows only — never on the ceiling that cut them — so the block +// stays byte-identical at the default and at any explicit ceiling above it (forbudgetmonotoncheck's invariant). +// EXEMPT FROM THE SIG TRIM, in every regime — the e= / docs_after_code convention (a disclosure never costs a row the ceiling +// did not already cut): the plan never reserves it, so the ranked set is the one it was at every ceiling. Measured before +// choosing (knob-honesty-068 report): charging it like shown=/total= cost one row on 11 of 20 queries at the wrap recipe's +// --token-budget=2000. Its bytes stay real downstream — est_tokens measures them, and the explicit-ceiling header ladder +// and over_ceiling= see them — so an overshoot is disclosed, never hidden. +// COMPATIBLE WITH #362 (the budgeted-bundle candidate page, issue #294): when that page lands, the resumable form is +// `--for=TASK --token-budget=N --limit=L --offset=`, which serves ONLY the cut rows; it replaces the VALUE built +// here, not the attribute (same element, same name — #362's page root carries its own continuation as next= on ). +struct SigsCutContinuation +{ + std::string_view task; // the --for task, echoed verbatim (nextFlag quotes it for a shell) + int packTopN = 0; // the caller's explicit --pack-top-n (0 = the default pool): it sets total=, so it is echoed + std::string_view rankFlags = {}; // the caller's RANKING flags, pre-spelled (" --no-route --adaptive"…): the re-run must rank + // the same set, or "the rows this block cut" are rows of a different list + std::size_t fixedBytes = 0; // what the re-run's ceiling must carry besides the block: the charged header + sibling blocks + bool json = false; // the --json dialect: the re-run is --json too +}; + +inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept +{ + constexpr std::size_t kSlackBytes = 512; // the re-run's own spliced attributes (est_tokens=, budget_tokens=, over-reserves) + constexpr std::size_t kTokenStep = 100; + const std::size_t needBytes = blockBytes + fixedBytes + kSlackBytes; + std::size_t tokens = std::size_t( double( needBytes ) / ( kMinBytesPerToken * kBudgetHeadroom ) ) + 1; + tokens = ( tokens + kTokenStep - 1 ) / kTokenStep * kTokenStep; + while( budgetBytesForTokens( tokens ) < needBytes ) // the conversion floors; never hand back a ceiling below the need + { + tokens += kTokenStep; + } + ENSURES( budgetBytesForTokens( tokens ) >= needBytes ); + return tokens; +} + +inline std::string sigsCutNextInvocation( const SigsCutContinuation& c, std::size_t blockBytes ) +{ + std::string inv = nextFlag( "--for=", c.task ); + inv += " --signatures-only"; + inv += c.rankFlags; + if( c.packTopN > 0 ) + { + inv += " --pack-top-n=" + std::to_string( c.packTopN ); + } + inv += " --token-budget=" + std::to_string( sigsCutNextTokens( blockBytes, c.fixedBytes ) ); + if( c.json ) + { + inv += " --json"; + } + return inv; +} + inline std::size_t sigsDecimalDigits( std::size_t n ) noexcept { std::size_t d = 1; @@ -4554,6 +4626,7 @@ inline std::string sigsOpenTag( const SigsCutReport& cut ) rw::formatTo( nb, sizeof( nb ), " docs_dropped=\"{}\"", cut.docsDropped ); tag += nb; } + tag += nextAttrXml( cut.next ); // knob-honesty-068: the cut's continuation ("" unless capped and asked for) tag += ">"; return tag; } @@ -4634,9 +4707,11 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: std::string_view topRowNext = {}, // L-W (forpage.h): the r=1 row's next= when the caller // judged the answer THIN (the widening page); "" ⇒ the // --expand body follow-up, byte-identical to before. - SigsCutReport* cutOut = nullptr ) // cut-fix lane A: the tag's shown/total/docs_dropped/capped, + SigsCutReport* cutOut = nullptr, // cut-fix lane A: the tag's shown/total/docs_dropped/capped, // for the caller's legend splices. Lens path only; zeroed // (nothing cut) on every other path. + const SigsCutContinuation* cutNext = nullptr ) // knob-honesty-068: a capped block's next= (the + // --for lens and its MCP twin); nullptr ⇒ none, byte-identical { if( cutOut ) { @@ -4924,6 +4999,9 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // the tag's own attributes cost bytes — budget the trimmed state INCLUDING them (planSigsTrim; capture-audit // 2026-09-04 for the shown=/total= marker, cut-fix lane A for docs_dropped=). + // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block, and NOT reserved by the + // plan (SigsCutContinuation: exempt from the sig trim, so the rows shown are the ones they were) + const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); if( plan.ladderFires ) @@ -4969,7 +5047,11 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { *cappedOut = plan.ladderFires; } - const SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + if( cut.isCapped ) + { + cut.next = cutNextInv; // "" when the caller asked for none + } if( cutOut ) { *cutOut = cut; @@ -9246,9 +9328,11 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s std::size_t* droppedPositiveOut = nullptr, // A2: the XML sibling's own out-param — // see packSignatures for the full contract. std::vector* shownIdsOut = nullptr, // lane 2: the emitted rows' ids — see packSignatures - SigsCutReport* cutOut = nullptr ) // cut-fix lane A: the XML tag's shown/total/docs_dropped/ + SigsCutReport* cutOut = nullptr, // cut-fix lane A: the XML tag's shown/total/docs_dropped/ // capped, for the caller's root keys (sigs_shown/ // sigs_total/docs_dropped); see packSignatures + const SigsCutContinuation* cutNext = nullptr ) // knob-honesty-068: the XML twin's next=, as the + // caller's "sigs_next" root key; see packSignatures { const bool rankAdaptivePayload = lens.rankAdaptivePayload; if( outCapped ) @@ -9340,6 +9424,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // reserved INSIDE this array's budget exactly as the XML tag's attributes are inside its block's — they used to be // absent, and a JSON consumer could not tell 24 rows of 40 from 24 of 24. // (Without the rank-adaptive payload every globalRank is 0: no row is droppable and no doc is ever removed.) + // knob-honesty-068: the "sigs_next" key, exempt from the trim exactly as the XML twin's next= (SigsCutContinuation) + const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); if( plan.ladderFires ) @@ -9353,6 +9439,10 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s if( cutOut ) { *cutOut = sigsCutReportOf( entries, totalRows, plan ); + if( cutOut->isCapped ) + { + cutOut->next = cutNextInv; // knob-honesty-068: "" when the caller asked for none + } } if( droppedPositiveOut ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index dfe6a6967..62617364b 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -422,6 +422,11 @@ struct ForLensJsonInputs // MainDispatch) — REQUIRED like fileTail above, for the same reason: a defaulted bool is how a degrade // silently drops off this dialect while the XML twin keeps carrying it. bool notesDegraded; + // knob-honesty-068: what the "sigs_next" continuation echoes (serialize.h SigsCutContinuation) — REQUIRED like the two + // above: a capped JSON bundle names the call that serves its cut rows exactly as the XML twin's does. + std::string_view task; + int packTopN; + std::string_view rankFlags; // forRankShapingFlags( cfg ), owned by the caller for the call's duration }; // The lens bundle's opening keys. Every note is absent-unless-present — the same silence-means-nothing- @@ -1450,6 +1455,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t fixedBytes = header.size() + kJsonEnvelopeBytes + kJsonSurfaceCountsBytes + ( in.noteIndex ? kJsonNotesStanzaBytes : 0 ); const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; + // knob-honesty-068: the cut's continuation, exempt from the trim like the XML twin's next= (SigsCutContinuation) + const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*json=*/true }; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex }; @@ -1460,7 +1467,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For in.rootArg, /*hasRelevanceFloor=*/true, // LB-A: same admission rule as the XML twin (R-R: root-relative p/id) outDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) outShownIds, // lane 2: the emitted rows' ids — the tail excludes these files - outCut ); }; // cut-fix lane A: the XML tag's shown/total/docs_dropped + outCut, // cut-fix lane A: the XML tag's shown/total/docs_dropped + &cutNext ); }; // knob-honesty-068: "sigs_next" on a capped array // §B1.4: built once, used on both the degrade path below and the normal return — these three are plain // size_t values already computed by the caller (no rendering, no redaction seam), so unlike est_tokens @@ -1550,6 +1558,7 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For if( sigsCut.isCapped ) { sigsCutStanza += ",\"sigs_shown\":" + std::to_string( sigsCut.shown ) + ",\"sigs_total\":" + std::to_string( sigsCut.total ); + sigsCutStanza += rw::nextFieldJson( sigsCut.next, "sigs_next" ); // knob-honesty-068: the call serving the cut rows } if( sigsCut.docsDropped > 0 ) { @@ -1793,6 +1802,20 @@ void restrictBodiesToRouteAnchor( const rw::IngestResult& ing, std::vector cuts), pre-spelled for the +// capped block's continuation (serialize.h SigsCutContinuation::rankFlags) — the re-run must rank the same list. +inline std::string forRankShapingFlags( const rw::Config& cfg ) +{ + std::string f; + f += cfg.anchor ? " --anchor" : ""; + f += cfg.noRoute ? " --no-route" : ""; + f += cfg.adaptive ? " --adaptive" : ""; + f += cfg.noMentionBoost ? " --no-mention-boost" : ""; + f += cfg.cochangeBoost ? " --cochange-boost" : ""; + f += cfg.noDocMention ? " --no-doc-mention" : ""; + return f; +} + std::size_t forSigSideCeiling( bool autoBundleMode, int packTopN, std::size_t bundleBudget ) { if( autoBundleMode && packTopN == 0 ) @@ -2747,7 +2770,8 @@ std::optional runForLens( const MainDispatch& d ) &forClone, testedPtr, ampPtr, redactPtr, cfg.packBudgetBytes, cfg.tokenBudget, notesPtr, legoTotal, composeTotal, routesTotal, flRootArg, - &forFileTail, d.notesDegraded } ); + &forFileTail, d.notesDegraded, cfg.forTask, cfg.packTopN, + forRankShapingFlags( cfg ) } ); // §B0: this early return skipped the end-of-function tally below, so a --for --json run redacted // SILENTLY — the one stderr line that tells the user a secret was in their tree never appeared. reportRedactions( stderr, redactCounts ); @@ -2975,6 +2999,11 @@ std::optional runForLens( const MainDispatch& d ) // headerStr is already flushed to stdout by the time that path runs and cannot be edited retroactively // (the same reason est_tokens is "omitted", not "wrong", on that path — see its DISCLOSE). std::size_t forDroppedPositive = 0; + // knob-honesty-068: a capped names the call that serves it uncut (serialize.h SigsCutContinuation, where the + // exempt-from-the-trim rule is argued: the ranked set is the one it was at every ceiling). Its bytes are measured + // downstream like every spliced disclosure: est_tokens, the header ladder and over_ceiling= see them. + const std::string forRankFlags = forRankShapingFlags( cfg ); + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, /*json=*/false }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below sigsPreRendered = preRender( [ & ]( std::FILE* sm ) @@ -2990,7 +3019,8 @@ std::optional runForLens( const MainDispatch& d ) &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then forTopRowNext, // L-W: the widening page on a thin answer, else the body - &forSigsCut ); // cut-fix lane A: which cut readings the tag owes + &forSigsCut, // cut-fix lane A: which cut readings the tag owes + &forSigsNext ); // knob-honesty-068: a capped block's own next= }, sigsStr ); if( !sigsPreRendered ) @@ -3069,7 +3099,7 @@ std::optional runForLens( const MainDispatch& d ) // cut-fix lane A: the tag's two cut readings (docs_dropped=, shrunk-not-dropped) ride the same splice, present // only when the tag carries the case (serialize.h sigsCutLegendNotes), so the reserve below already covers them. const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( rw::kForBudgetBytesNote ) : std::string() ) - + rw::sigsCutLegendNotes( forSigsCut.isCapped, forSigsCut.shown, forSigsCut.total, forSigsCut.docsDropped ); + + rw::sigsCutLegendNotes( forSigsCut.isCapped, forSigsCut.shown, forSigsCut.total, forSigsCut.docsDropped, !forSigsCut.next.empty() ); // ── the INDEXING-cap disclosure (mention.h CapDisclosure), at the same splice point and for the // same reason: a --for header is charged against the payload ceiling, so a disclosure folded into // the notes above is paid for in ranked rows. Measured on sixteen real invocations, that cost three @@ -3472,7 +3502,8 @@ std::optional runForLens( const MainDispatch& d ) packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, /*hasRelevanceFloor=*/true, nullptr, nullptr, nullptr, // LB-A: the direct-emission degrade path selects identically - forTopRowNext ); // L-W: same next= rule on the degrade path + forTopRowNext, // L-W: same next= rule on the degrade path + nullptr, &forSigsNext ); // knob-honesty-068: and the same } if( legoPreRendered ) { diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 783df6376..e9791d316 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -413,5 +413,105 @@ case "$first_capped" in *) no "(8) first capped state is not shrunk-not-dropped: $first_capped" ;; esac +# ── (9) A CAPPED NAMES THE CALL THAT SERVES IT UNCUT (knob-honesty-068; PROCESS rule 5) ─────────────────── +# The ladder disclosed its cut (shown= total= capped="1") and named no call that serves the cut rows: the bundle's only +# next= was the r=1 row's (a body, or the widening page). Fixture: 40 matching C functions with long docs and long +# parameter lists, so the DEFAULT ceiling drops rows. The tag must carry next= (+ its legend clause), pasting it must +# serve all total= rows uncut in the same rank order, the shown rows must be its prefix, and under a tight explicit +# ceiling it is still there and either fits (est_tokens <= budget_tokens) or the root says over_ceiling="1" — it is +# exempt from the sig trim (no row is spent on it), so an overshoot is disclosed, never hidden. JSON + MCP twins. +# Negatives: an uncapped tag (arm 8's fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). +mkdir -p "$TMP/fxcut" +python3 - "$TMP/fxcut" <<'PY' +import sys +for f in range( 2 ): + with open( "%s/cut%d.c" % ( sys.argv[1], f ), "w" ) as o: + for i in range( 20 ): + n = f * 20 + i + 1 + o.write( "/* gadget assembler %d builds one gadget from its parts, validating every part against the catalogue before it is placed and logged */\n" % n ) + o.write( "int gadgetAssembler%d( int partCount, const char* catalogueName, double toleranceMillimetres, unsigned long serialNumber ) { return partCount + %d; }\n\n" % ( n, n ) ) +PY +cut_arm(){ python3 -c ' +import json, re, shlex, sys +s = sys.stdin.read() +m = re.search( r"]*)>(.*?)", s, re.S ) +if not m: print( "FAIL no " ); sys.exit( 1 ) +tag = m.group( 1 ) +nx = re.search( r"\snext=\"([^\"]*)\"", tag ) +sh = re.search( r"shown=\"([0-9]+)\"", tag ); tt = re.search( r"total=\"([0-9]+)\"", tag ) +names = re.findall( r"]*\bn=\"([^\"]*)\"", m.group( 2 ) ) +out = { "capped": "capped=\"1\"" in tag, "shown": int( sh.group( 1 ) ) if sh else len( names ), "total": int( tt.group( 1 ) ) if tt else len( names ), + "next": nx.group( 1 ).replace( "'", chr( 39 ) ).replace( """, chr( 34 ) ).replace( "&", "&" ) if nx else "", + "clause": "[sigs next=:" in s[ : s.find( "]*>", s ); r = root.group( 0 ) if root else "" +for k in ( "est_tokens", "budget_tokens" ): + v = re.search( r"\b%s=\"([0-9]+)\"" % k, r ); out[ k ] = int( v.group( 1 ) ) if v else None +out[ "over" ] = "over_ceiling=\"1\"" in r +print( json.dumps( out ) )'; } +cfield(){ python3 -c 'import json,sys; d=json.loads(sys.argv[1]); v=d.get(sys.argv[2]); print(json.dumps(v) if isinstance(v,(list,dict,bool)) or v is None else v)' "$1" "$2"; } +C1="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-cache 2>/dev/null | cut_arm )" +if [ "$( cfield "$C1" capped )" = true ] && [ "$( cfield "$C1" shown )" -lt "$( cfield "$C1" total )" ]; then + ok "(9) presence guard: the default ceiling drops rows ($( cfield "$C1" shown ) of $( cfield "$C1" total ))" +else + no "(9) presence guard: the fixture is not cut at the default ceiling: $C1" +fi +NX1="$( cfield "$C1" next )" +case "$NX1" in + "--for='gadget assembler' --signatures-only --token-budget="[0-9]*) ok "(9) the capped names its call: next=\"$NX1\"" ;; + *) no "(9) the capped carries next='$NX1' (want --for='gadget assembler' --signatures-only --token-budget=N)" ;; +esac +[ "$( cfield "$C1" clause )" = true ] && ok "(9) the [sigs next=: …] clause defines it ahead of " || no "(9) rides with no legend clause" +# paste it (shlex-split, as an agent would) +pasted(){ python3 -c 'import shlex,sys; print( "\0".join( shlex.split( sys.argv[1] ) ), end = "" )' "$1" > "$TMP/nx.argv"; ( cd "$TMP" && xargs -0 "$BIN" "$2" --no-cache < "$TMP/nx.argv" 2>/dev/null ); } +C2="$( [ -n "$NX1" ] && pasted "$NX1" fxcut | cut_arm )" +if [ -n "$C2" ] && [ "$( cfield "$C2" capped )" = false ] && [ "$( cfield "$C2" shown )" = "$( cfield "$C1" total )" ] && [ -z "$( cfield "$C2" next )" ]; then + ok "(9) pasting it serves all $( cfield "$C2" shown ) rows uncut, and that answer carries no " +else + no "(9) the pasted next= did not serve the block uncut: ${C2:-}" +fi +python3 -c 'import json,sys; a=json.loads(sys.argv[1])["names"]; b=json.loads(sys.argv[2])["names"]; sys.exit(0 if a and b[:len(a)]==a else 1)' "$C1" "${C2:-{\"names\":[]\}}" 2>/dev/null \ + && ok "(9) the cut block's rows are a prefix of the continuation's (same ranked list, same order)" \ + || no "(9) the continuation does not extend the cut block's rows in order" +# tight explicit ceiling: still named, and either inside the budget the root names or disclosed over it; the call recovers the rows +C3="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=1500 --no-cache 2>/dev/null | cut_arm )" +NX3="$( cfield "$C3" next )" +{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] \ + && { [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ] || [ "$( cfield "$C3" over )" = true ]; }; } \ + && ok "(9) --token-budget=1500: next= present; est_tokens $( cfield "$C3" est_tokens ) vs budget_tokens 1500 (over_ceiling=$( cfield "$C3" over ))" \ + || no "(9) --token-budget=1500: $C3" +C4="$( [ -n "$NX3" ] && pasted "$NX3" fxcut | cut_arm )" +[ -n "$C4" ] && [ "$( cfield "$C4" capped )" = false ] && [ "$( cfield "$C4" shown )" = "$( cfield "$C3" total )" ] \ + && ok "(9) --token-budget=1500: pasting its next= serves the block uncut" || no "(9) --token-budget=1500: the pasted next= left it cut: ${C4:-}" +# negatives: an uncapped tag carries no next=; a ranking flag is echoed (the re-run must rank the same list) +U1="$( cd "$TMP" && "$BIN" fxdocs --for="widget helper" --no-cache 2>/dev/null | cut_arm )" +[ "$( cfield "$U1" capped )" = false ] && [ -z "$( cfield "$U1" next )" ] && [ "$( cfield "$U1" clause )" = false ] \ + && ok "(9) negative: an uncapped carries no next= and no clause" || no "(9) negative: uncapped fixture: $U1" +case "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-route --no-cache 2>/dev/null | cut_arm | python3 -c 'import json,sys; print(json.load(sys.stdin)["next"])' )" in + *' --no-route'*) ok "(9) --no-route is echoed in the continuation" ;; + *) no "(9) --no-route was not echoed in the continuation" ;; +esac +# JSON twin: "sigs_next" on a capped array, and pasting it serves the array uncut +J1="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --json --no-cache 2>/dev/null )" +JNX="$( printf '%s' "$J1" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sigs_next",""))' 2>/dev/null )" +case "$JNX" in + *' --signatures-only '*' --json') ok "(9) JSON twin: \"sigs_next\" names the --json call" ;; + *) no "(9) JSON twin: sigs_next='$JNX'" ;; +esac +J2="$( [ -n "$JNX" ] && pasted "$JNX" fxcut | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("capped"), len(d["sigs"]), "sigs_next" in d)' 2>/dev/null )" +[ "$J2" = "False $( printf '%s' "$J1" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sigs_total"))' 2>/dev/null ) False" ] \ + && ok "(9) JSON twin: pasting sigs_next serves every row uncut" || no "(9) JSON twin: pasted sigs_next gave '$J2'" +# MCP twin (signatures only, the same default ceiling): the same attribute on its +M1="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"gadget assembler"}}}\n' "$TMP/fxcut" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import json,sys +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line) + if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm )" +case "$( cfield "${M1:-{\}}" next 2>/dev/null )" in + "--for='gadget assembler' --signatures-only --token-budget="[0-9]*) ok "(9) MCP twin: the capped carries the same next= shape" ;; + *) no "(9) MCP twin: ${M1:-}" ;; +esac + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" From 9293ab0105c6d3cdaa425ef8bdc336fa5405311b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:26:06 -0400 Subject: [PATCH 072/229] test: pathgapcheck (M) reports its verdict from the helper's own exit status The MCP parity arm ended a python heredoc with && ok || no on one line, so a failed write of the PASS line printed FAIL for an arm that passed. Take the helper's rc into a variable and branch on it, as the other gates do. Co-Authored-By: Claude Sonnet 5.5 --- test/pathgapcheck.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index fa4a78eb2..1971a9bd8 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -264,7 +264,7 @@ fi cleanArm "(N5) a value the search also calls" "$TMP/valin" "handleRequest,other" echo "(M) MCP path_between carries the same clause" -python3 - "$BIN" "$TMP" <<'PY' && ok "(M) path_between: same searched/gaps/gap_syms/rows/next as the CLI on P1-P3, plain hint on N1" || no "(M) MCP path_between disagrees with the CLI (above)" +python3 - "$BIN" "$TMP" <<'PY' import json, re, subprocess, sys binp, tmp = sys.argv[1], sys.argv[2] cases = [ ( "decl", "Machine_run", "Process_update", True ), ( "val", "listen", "respond", True ), @@ -304,6 +304,12 @@ for i, ( d, a, b, gap ) in enumerate( cases ): print( " %s: MCP lost the plain no-path hint" % d ); bad = 1 sys.exit( bad ) PY +mrc=$? +if [ "$mrc" -eq 0 ]; then + ok "(M) path_between: same searched/gaps/gap_syms/rows/next as the CLI on P1-P3, plain hint on N1" +else + no "(M) MCP path_between disagrees with the CLI or the helper crashed (rc=$mrc, details above)" +fi if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAIL"; fi exit "$fail" From d6d6e69544a08581419c4d914df193661793f2cc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:26:06 -0400 Subject: [PATCH 073/229] fix: the Go t=type legend clause says the form is WRITTEN, not underlying type Wrapped Pair (a struct underneath) and type HF http.HandlerFunc (a func underneath) are t="type" because the kind follows the written form, so 'a-defined-type-not-a-struct' was false of them. Reword the clause (and the model.h/serialize.h comments); gokindcheck arm (H) pins the qualified wording and the HF row. Co-Authored-By: Claude Sonnet 5.5 --- src/model.h | 2 +- src/serialize.h | 6 ++++-- test/gokindcheck.sh | 16 +++++++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/model.h b/src/model.h index 9e01d03d6..74ad5c92f 100644 --- a/src/model.h +++ b/src/model.h @@ -97,7 +97,7 @@ inline const char* symTag( SymKind k ) noexcept case SymKind::Field: return "field"; // member variable (id=path::Owner::field; use-sites via --uses=Owner.field) case SymKind::Other: return "other"; case SymKind::ModuleScope: return "modscope"; // the file's module scope: n="", no body to expand - case SymKind::NamedType: return "type"; // Go `type N string`: a defined type, NOT a struct + case SymKind::NamedType: return "type"; // Go `type N string`: a defined type whose WRITTEN form is not a struct/iface/func literal case SymKind::Alias: return "alias"; // Go `type A = B`: another name for B case SymKind::FuncType: return "functype"; // Go `type F func(...)`: a named function type } diff --git a/src/serialize.h b/src/serialize.h index 72a257975..b64ee7826 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2367,11 +2367,13 @@ inline constexpr std::string_view kModScopeMapLegend = // honesty-small-068: the three Go named-type kinds' reading, appended ONLY when the corpus has one (the modscope clause's // byte-identity rule: a corpus without such a row keeps every byte of this legend). What it does NOT mean: t=type is not -// "a struct" (its underlying form is not a struct/interface/func, or is another named type the parse cannot see into), +// "a struct" by what it IS: the kind says how the type is WRITTEN, so `type W Pair` and `type H pkg.HandlerFunc` are t=type +// even when the type underneath is a struct or a func (the parse cannot see into another named type), // and only Go is split — another language's typedef/alias/enum still reads t=struct (said in model.h and the gate's // arm F, not in every Go answer: the clause rides each Go map, so it carries only what a Go row needs). inline constexpr std::string_view kNamedTypeMapLegend = - ""; // EXTENT HONESTY (src/extentsuspect.h, gate test/extentcheck.sh) — the ONE reading of extent_suspect= on every ROW diff --git a/test/gokindcheck.sh b/test/gokindcheck.sh index 3d3c5a789..743fe785a 100755 --- a/test/gokindcheck.sh +++ b/test/gokindcheck.sh @@ -22,6 +22,9 @@ # (D) the cache round-trips the new kind bytes: a warm run equals the --no-cache run byte for byte. # (E) the legend defines the three kinds under --legend=full on a Go corpus; the graph query can select them. # (F) scope floor: a C typedef keeps t="struct" and its map carries NO named-type clause (byte-identity rule). +# (H) the t=type legend clause is true of EVERY t=type row: `type Wrapped Pair` (a struct underneath) and +# `type HF http.HandlerFunc` (a func underneath) are t="type" because the kind follows the WRITTEN form, so the +# clause must say WRITTEN and must not claim the bare "not-a-struct" (red on the wording it replaced). # (G) cross_kind= on --callers counts the new kinds (a name that is a func in one file and a defined type in another) # without tripping callhierarchy.h's kind bound. # @@ -82,6 +85,7 @@ type ( OldName = Count Wrapped Pair Qualified ext.Thing + HF http.HandlerFunc ) type Outer struct { @@ -121,7 +125,7 @@ expectKind(){ # $1 arm label, $2 kind, $3 name } echo "(A) a non-struct named type says its form" -for n in TestName List Set Count Lookup Events PairPtr Names Grid Wrapped Qualified; do expectKind "(A)" type "$n"; done +for n in TestName List Set Count Lookup Events PairPtr Names Grid Wrapped Qualified HF; do expectKind "(A)" type "$n"; done for n in eventFormatterFunc Handler; do expectKind "(A)" functype "$n"; done for n in Alias OldName; do expectKind "(A)" alias "$n"; done @@ -177,6 +181,16 @@ else no "(E) kind(all,struct) rc=$rc selected [$qs]" fi +echo "(H) the t=type clause names the written form (a struct/func underneath still reads t=type)" +tline="$( grep -o '' "$TMP/full.xml" )" +if [ -n "$tline" ] && printf '%s\n' "$tline" | grep -q 'type=a-defined-type-whose-WRITTEN-form-is-not-a-struct/iface/func-literal' \ + && printf '%s\n' "$tline" | grep -q 'type-W-Pair-too,whatever-Pair-is-underneath' \ + && ! printf '%s\n' "$tline" | grep -q 'type=a-defined-type-not-a-struct'; then + ok "(H) the clause says the form is WRITTEN, naming type W Pair; Wrapped and HF (struct/func underneath) are the t=type rows it must stay true of" +else + no "(H) the t=type clause is not qualified by the written form: [$tline]" +fi + echo "(F) scope floor: only Go is split" cat > "$TMP/c/t.c" <<'EOF' typedef int handle_t; From 1cf71ab44557d3d3049eb2ea8158b701178bfcb5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:40:38 -0400 Subject: [PATCH 074/229] test: mcpincrementalcheck runs its byte-identity arms on --mcp-legend=inline; default-posture twin A stdio session now takes the legend="ref" posture after its first answer, so a warm server's later answers no longer equal a cold server's single inline answer byte for byte. The legend posture is orthogonal to what the gate measures (the incremental index), so the existing arms run unchanged on --mcp-legend=inline (the previous posture), and a twin warm server on the default posture must answer legend="ref" with the cold answer's rows and root facts on every arm (red when the root facts differ). Co-Authored-By: Claude Opus 5.5 --- test/mcpincrementalcheck.sh | 43 +++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/test/mcpincrementalcheck.sh b/test/mcpincrementalcheck.sh index 239666aa2..69b32fd2a 100755 --- a/test/mcpincrementalcheck.sh +++ b/test/mcpincrementalcheck.sh @@ -108,12 +108,23 @@ wait_for_id() { } # ─── the long-lived WARM server ─────────────────────────────────────────────────────────────────── +# lean-answers lane: a stdio session's legend posture is now "ref" after its first answer (--mcp-legend=session, the +# default), so a warm server's later answers carry their legend once per session while a cold server's single answer is +# inline. That posture is orthogonal to what this gate measures (the incremental INDEX), so the byte-identity arms run +# on --mcp-legend=inline (the pre-lane posture, unchanged assertions) and a TWIN warm server on the default posture is +# held to the same tree-state rows (equiv's twin arm below). FIFO="$WORK/in.fifo"; mkfifo "$FIFO" -TMPDIR="$WARMTMP" "$BIN" --mcp <"$FIFO" >"$TMP/warm.out" 2>/dev/null & +TMPDIR="$WARMTMP" "$BIN" --mcp --mcp-legend=inline <"$FIFO" >"$TMP/warm.out" 2>/dev/null & SRV=$! exec 9>"$FIFO" -trap 'exec 9>&- 2>/dev/null; kill "$SRV" 2>/dev/null; rm -rf "$TMP"' EXIT +WARMTMP_D="$TMP/warmtmp_d"; mkdir -p "$WARMTMP_D" +FIFO_D="$WORK/in_d.fifo"; mkfifo "$FIFO_D" +TMPDIR="$WARMTMP_D" "$BIN" --mcp <"$FIFO_D" >"$TMP/warm_d.out" 2>/dev/null & +SRV_D=$! +exec 8>"$FIFO_D" +trap 'exec 9>&- 2>/dev/null; exec 8>&- 2>/dev/null; kill "$SRV" "$SRV_D" 2>/dev/null; rm -rf "$TMP"' EXIT printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' >&9 +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' >&8 # ask the WARM server for verb `for` with task $2, response id $1 → sets WTEXT / WSTAMP / WREINGEST. warm_for() { @@ -124,6 +135,11 @@ warm_for() { WTEXT="$( inner_for_id "$TMP/warm.out" "$id" )" WSTAMP="$( stamp_for_id "$TMP/warm.out" "$id" )" WREINGEST="$( reingest_for_id "$TMP/warm.out" "$id" )" + # the default-posture twin server gets the same request (WTEXT_D; empty when it never answered) + printf '{"jsonrpc":"2.0","id":%s,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s"}}}\n' \ + "$id" "$WORK" "$task" >&8 + WTEXT_D="" + wait_for_id "$TMP/warm_d.out" "$id" && WTEXT_D="$( inner_for_id "$TMP/warm_d.out" "$id" )" } # ask a FRESH server (fresh TMPDIR ⇒ no cache blob ⇒ genuine COLD full reingest) → CTEXT / CSTAMP. @@ -132,7 +148,7 @@ cold_for() { local ct; ct="$( mktemp -d "$TMP/cold.XXXXXX" )" printf '%s\n{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s"}}}\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize"}' "$WORK" "$task" \ - | TMPDIR="$ct" "$BIN" --mcp >"$ct/out" 2>/dev/null + | TMPDIR="$ct" "$BIN" --mcp --mcp-legend=inline >"$ct/out" 2>/dev/null CTEXT="$( inner_for_id "$ct/out" 7 )" CSTAMP="$( stamp_for_id "$ct/out" 7 )" } @@ -150,6 +166,24 @@ equiv() { no "$what: incremental verb text DIFFERS from a cold full reingest" diff <( printf '%s\n' "$WTEXT" ) <( printf '%s\n' "$CTEXT" ) | head -20 fi + # twin (default session posture): the warm answer is legend="ref" and its rows and root facts equal the cold answer's + local tw + tw="$( printf '%s\n%s\n' "$WTEXT_D" "$CTEXT" | python3 -c ' +import sys, json, re +w, c = [ json.loads( l ) for l in sys.stdin.read().splitlines()[:2] ] +A = re.compile( r"\s([\w:.-]+)=\"([^\"]*)\"" ) +def rows( t ): + m = re.search( r".*?]*>.*?", t, re.S ); return m.group( 0 ) if m else None +ab = re.search( r"]*/>", w ); rt = re.search( r"]*>", c ) +if not ab or "legend=\"ref\"" not in ab.group( 0 ): print( "NOREF" ); sys.exit() +wa = { k: v for k, v in A.findall( ab.group( 0 ) ) if k not in ( "legend", "dict", "dictv" ) } +ca = { k: v for k, v in A.findall( rt.group( 0 ) ) if k != "task" } if rt else None +print( "SAME" if rows( w ) is not None and rows( w ) == rows( c ) and wa == ca else "DIFF" ) +' 2>&1 )" + case "$tw" in + SAME) ok "$what (default legend posture twin): warm legend=\"ref\" answer has the cold answer's rows and root facts";; + *) no "$what (default legend posture twin): warm answer vs cold: $tw";; + esac if [ "$WSTAMP" = "$CSTAMP" ]; then ok "$what: _index stamp matches cold ('$WSTAMP')" else @@ -403,7 +437,8 @@ esac # ═══════════════════════════════════════════════════════════════════════════════════════════════ exec 9>&- -wait "$SRV" 2>/dev/null +exec 8>&- +wait "$SRV" "$SRV_D" 2>/dev/null echo if [ "$fail" -eq 0 ]; then From d42ab0841605c4102138ddfd4ac69a30f0f6d90b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 08:52:01 -0400 Subject: [PATCH 075/229] test: printf parity re-pins help and help_all for the two new flags RE-PIN (lane/lean-answers-068): --help and --help=all move only by the added --whereis-listing= and --mcp-legend= help lines (and the whereis LISTING note); base vs head differ by added lines only, none removed or changed. The other 40 verbs keep their pins. Co-Authored-By: Claude Opus 5.5 --- test/printf_parity.manifest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 41c8cfe8b..008d6a8a6 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 c74a677dde09eb76a71bfd0b9e2c75d074c47ecfac838654f84df80af6725b02 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 c4d340f8ad76e35bf282d4dcd0c82abe15324a6d0fc1559ba1107df538760214 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 5e82bb6e129da4086c1e64f5939be191964e5db6e7c49154edfee9d1cfdc44e6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 62d2d7dc1b44d708e44cecdf4a351833f07cd5d34abfc2666073cdc0db65da51 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 05edaf1aa8e9e0eecaae72ab55089bc47beab962 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:03:39 -0400 Subject: [PATCH 076/229] README: 187 long flags (--whereis-listing=, --mcp-legend=) Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 26dd08715..1cb564a46 100644 --- a/README.md +++ b/README.md @@ -945,9 +945,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-185 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin +187 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin -Around the core sit 185 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 187 long flags advertised in `--help`, across seven families — plus an MCP server, so a coding agent can call any of them mid-task instead of grepping and reading whole files. `--help` prints one line per flag (~4.5K tokens); `--help=--FLAG` prints that flag's full entry with every caveat, `--help=SECTION` one family, and `--help=all` the whole catalog. @@ -2644,7 +2644,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 185 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 187 long flags advertised in `--help` into seven families. The `--help=` column below is the argument that prints one family: `ripwire --help=navigate`. `ripwire --help=--FLAG` prints one flag's full text — the caveats, the units, what it refuses and why. `ripwire --help=all` is the whole catalog, about 46,000 tokens. From c71a27c0687188ef9064b094fcc39737fcad95e9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:38:28 -0400 Subject: [PATCH 077/229] test(gate): mcpreloadasancheck scopes the zero-quarantine setting to the memory-release sessions quarantine_size_mb=0 was set for every sanitizer process of the gate (the long-lived session and all one-shot CLI runs), so a freed chunk was reused at once and a use-after-free landing on a same-size reuse would read live memory and report nothing. Only the release arm needs it (its footprint has to fall under the 64M floor). The session, cold server and CLI runs keep the default quarantine; the warm-up and release sessions use RLSANENV. The gate header says why. Co-Authored-By: Claude Sonnet 5.5 --- test/mcpreloadasancheck.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index 57cdd9c77..a0f54a018 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -50,6 +50,11 @@ # # LeakSanitizer is OFF here (detect_leaks=0, as CI's macOS asan leg): the subject is lifetime, not leaks, and a leak report at # the exit of a long-lived server would be a different finding with its own gate. +# The freed-chunk quarantine stays ON everywhere but the memory-release sessions (RLSANENV: quarantine_size_mb=0 and +# allocator_release_to_os_interval_ms=0, because that arm's footprint must fall back under the 64M floor, which a quarantine +# prevents). With it off, a freed chunk is handed out again at once, and a use-after-free that lands on a same-size reuse reads +# live memory and reports nothing: the class this gate exists to catch. (The known bug reported as heap-buffer-overflow with +# the quarantine off everywhere, and as the heap-use-after-free it is with it on.) # # Usage: # bash test/mcpreloadasancheck.sh [PLAIN_BIN] # CLI reference binary (default build/ripwire); ASan binary asan/ripwire @@ -132,7 +137,7 @@ SAN = os.path.join(WORK, "san") SRVTMP, CLITMP = os.path.join(WORK, "srvtmp"), os.path.join(WORK, "clitmp") DARWIN = sys.platform == "darwin" SANENV = { - "ASAN_OPTIONS": "detect_leaks=0:halt_on_error=1:abort_on_error=1:quarantine_size_mb=0:allocator_release_to_os_interval_ms=0:log_path=%s/asan" % (SAN,), + "ASAN_OPTIONS": "detect_leaks=0:halt_on_error=1:abort_on_error=1:log_path=%s/asan" % (SAN,), "UBSAN_OPTIONS": "halt_on_error=1:print_stacktrace=1:log_path=%s/ubsan" % SAN, } for cand in ("/opt/homebrew/opt/llvm@22/bin/llvm-symbolizer", "/usr/bin/llvm-symbolizer"): @@ -142,6 +147,8 @@ for cand in ("/opt/homebrew/opt/llvm@22/bin/llvm-symbolizer", "/usr/bin/llvm-sym SANENV["UBSAN_OPTIONS"] += ":external_symbolizer_path=" + cand break TOPK = "6" +# the release arm only: its footprint must fall under the 64M floor, so the freed-chunk quarantine is off THERE +RLSANENV = dict(SANENV, ASAN_OPTIONS=SANENV["ASAN_OPTIONS"] + ":quarantine_size_mb=0:allocator_release_to_os_interval_ms=0") fails, notes = [], [] checks = 0 stats = {"stamp": 0, "content": 0, "edit": 0, "switch": 0, "release": 0, "cold": 0, "changed": 0} @@ -595,7 +602,7 @@ def release_attempt(k, refR, refA, kk): process that has run the whole cycle never falls back under it, so the release path gets a fresh process. Root R's resident index alone carries it over the line, so the SECOND call on R releases that index (releaseMcpIndexMemory) and rebuilds the unchanged tree; the next call, on A, releases R's rebuild again and rebuilds A. Only those calls are made.""" - s = Server(ASAN, ["--top-k=" + TOPK, "--max-memory=64M"], SRVTMP + "_rl", os.path.join(WORK, "release.err"), SANENV) + s = Server(ASAN, ["--top-k=" + TOPK, "--max-memory=64M"], SRVTMP + "_rl", os.path.join(WORK, "release.err"), RLSANENV) status = "ok" try: s.rpc("initialize") @@ -637,7 +644,7 @@ def release_session(k, refR, refA, kk): if not stats.get("rl_warm"): # the memory guard stops a parse that runs past five seconds, and a cold sanitizer parse of R can: warm the ingest # cache once, unlimited, so every limited session below reads facts from it and its ingest is short - w = Server(ASAN, ["--top-k=" + TOPK], SRVTMP + "_rl", os.path.join(WORK, "warm.err"), SANENV) + w = Server(ASAN, ["--top-k=" + TOPK], SRVTMP + "_rl", os.path.join(WORK, "warm.err"), RLSANENV) try: w.rpc("initialize") kind, payload, _ = w.tool("find_symbol", path=R, symbol="cb") From 30ca99649f5739580f776de2d3dc31468db4cdc8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:39:33 -0400 Subject: [PATCH 078/229] test(gate): mcpreloadasancheck exits 2 when RIPWIRE_ASAN_BIN names no executable An explicit override that points at nothing is a typo, not the named expected premise (this checkout has no asan/ build), so it is an error rather than a SKIP. Co-Authored-By: Claude Sonnet 5.5 --- test/mcpreloadasancheck.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index a0f54a018..8e6a5753e 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -94,6 +94,8 @@ is_asan(){ LC_ALL=C grep -q -a '__asan_init' "$1" 2>/dev/null; } # the sanitizer binary: RIPWIRE_ASAN_BIN, else the binary under test when it is one (RIPWIRE_BIN=asan/ripwire), else asan/ripwire if [ -n "${RIPWIRE_ASAN_BIN:-}" ]; then ASAN_BIN="$RIPWIRE_ASAN_BIN"; [ "${ASAN_BIN#/}" = "$ASAN_BIN" ] && ASAN_BIN="$ROOT/$ASAN_BIN" + # an override that names nothing is a typo, not the expected "this checkout has no asan/ build": exit 2, never SKIP + [ -x "$ASAN_BIN" ] || { echo "RIPWIRE_ASAN_BIN is set to '$RIPWIRE_ASAN_BIN' but no executable is there"; exit 2; } elif is_asan "$BIN"; then ASAN_BIN="$BIN" else From 0891c538b95d48234421931c0fca9546c0205dfa Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:39:33 -0400 Subject: [PATCH 079/229] test(gate): mcpreloadasancheck keeps its sanitizer reports where CI's failure upload finds them The gate's log_path is inside its own temp dir, which is removed on exit, so the failure upload (/tmp/asanlog*) never saw the report and the FAIL row keeps only its first 3000 characters. On a failure the reports and the server stderr files are now copied to /tmp/asanlog-mcpreload.* (RIPWIRE_SAN_KEEP_DIR overrides the parent directory). Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 1 + test/mcpreloadasancheck.sh | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64a1642c7..926176942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -727,6 +727,7 @@ jobs: # file, an edit, a root switch, a memory release) and queried again each time. A cached ValueRefIndex once outlived # the index it pointed into (heap-use-after-free) and no single-query gate could see it. The gate drives that session # on this binary for 20 cycles and checks every answer against a one-shot CLI run; one binary plays both roles. + # A failure leaves the gate's sanitizer reports in /tmp/asanlog-mcpreload.* for the upload step below. - name: Sanitizer-only class — MCP index reloads under ASan (test/mcpreloadasancheck.sh) run: RIPWIRE_BIN=asan/ripwire bash test/mcpreloadasancheck.sh diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index 8e6a5753e..bb0df6fe9 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -911,5 +911,12 @@ else ok "no process of this gate outlives it" fi echo "mcpreloadasancheck: ${ELAPSED}s" +# keep the sanitizer reports past the mktemp cleanup, where CI's failure upload (/tmp/asanlog*) finds them: the FAIL row above +# keeps only the first 3000 characters of one report, and the freed-by / allocated-by stacks are the part that follows +if [ "$fail" -ne 0 ] && [ -n "$( ls -A "$TMP/san" 2>/dev/null )" ]; then + KEEP="$( mktemp -d "${RIPWIRE_SAN_KEEP_DIR:-/tmp}/asanlog-mcpreload.XXXXXX" 2>/dev/null )" \ + && cp "$TMP"/san/* "$TMP"/*.err "$KEEP"/ 2>/dev/null \ + && echo "mcpreloadasancheck: sanitizer reports kept in $KEEP" +fi if [ "$fail" -eq 0 ]; then echo "mcpreloadasancheck: ALL PASS"; else echo "mcpreloadasancheck: FAIL"; fi exit "$fail" From 30118561b0e01fc28a9a0e8733ab902af5b5741a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:39:46 -0400 Subject: [PATCH 080/229] test(gate): say in the gate header and the CI step that LeakSanitizer is off on both legs The ubuntu leg's job-level detect_leaks=1 is overridden inside this gate's processes. It stays off there because the Linux leg has never run the gate, so an exit-time leak report (rc 23) could not be told from a gate defect; the reason is now written next to the setting. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 2 ++ test/mcpreloadasancheck.sh | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 926176942..177f332dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -727,6 +727,8 @@ jobs: # file, an edit, a root switch, a memory release) and queried again each time. A cached ValueRefIndex once outlived # the index it pointed into (heap-use-after-free) and no single-query gate could see it. The gate drives that session # on this binary for 20 cycles and checks every answer against a one-shot CLI run; one binary plays both roles. + # LeakSanitizer is off in this step on BOTH legs (the gate sets detect_leaks=0 for its own processes; its subject is + # lifetime, not leaks, and the Linux leg has not yet shown a clean exit-time leak set for it). # A failure leaves the gate's sanitizer reports in /tmp/asanlog-mcpreload.* for the upload step below. - name: Sanitizer-only class — MCP index reloads under ASan (test/mcpreloadasancheck.sh) run: RIPWIRE_BIN=asan/ripwire bash test/mcpreloadasancheck.sh diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index bb0df6fe9..9d56eae68 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -48,8 +48,11 @@ # Each rebuild is proved by the server's own timing line (rebuilt=1), so an arm that no longer reaches its path FAILs # instead of passing blind. A mutation that no compared answer shows (the new reference equal to the old) FAILs too. # -# LeakSanitizer is OFF here (detect_leaks=0, as CI's macOS asan leg): the subject is lifetime, not leaks, and a leak report at -# the exit of a long-lived server would be a different finding with its own gate. +# LeakSanitizer is OFF here on BOTH CI legs (detect_leaks=0 in every sanitizer process of this gate; the ubuntu leg's job-level +# detect_leaks=1 is overridden inside them, and macOS has no LeakSanitizer): the subject is lifetime, not leaks, and a leak +# report at the exit of a long-lived server would be a different finding with its own gate. It is not switched on for Linux +# because the Linux leg has never run this gate, so nothing proves that leg's exit-time leak set is empty, and an unproven +# rc 23 would fail the gate for a reason that is not its subject. Turn it on once the train's first Linux run shows a clean set. # The freed-chunk quarantine stays ON everywhere but the memory-release sessions (RLSANENV: quarantine_size_mb=0 and # allocator_release_to_os_interval_ms=0, because that arm's footprint must fall back under the 64M floor, which a quarantine # prevents). With it off, a freed chunk is handed out again at once, and a use-after-free that lands on a same-size reuse reads From 530d8d572d72f0dd688adccebf297e7adc4cfb88 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:39:46 -0400 Subject: [PATCH 081/229] docs(changelog): mcpreloadasancheck entry states what is compared, what can SKIP and the exit-2 case Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dbaae33b..86b92b424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,13 @@ A cached object that points into the MCP server's index dangles when the index i `ValueRefIndex` once did exactly that (heap-use-after-free after a rebuild that kept its content stamp), and no single-query gate can see it. The gate runs one long-lived `--mcp` session on the sanitizer binary for 20 cycles of query, stamp-keeping mutation (chmod, a new non-source file, an empty directory), content mutation (file rewrite or the edit verbs), root -switch, workspace and memory release, and checks every answer against a one-shot CLI run on the same tree state. It -asserts each rebuild happened (`rebuilt=1`), zero sanitizer reports, and SKIPs by name when no sanitizer build is present. -It goes red on the build that still had that bug and is green after the fix. +switch, workspace and memory release. Every query answer is compared with a one-shot CLI run on the same tree state; the +`analyze` answer on a dirty root is compared with a cold server instead. It asserts that each rebuild it drives happened +(`rebuilt=1`) and that there were zero sanitizer reports. The memory-release arm is host-dependent: when no release session +meets its premises it prints a named SKIP and the other arms still decide the verdict. The whole gate SKIPs by name when no +sanitizer build is present, and exits 2 when `RIPWIRE_ASAN_BIN` is set to a path with no executable. The freed-chunk +quarantine stays on except in the memory-release sessions. It goes red on the build that still had that bug and is green +after the fix. ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) From dfe3024f08337cbb27c5f3ce6e047b72b26b298c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 09:40:55 -0400 Subject: [PATCH 082/229] test: shapingflagcheck probes the two new closed-value flags with a valid value --whereis-listing= and --mcp-legend= refuse any value outside their closed set before a knob is read, so the generic bogus probe value lands outside the three buckets; like --grep-in= they get a valid sample value (all / inline) and then classify as modifiers by their own refusal. Co-Authored-By: Claude Opus 5.5 --- test/shapingflagcheck.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/shapingflagcheck.sh b/test/shapingflagcheck.sh index 2c42bb6fd..f677aa08b 100755 --- a/test/shapingflagcheck.sh +++ b/test/shapingflagcheck.sh @@ -417,6 +417,8 @@ fprobeFor() --color-by=) printf '%s' '--color-by=lang' ;; --grep-scope=) printf '%s' '--grep-scope=file' ;; --grep-in=) printf '%s' '--grep-in=any' ;; + --whereis-listing=) printf '%s' '--whereis-listing=all' ;; # closed value set (defs|refs|all): a bogus value refuses before any knob + --mcp-legend=) printf '%s' '--mcp-legend=inline' ;; # closed value set (session|inline), same reason --legend=) printf '%s' '--legend=compact' ;; --slice-flow=) printf '%s' '--slice-flow=back' ;; --agent=) printf '%s' '--agent=codex' ;; From 08e2709007484723ada44078e8f31d050286439b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 10:13:32 -0400 Subject: [PATCH 083/229] test: shapingflagcheck keeps fprobeFor's length (quality-delta) The two closed-value probe arms share the --grep-in= line (the --max-file-size=/--max-memory= precedent), so fprobeFor stays at its verbosity bar. Co-Authored-By: Claude Opus 5.5 --- test/shapingflagcheck.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/shapingflagcheck.sh b/test/shapingflagcheck.sh index f677aa08b..5ac0b39c8 100755 --- a/test/shapingflagcheck.sh +++ b/test/shapingflagcheck.sh @@ -416,9 +416,7 @@ fprobeFor() --format=) printf '%s' '--format=columnar' ;; --color-by=) printf '%s' '--color-by=lang' ;; --grep-scope=) printf '%s' '--grep-scope=file' ;; - --grep-in=) printf '%s' '--grep-in=any' ;; - --whereis-listing=) printf '%s' '--whereis-listing=all' ;; # closed value set (defs|refs|all): a bogus value refuses before any knob - --mcp-legend=) printf '%s' '--mcp-legend=inline' ;; # closed value set (session|inline), same reason + --grep-in=) printf '%s' '--grep-in=any' ;; --whereis-listing=) printf '%s' '--whereis-listing=all' ;; --mcp-legend=) printf '%s' '--mcp-legend=inline' ;; # closed value sets: a bogus value refuses before any knob --legend=) printf '%s' '--legend=compact' ;; --slice-flow=) printf '%s' '--slice-flow=back' ;; --agent=) printf '%s' '--agent=codex' ;; From de0865688553e660528ef20837ca063932570ce3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 10:48:18 -0400 Subject: [PATCH 084/229] test(compactlegendcheck): twin for the safe-delete 720 -> 820 re-pin holds the rest at 720 The re-pin paid for one compact reading, the caller row's sites_l= (90 B with its separator). The new (U-twin) arm cuts exactly that reading from a copy of the --safe-delete=distance compact answer and measures the copy with the same prose operand the (U) arm uses: 715 B <= 720 B, the base binary's whole figure. It is taken only when a caller row carries sites_l= and the reading occurs once; any other premise fails. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index efb51e3ed..fa78ce55f 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -590,6 +590,39 @@ pinFor() { printf '%s\n' "$PIN_TABLE" | awk -v schema="$1" '$1 == schema { printf "%s", $2; exit }' } +# (U-twin) the pre-2026-10-04 ripwire.safe-delete/v1 pin, kept (PROCESS rule 4: a re-pin keeps a twin for the old path). +# The 720 -> 820 re-pin above paid for ONE reading, the caller row's sites_l= (+90 B with its separator). Everything else +# in that prose legend still fits the OLD 720 B: the same --safe-delete=distance probe, with exactly that reading cut out +# of a copy of the compact answer, is measured by the same `leg prose` operand the (U) arm uses. The cut is taken only when +# a caller row carries sites_l= AND the reading occurs once; any other premise FAILs (a cut that removes nothing proves nothing). +( cd "$REPO" && "$BIN" . --safe-delete=distance --legend=compact >"$TMP/sdt.c" 2>/dev/null "$TMP/sdt.full" 2>/dev/null ", "", doc, flags = re.S ) +if not re.search( r']*sites_l="[0-9]', body ): + print( "NOPREMISE no caller row carries sites_l=" ); sys.exit( 0 ) +if doc.count( reading ) != 1: + print( "NOPREMISE the sites_l= reading occurs %d times, not once" % doc.count( reading ) ); sys.exit( 0 ) +open( sys.argv[2], "w", encoding = "utf-8" ).write( doc.replace( reading, "", 1 ) ) +print( len( reading.encode() ) ) +PY +)" +if [ "$rcSdt" -ne 0 ]; then + no "(U-twin) --safe-delete=distance exited $rcSdt — the twin cannot measure" +else + case "$sdtCut" in + NOPREMISE*) no "(U-twin) safe-delete twin cannot measure: ${sdtCut#NOPREMISE }" ;; + *) sdtRest="$( leg prose "$TMP/sdt.cut" "$TMP/sdt.full" )" + if [ -n "$sdtRest" ] && [ "$sdtRest" -eq "$sdtRest" ] 2>/dev/null && [ "$sdtRest" -le 720 ]; then + ok "(U-twin) safe-delete compact prose minus the sites_l= reading ($sdtCut B) is $sdtRest B <= 720 B (the pre-CALLSITE-LINE pin)" + else + no "(U-twin) safe-delete compact prose minus the sites_l= reading ($sdtCut B) is ${sdtRest:-?} B > 720 B — the rest re-inflated" + fi ;; + esac +fi nXml=0; nXmlBad=0; nRefuse=0; nSkip=0; loopBytes=0; xmlVerbs=""; nDefBad=0 # (UG) rv-r1-L1-2: EVERY XML verb the flag universe reaches, EVERY instance of every attribute its DEFAULT answer carries, # defined `name=` in that answer's own legend (legendcoveragecheck (G)'s predicate, on this gate's fixture). No floor. From a169a389ff3a2e208a52a3eb0762a28e325bbbc4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:08:11 -0400 Subject: [PATCH 085/229] feat(for): the continuation carries its resume index; MCP gets the machine form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the capped- next= (cd76281a), shaped so it maps 1:1 onto the budgeted-bundle candidate page proposed for #294 once that lands: - next_offset= (JSON "sigs_next_offset") rides a block whose rows were DROPPED: the candidate index the cut starts at — the last printed row's rank in the (score desc, id asc) order, never a printed-row count, so a slot that prints no row cannot skew it. The root's at= stamps the index it was cut from (the clause says a resume must match it). - MCP `for` carries no pasteable CLI argv (the CLI ranks its own list, so a CLI re-run could continue a different one): next_budget_tokens=T, the budget_tokens a re-call of the same tool needs. - Who pays: EXEMPT at the default ceiling and when an explicit ceiling leaves the sig side frozen at the default's (rows unchanged, forbudgetmonotoncheck's identity holds); CHARGED — attributes and clauses reserved inside the block like shown=/total= — under an explicit ceiling whose sig side is the whole ceiling, so est_tokens stays within a hard --token-budget (fornotesbudgetcheck). detailcheck/truncvocabcheck: their tag shapes admit the trailing continuation attributes; the counts and the shown/total/capped pair they assert are unchanged. Gate: forrankordercheck.sh arm 9 (resume index = last printed rank, JSON and MCP twins, the MCP re-call serves every row uncut, charged at 1500 tokens, exempt at 8000 = the default's block). Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 14 ++-- src/serialize.h | 151 +++++++++++++++++++++++++++++++------- src/verbs_for.h | 23 ++++-- test/detailcheck.sh | 4 +- test/forrankordercheck.sh | 68 ++++++++++++----- test/truncvocabcheck.sh | 4 +- 6 files changed, 201 insertions(+), 63 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 123915243..3d420d0fa 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2185,11 +2185,12 @@ inline std::optional forTaskText( const std::string& root, const st const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" - // knob-honesty-068: the CLI twin's (serialize.h SigsCutContinuation: exempt from the trim, so the served - // rows are the ones they were). The value spells the CLI flags, as every next= this surface emits does (mcp `for` with - // budget_tokens=T is the same call: this surface is signatures-only). - const rw::SigsCutContinuation mcpSigsNext{ task, 0, noRoute ? std::string_view( " --no-route" ) : std::string_view(), fixedBytes, - /*json=*/false }; + // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, + // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO pasteable CLI argv here + // (the CLI ranks its own list, so a CLI re-run could continue a different one): the machine form next_budget_tokens=T — + // re-call `for` with budget_tokens=T — plus next_offset=, the resume index. + const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, + /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false }; // L3: field-notes surfacing — parity with the CLI --for lens. loadNoteIndex reads root/.ripwire_notes (a // small file); nullptr when EMPTY so the bundle stays byte-identical when there is nothing to surface. @@ -2258,8 +2259,7 @@ inline std::optional forTaskText( const std::string& root, const st } // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). - if( const std::string cutNotes = rw::sigsCutLegendNotes( mcpSigsCut.isCapped, mcpSigsCut.shown, mcpSigsCut.total, mcpSigsCut.docsDropped, - !mcpSigsCut.next.empty() ); + if( const std::string cutNotes = rw::sigsCutReportLegend( mcpSigsCut ); !cutNotes.empty() ) { const std::size_t closeAt = headerStr.rfind( " -->" ); diff --git a/src/serialize.h b/src/serialize.h index 6a37d3e92..a9008118f 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -33,6 +33,7 @@ #include #include // §H7 degrade seam: std::getenv for the non-release fault switch #include +#include // knob-honesty-068: the resume index's widest digit count (sigsCutContinuationReserve) #include #include #include @@ -868,10 +869,15 @@ static_assert( kForDocExcerptRankCount == 24 && kForDocFullRankCount == 12, // • next= on (knob-honesty-068): the call that serves the block uncut (sigsCutNextInvocation). Not the r=1 row's // next= (a body or the widening page), so it says which rows it serves. inline constexpr std::string_view kForSigsNextNote = " [sigs next=: one call serving all total= rows uncut]"; +inline constexpr std::string_view kForSigsNextBudgetNote = + " [sigs next_budget_tokens=: call this tool again with that budget_tokens to get all total= rows uncut]"; +inline constexpr std::string_view kForSigsNextOffsetNote = + " [sigs next_offset=: the candidate index the cut starts at; a resume must match the root at=]"; // The clauses a cut report owes, concatenated in a fixed order ("" when it owes none). `hasNext`: the tag carries // the cut's continuation (SigsCutReport::next). -inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped, bool hasNext = false ) +inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped, bool hasNext = false, + bool hasNextBudget = false, bool hasNextOffset = false ) { std::string notes; if( isCapped && shown == total ) @@ -882,6 +888,14 @@ inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::si { notes += kForSigsNextNote; } + if( isCapped && hasNextBudget ) + { + notes += kForSigsNextBudgetNote; + } + if( isCapped && hasNextOffset ) + { + notes += kForSigsNextOffsetNote; + } if( docsDropped > 0 ) { notes += kForDocsDroppedNote; @@ -4486,7 +4500,12 @@ struct SigsCutReport std::size_t docsDropped = 0; bool isCapped = false; std::string next; // knob-honesty-068: the cut's continuation (sigsCutNextInvocation), set only on a capped - // block whose caller asked for one; the tag's next= / the JSON root's "sigs_next" + // block whose caller asked for a PASTEABLE one; the tag's next= / the JSON "sigs_next" + std::size_t nextBudgetTokens = 0; // …or, on a surface with no pasteable argv (MCP), the budget_tokens a re-call needs: + // the tag's next_budget_tokens=; 0 ⇒ none + std::size_t lastShownRank = 0; // the 1-based candidate rank of the last row printed (rows are a rank PREFIX: the ladder + // drops tail-first) — the resume index; next_offset= when rows were dropped + bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) }; // ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── @@ -4500,14 +4519,27 @@ struct SigsCutReport // as big as its content, bounded by the same candidate pool). --pack-top-n rides only when the caller set it (it sets // total=). The value depends on the task and the untrimmed rows only — never on the ceiling that cut them — so the block // stays byte-identical at the default and at any explicit ceiling above it (forbudgetmonotoncheck's invariant). -// EXEMPT FROM THE SIG TRIM, in every regime — the e= / docs_after_code convention (a disclosure never costs a row the ceiling -// did not already cut): the plan never reserves it, so the ranked set is the one it was at every ceiling. Measured before -// choosing (knob-honesty-068 report): charging it like shown=/total= cost one row on 11 of 20 queries at the wrap recipe's -// --token-budget=2000. Its bytes stay real downstream — est_tokens measures them, and the explicit-ceiling header ladder -// and over_ceiling= see them — so an overshoot is disclosed, never hidden. +// WHO PAYS FOR ITS BYTES — two regimes, one predicate per caller (`charged`): +// • EXEMPT (the default ceiling; an explicit one wide enough that the sig side is frozen at the default's): the plan never +// reserves it — the e= / docs_after_code convention, a disclosure never costs a row the ceiling did not already cut — so +// the ranked set is the one it was and forbudgetmonotoncheck's byte identity holds. Downstream sections pay (the body +// leftover, the tail funded last); est_tokens measures it. +// • CHARGED (an explicit ceiling whose sig side is ALL of it: a --token-budget at or under the default sig share, any +// explicit one beside --signatures-only, MCP budget_tokens, --json --token-budget): nothing downstream can pay there, and +// an explicit --token-budget is a HARD ceiling (est_tokens <= budget, fornotesbudgetcheck), so the attribute AND its +// legend clause are reserved inside the block like shown=/total=. Cost, measured (knob-honesty-068 report): at the wrap +// recipe's --token-budget=2000 one row on 11 of 20 queries — a row the next= itself serves. Exempting it there instead +// kept every row and pushed est_tokens over the stated ceiling (disclosed by over_ceiling=, but past a hard ceiling). +// THE RESUME INDEX, next_offset= (present when rows were DROPPED, not only shrunk): the candidate index the cut starts at — +// the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no +// row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation +// whose at= differs was served by a different index (re-run from the start). +// MCP: no pasteable argv (the CLI's ranking pipeline is not the twin's, so a CLI re-run could continue a different list) — +// the tag carries the machine form instead: next_budget_tokens=T, the budget_tokens a re-call of the same tool needs. // COMPATIBLE WITH #362 (the budgeted-bundle candidate page, issue #294): when that page lands, the resumable form is -// `--for=TASK --token-budget=N --limit=L --offset=`, which serves ONLY the cut rows; it replaces the VALUE built -// here, not the attribute (same element, same name — #362's page root carries its own continuation as next= on ). +// `--for=TASK --token-budget=N --limit=L --offset=`, which serves ONLY the cut rows (walking pages until +// has_more="0"); it replaces the VALUE of next= built here, not the attributes — next_offset= is already that page's +// resume index, and #362's page root carries the same next=/next_offset= pair on . struct SigsCutContinuation { std::string_view task; // the --for task, echoed verbatim (nextFlag quotes it for a shell) @@ -4515,7 +4547,10 @@ struct SigsCutContinuation std::string_view rankFlags = {}; // the caller's RANKING flags, pre-spelled (" --no-route --adaptive"…): the re-run must rank // the same set, or "the rows this block cut" are rows of a different list std::size_t fixedBytes = 0; // what the re-run's ceiling must carry besides the block: the charged header + sibling blocks + bool charged = false; // reserve it (and its legend clause) INSIDE the block's budget — see the regime rule above bool json = false; // the --json dialect: the re-run is --json too + bool pasteHandle = true; // false on MCP: no pasteable CLI argv (the CLI ranks its own list) — the machine form, + // next_budget_tokens=, is what a re-call of the same tool needs }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -4525,11 +4560,13 @@ inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedB const std::size_t needBytes = blockBytes + fixedBytes + kSlackBytes; std::size_t tokens = std::size_t( double( needBytes ) / ( kMinBytesPerToken * kBudgetHeadroom ) ) + 1; tokens = ( tokens + kTokenStep - 1 ) / kTokenStep * kTokenStep; - while( budgetBytesForTokens( tokens ) < needBytes ) // the conversion floors; never hand back a ceiling below the need + std::size_t grantedBytes = budgetBytesForTokens( tokens ); + while( grantedBytes < needBytes ) // the conversion floors; never hand back a ceiling below the need { - tokens += kTokenStep; + tokens += kTokenStep; + grantedBytes = budgetBytesForTokens( tokens ); } - ENSURES( budgetBytesForTokens( tokens ) >= needBytes ); + ENSURES( grantedBytes >= needBytes, "the re-run's ceiling covers the untrimmed block plus what rides beside it" ); return tokens; } @@ -4604,12 +4641,55 @@ inline SigsCutReport sigsCutReportOf( const std::vector& entries, std::s { cut.shown += e.dropped ? 0u : 1u; cut.docsDropped += ( !e.dropped && e.hadDoc && e.doc.empty() ) ? 1u : 0u; + cut.lastShownRank = e.dropped ? cut.lastShownRank : std::max( cut.lastShownRank, e.globalRank ); } ASSUME( cut.docsDropped <= plan.docsDroppable ); // the reservation is an upper bound (the floor keeps its doc) ENSURES( cut.shown <= cut.total ); return cut; } +// knob-honesty-068: the cut's continuation, filled from the caller's request once the cut is known. Rows dropped (not only +// shrunk) ⇒ next_offset= too (the resume index). `tokens` sizes the re-run (sigsCutNextTokens). +inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinuation* req, const std::string& invocation, std::size_t tokens ) +{ + if( req == nullptr || !cut.isCapped ) + { + return; + } + cut.hasContinuation = true; + if( req->pasteHandle ) + { + cut.next = invocation; + } + else + { + cut.nextBudgetTokens = tokens; + } +} +inline bool sigsCutHasNextOffset( const SigsCutReport& cut ) noexcept +{ + return cut.hasContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; +} +// the legend clauses a cut report owes, every reading included (sigsCutLegendNotes, from the report itself) +inline std::string sigsCutReportLegend( const SigsCutReport& cut ) +{ + return sigsCutLegendNotes( cut.isCapped, cut.shown, cut.total, cut.docsDropped, !cut.next.empty(), cut.nextBudgetTokens > 0, + sigsCutHasNextOffset( cut ) ); +} +// The bytes a CHARGED continuation adds, an upper bound fixed before the plan: the attribute(s) at their widest digits, and +// every clause they may bring. `dialectAttrBytes` is the dialect's own spelling of the value-bearing attribute(s). +inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, std::size_t valueAttrBytes, std::size_t offsetAttrBytes, + bool withClauses ) +{ + if( req == nullptr || !req->charged ) + { + return 0; + } + const std::size_t clauses = withClauses ? ( req->pasteHandle ? kForSigsNextNote.size() : kForSigsNextBudgetNote.size() ) + kForSigsNextOffsetNote.size() + : 0u; + return valueAttrBytes + offsetAttrBytes + clauses; +} + // The open tag: `` untrimmed; ` shown= total= capped="1"` when rows were cut or shrunk; ` docs_dropped=` // when a shown row lost its doc comment. The bytes it adds are the ones planSigsTrim reserved. inline std::string sigsOpenTag( const SigsCutReport& cut ) @@ -4626,7 +4706,18 @@ inline std::string sigsOpenTag( const SigsCutReport& cut ) rw::formatTo( nb, sizeof( nb ), " docs_dropped=\"{}\"", cut.docsDropped ); tag += nb; } - tag += nextAttrXml( cut.next ); // knob-honesty-068: the cut's continuation ("" unless capped and asked for) + // knob-honesty-068: the cut's continuation — the resume index, then the call (pasteable, or the MCP machine form) + if( sigsCutHasNextOffset( cut ) ) + { + rw::formatTo( nb, sizeof( nb ), " next_offset=\"{}\"", cut.lastShownRank ); + tag += nb; + } + tag += nextAttrXml( cut.next ); + if( cut.nextBudgetTokens > 0 ) + { + rw::formatTo( nb, sizeof( nb ), " next_budget_tokens=\"{}\"", cut.nextBudgetTokens ); + tag += nb; + } tag += ">"; return tag; } @@ -4999,11 +5090,17 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // the tag's own attributes cost bytes — budget the trimmed state INCLUDING them (planSigsTrim; capture-audit // 2026-09-04 for the shown=/total= marker, cut-fix lane A for docs_dropped=). - // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block, and NOT reserved by the - // plan (SigsCutContinuation: exempt from the sig trim, so the rows shown are the ones they were) - const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); + // knob-honesty-068: the continuation a capped tag carries — sized from the UNTRIMMED block, known before the plan, so + // a CHARGED one rides the marker's reservation (attribute + its legend clause) and an exempt one costs the plan nothing + const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); + const std::size_t cutNextTokens = cutNext != nullptr ? sigsCutNextTokens( total, cutNext->fixedBytes ) : 0u; + const std::size_t cutNextCost = sigsCutContinuationReserve( + cutNext, + ( cutNext != nullptr && cutNext->pasteHandle ) ? nextAttrXml( cutNextInv ).size() + : sizeof( " next_budget_tokens=\"\"" ) - 1 + sigsDecimalDigits( cutNextTokens ), + sizeof( " next_offset=\"\"" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/true ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); + sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1 + cutNextCost, sizeof( " docs_dropped=\"\"" ) - 1 ); if( plan.ladderFires ) { // one ladder ACTION on one entry, tail-first; every action re-checks the budget so the ladder @@ -5048,10 +5145,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: *cappedOut = plan.ladderFires; } SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); - if( cut.isCapped ) - { - cut.next = cutNextInv; // "" when the caller asked for none - } + sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { *cutOut = cut; @@ -9424,10 +9518,14 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s // reserved INSIDE this array's budget exactly as the XML tag's attributes are inside its block's — they used to be // absent, and a JSON consumer could not tell 24 rows of 40 from 24 of 24. // (Without the rank-adaptive payload every globalRank is 0: no row is droppable and no doc is ever removed.) - // knob-honesty-068: the "sigs_next" key, exempt from the trim exactly as the XML twin's next= (SigsCutContinuation) - const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); + // knob-honesty-068: the "sigs_next" key, charged or exempt by the XML twin's rule (SigsCutContinuation; no legend here) + const std::string cutNextInv = cutNext != nullptr ? sigsCutNextInvocation( *cutNext, total ) : std::string(); + const std::size_t cutNextTokens = cutNext != nullptr ? sigsCutNextTokens( total, cutNext->fixedBytes ) : 0u; + const std::size_t cutNextCost = sigsCutContinuationReserve( cutNext, nextFieldJson( cutNextInv, "sigs_next" ).size(), + sizeof( ",\"sigs_next_offset\":" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), + /*withClauses=*/false ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); + sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1 + cutNextCost, sizeof( ",\"docs_dropped\":" ) - 1 ); if( plan.ladderFires ) { trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost ); @@ -9439,10 +9537,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s if( cutOut ) { *cutOut = sigsCutReportOf( entries, totalRows, plan ); - if( cutOut->isCapped ) - { - cutOut->next = cutNextInv; // knob-honesty-068: "" when the caller asked for none - } + sigsCutAttachContinuation( *cutOut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for } if( droppedPositiveOut ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index 62617364b..292cea6ab 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1455,8 +1455,9 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t fixedBytes = header.size() + kJsonEnvelopeBytes + kJsonSurfaceCountsBytes + ( in.noteIndex ? kJsonNotesStanzaBytes : 0 ); const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; - // knob-honesty-068: the cut's continuation, exempt from the trim like the XML twin's next= (SigsCutContinuation) - const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*json=*/true }; + // knob-honesty-068: the cut's continuation. This dialect serves no bodies, so its sig side IS the ceiling: charged under + // an explicit --token-budget, exempt at the default (the XML twin's rule, SigsCutContinuation) + const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*charged=*/in.tokenBudget > 0, /*json=*/true }; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex }; @@ -1558,7 +1559,11 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For if( sigsCut.isCapped ) { sigsCutStanza += ",\"sigs_shown\":" + std::to_string( sigsCut.shown ) + ",\"sigs_total\":" + std::to_string( sigsCut.total ); - sigsCutStanza += rw::nextFieldJson( sigsCut.next, "sigs_next" ); // knob-honesty-068: the call serving the cut rows + if( rw::sigsCutHasNextOffset( sigsCut ) ) // knob-honesty-068: the resume index, then the call serving the cut rows + { + sigsCutStanza += ",\"sigs_next_offset\":" + std::to_string( sigsCut.lastShownRank ); + } + sigsCutStanza += rw::nextFieldJson( sigsCut.next, "sigs_next" ); } if( sigsCut.docsDropped > 0 ) { @@ -3000,10 +3005,12 @@ std::optional runForLens( const MainDispatch& d ) // (the same reason est_tokens is "omitted", not "wrong", on that path — see its DISCLOSE). std::size_t forDroppedPositive = 0; // knob-honesty-068: a capped names the call that serves it uncut (serialize.h SigsCutContinuation, where the - // exempt-from-the-trim rule is argued: the ranked set is the one it was at every ceiling). Its bytes are measured - // downstream like every spliced disclosure: est_tokens, the header ladder and over_ceiling= see them. - const std::string forRankFlags = forRankShapingFlags( cfg ); - const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, /*json=*/false }; + // two regimes are argued): CHARGED exactly when an explicit ceiling's sig side is the whole ceiling (nothing + // downstream can pay, and the ceiling is hard); exempt at the default and when the sig side is frozen at the + // default's share (forSigSideCeiling), so those ranked sets — and forbudgetmonotoncheck's identity — are untouched. + const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; + const std::string forRankFlags = forRankShapingFlags( cfg ); + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below sigsPreRendered = preRender( [ & ]( std::FILE* sm ) @@ -3099,7 +3106,7 @@ std::optional runForLens( const MainDispatch& d ) // cut-fix lane A: the tag's two cut readings (docs_dropped=, shrunk-not-dropped) ride the same splice, present // only when the tag carries the case (serialize.h sigsCutLegendNotes), so the reserve below already covers them. const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( rw::kForBudgetBytesNote ) : std::string() ) - + rw::sigsCutLegendNotes( forSigsCut.isCapped, forSigsCut.shown, forSigsCut.total, forSigsCut.docsDropped, !forSigsCut.next.empty() ); + + rw::sigsCutReportLegend( forSigsCut ); // ── the INDEXING-cap disclosure (mention.h CapDisclosure), at the same splice point and for the // same reason: a --for header is charged against the payload ceiling, so a disclosure folded into // the notes above is paid for in ranked rows. Measured on sixteen real invocations, that cost three diff --git a/test/detailcheck.sh b/test/detailcheck.sh index 7f99f83a1..608e827a0 100755 --- a/test/detailcheck.sh +++ b/test/detailcheck.sh @@ -39,7 +39,9 @@ bodies(){ grep -o '' "$1" | wc -l | tr -d ' '; } +# knob-honesty-068: a capped tag also carries its continuation, next="…" (the call that serves it uncut) — the block is the +# same block, so the shape admits the attribute; the count it asserts is unchanged. +sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } # ── #1: --detail=0 == no --detail (byte-identical, golden-neutral) ────────────────────────────────────── "$BIN" src --for="$TASK" --no-cache >"$TMP/plain" 2>/dev/null diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index e9791d316..ad035f8fd 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -417,9 +417,9 @@ esac # The ladder disclosed its cut (shown= total= capped="1") and named no call that serves the cut rows: the bundle's only # next= was the r=1 row's (a body, or the widening page). Fixture: 40 matching C functions with long docs and long # parameter lists, so the DEFAULT ceiling drops rows. The tag must carry next= (+ its legend clause), pasting it must -# serve all total= rows uncut in the same rank order, the shown rows must be its prefix, and under a tight explicit -# ceiling it is still there and either fits (est_tokens <= budget_tokens) or the root says over_ceiling="1" — it is -# exempt from the sig trim (no row is spent on it), so an overshoot is disclosed, never hidden. JSON + MCP twins. +# serve all total= rows uncut in the same rank order, the shown rows must be its prefix. Under a tight explicit (hard) +# ceiling it is CHARGED: still there, and est_tokens stays within budget_tokens. At an explicit ceiling wide enough that +# the sig side is frozen at the default's it is EXEMPT: the block is byte-identical to the default's. JSON + MCP. # Negatives: an uncapped tag (arm 8's fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). mkdir -p "$TMP/fxcut" python3 - "$TMP/fxcut" <<'PY' @@ -440,9 +440,14 @@ tag = m.group( 1 ) nx = re.search( r"\snext=\"([^\"]*)\"", tag ) sh = re.search( r"shown=\"([0-9]+)\"", tag ); tt = re.search( r"total=\"([0-9]+)\"", tag ) names = re.findall( r"]*\bn=\"([^\"]*)\"", m.group( 2 ) ) +ranks = [ int( r ) for r in re.findall( r"]*\br=\"([0-9]+)\"", m.group( 2 ) ) ] +no_ = re.search( r"\snext_offset=\"([0-9]+)\"", tag ); nb_ = re.search( r"\snext_budget_tokens=\"([0-9]+)\"", tag ) out = { "capped": "capped=\"1\"" in tag, "shown": int( sh.group( 1 ) ) if sh else len( names ), "total": int( tt.group( 1 ) ) if tt else len( names ), "next": nx.group( 1 ).replace( "'", chr( 39 ) ).replace( """, chr( 34 ) ).replace( "&", "&" ) if nx else "", - "clause": "[sigs next=:" in s[ : s.find( "]*>", s ); r = root.group( 0 ) if root else "" for k in ( "est_tokens", "budget_tokens" ): v = re.search( r"\b%s=\"([0-9]+)\"" % k, r ); out[ k ] = int( v.group( 1 ) ) if v else None @@ -460,7 +465,14 @@ case "$NX1" in "--for='gadget assembler' --signatures-only --token-budget="[0-9]*) ok "(9) the capped names its call: next=\"$NX1\"" ;; *) no "(9) the capped carries next='$NX1' (want --for='gadget assembler' --signatures-only --token-budget=N)" ;; esac -[ "$( cfield "$C1" clause )" = true ] && ok "(9) the [sigs next=: …] clause defines it ahead of " || no "(9) rides with no legend clause" +if [ "$( cfield "$C1" clause )" = true ]; then ok "(9) the [sigs next=: …] clause defines it ahead of "; else no "(9) rides with no legend clause"; fi +# the resume index (the #362 candidate page's next_offset): the last printed row's candidate rank, defined by its own clause +if [ "$( cfield "$C1" next_offset )" = "$( cfield "$C1" max_r )" ] && [ "$( cfield "$C1" next_offset )" = "$( cfield "$C1" shown )" ] \ + && [ "$( cfield "$C1" offset_clause )" = true ]; then + ok "(9) next_offset=\"$( cfield "$C1" next_offset )\" is the resume index: the last printed row's rank (= shown, no pseudo slot here), defined" +else + no "(9) next_offset='$( cfield "$C1" next_offset )' vs last printed r=$( cfield "$C1" max_r ), shown=$( cfield "$C1" shown ), clause=$( cfield "$C1" offset_clause )" +fi # paste it (shlex-split, as an agent would) pasted(){ python3 -c 'import shlex,sys; print( "\0".join( shlex.split( sys.argv[1] ) ), end = "" )' "$1" > "$TMP/nx.argv"; ( cd "$TMP" && xargs -0 "$BIN" "$2" --no-cache < "$TMP/nx.argv" 2>/dev/null ); } C2="$( [ -n "$NX1" ] && pasted "$NX1" fxcut | cut_arm )" @@ -472,19 +484,26 @@ fi python3 -c 'import json,sys; a=json.loads(sys.argv[1])["names"]; b=json.loads(sys.argv[2])["names"]; sys.exit(0 if a and b[:len(a)]==a else 1)' "$C1" "${C2:-{\"names\":[]\}}" 2>/dev/null \ && ok "(9) the cut block's rows are a prefix of the continuation's (same ranked list, same order)" \ || no "(9) the continuation does not extend the cut block's rows in order" -# tight explicit ceiling: still named, and either inside the budget the root names or disclosed over it; the call recovers the rows +# tight explicit ceiling: CHARGED — still named, and est_tokens stays within the hard ceiling the root names C3="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=1500 --no-cache 2>/dev/null | cut_arm )" NX3="$( cfield "$C3" next )" -{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] \ - && { [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ] || [ "$( cfield "$C3" over )" = true ]; }; } \ - && ok "(9) --token-budget=1500: next= present; est_tokens $( cfield "$C3" est_tokens ) vs budget_tokens 1500 (over_ceiling=$( cfield "$C3" over ))" \ +{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] \ + && [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ]; } \ + && ok "(9) --token-budget=1500: next= present and charged (est_tokens $( cfield "$C3" est_tokens ) <= budget_tokens 1500)" \ || no "(9) --token-budget=1500: $C3" +# wide explicit ceiling (sig side frozen at the default's): EXEMPT — the block, next= included, is the default's byte for byte +sigsblk(){ python3 -c 'import re,sys; m=re.search(r"]*>.*?",sys.stdin.read(),re.S); print(m.group(0) if m else "")'; } +if [ "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-cache 2>/dev/null | sigsblk )" = "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=8000 --no-cache 2>/dev/null | sigsblk )" ]; then + ok "(9) --token-budget=8000: exempt — the block (its next= included) is the default's byte for byte" +else + no "(9) --token-budget=8000: the block differs from the default's (the exempt regime moved rows or its next=)" +fi C4="$( [ -n "$NX3" ] && pasted "$NX3" fxcut | cut_arm )" [ -n "$C4" ] && [ "$( cfield "$C4" capped )" = false ] && [ "$( cfield "$C4" shown )" = "$( cfield "$C3" total )" ] \ && ok "(9) --token-budget=1500: pasting its next= serves the block uncut" || no "(9) --token-budget=1500: the pasted next= left it cut: ${C4:-}" -# negatives: an uncapped tag carries no next=; a ranking flag is echoed (the re-run must rank the same list) +# negatives: an uncapped tag carries no next=/next_offset=; a ranking flag is echoed (the re-run must rank the same list) U1="$( cd "$TMP" && "$BIN" fxdocs --for="widget helper" --no-cache 2>/dev/null | cut_arm )" -[ "$( cfield "$U1" capped )" = false ] && [ -z "$( cfield "$U1" next )" ] && [ "$( cfield "$U1" clause )" = false ] \ +[ "$( cfield "$U1" capped )" = false ] && [ -z "$( cfield "$U1" next )" ] && [ "$( cfield "$U1" clause )" = false ] && [ "$( cfield "$U1" next_offset )" = null ] \ && ok "(9) negative: an uncapped carries no next= and no clause" || no "(9) negative: uncapped fixture: $U1" case "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --no-route --no-cache 2>/dev/null | cut_arm | python3 -c 'import json,sys; print(json.load(sys.stdin)["next"])' )" in *' --no-route'*) ok "(9) --no-route is echoed in the continuation" ;; @@ -497,21 +516,34 @@ case "$JNX" in *' --signatures-only '*' --json') ok "(9) JSON twin: \"sigs_next\" names the --json call" ;; *) no "(9) JSON twin: sigs_next='$JNX'" ;; esac +JNO="$( printf '%s' "$J1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("sigs_next_offset"), d.get("sigs_shown"))' 2>/dev/null )" +case "$JNO" in "None"*|"") no "(9) JSON twin: no sigs_next_offset ('$JNO')" ;; *) set -- $JNO; if [ "$1" = "$2" ]; then ok "(9) JSON twin: sigs_next_offset=$1 (the resume index)"; else no "(9) JSON twin: sigs_next_offset=$1 vs sigs_shown=$2"; fi ;; esac J2="$( [ -n "$JNX" ] && pasted "$JNX" fxcut | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("capped"), len(d["sigs"]), "sigs_next" in d)' 2>/dev/null )" [ "$J2" = "False $( printf '%s' "$J1" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("sigs_total"))' 2>/dev/null ) False" ] \ && ok "(9) JSON twin: pasting sigs_next serves every row uncut" || no "(9) JSON twin: pasted sigs_next gave '$J2'" -# MCP twin (signatures only, the same default ceiling): the same attribute on its -M1="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"gadget assembler"}}}\n' "$TMP/fxcut" \ +# MCP twin (signatures only, its own ranking pipeline): NO pasteable CLI argv — the machine form next_budget_tokens=T (and +# next_offset=); re-calling `for` with budget_tokens=T serves every row uncut +mcpfor(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"gadget assembler"%s}}}\n' "$TMP/fxcut" "$1" \ | "$BIN" --mcp 2>/dev/null | python3 -c 'import json,sys for line in sys.stdin: line=line.strip() if not line: continue d=json.loads(line) - if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm )" -case "$( cfield "${M1:-{\}}" next 2>/dev/null )" in - "--for='gadget assembler' --signatures-only --token-budget="[0-9]*) ok "(9) MCP twin: the capped carries the same next= shape" ;; - *) no "(9) MCP twin: ${M1:-}" ;; -esac + if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm; } +M1="$( mcpfor '' )" +MNB="$( cfield "${M1:-{\}}" next_budget_tokens 2>/dev/null )" +if [ -n "$M1" ] && [ "$( cfield "$M1" capped )" = true ] && [ -z "$( cfield "$M1" next )" ] && [ "$MNB" != null ] && [ "$( cfield "$M1" budget_clause )" = true ] \ + && [ "$( cfield "$M1" next_offset )" = "$( cfield "$M1" max_r )" ]; then + ok "(9) MCP twin: no CLI argv on ; next_budget_tokens=\"$MNB\" and next_offset=\"$( cfield "$M1" next_offset )\", both defined" +else + no "(9) MCP twin: ${M1:-}" +fi +M2="$( [ "$MNB" != null ] && mcpfor ",\"budget_tokens\":$MNB" )" +if [ -n "$M2" ] && [ "$( cfield "$M2" capped )" = false ] && [ "$( cfield "$M2" shown )" = "$( cfield "$M1" total )" ]; then + ok "(9) MCP twin: re-calling for with budget_tokens=$MNB serves all $( cfield "$M2" shown ) rows uncut" +else + no "(9) MCP twin: the re-call with budget_tokens=$MNB did not serve the block uncut: ${M2:-}" +fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" diff --git a/test/truncvocabcheck.sh b/test/truncvocabcheck.sh index ed9bfba24..d2f0f1c1e 100755 --- a/test/truncvocabcheck.sh +++ b/test/truncvocabcheck.sh @@ -318,7 +318,9 @@ run "$TINY" --for="add one to a number" > "$TMP/tiny_for.xml" grep -q '' "$TMP/tiny_for.xml" \ && ok "(C) --for untrimmed: bare , no marker (rule 5: absent = untrimmed)" \ || no "(C) --for on a two-symbol corpus is not the bare untrimmed " -grep -qE '' "$TMP/sweep_forbudget.xml" \ +# knob-honesty-068: a trimmed tag also carries its continuation, next= (the call that serves it uncut) — the pair is +# still asserted immediately after the tag name; only the closing `>` admits the trailing attributes the tag may carry. +grep -qE '' "$TMP/sweep_forbudget.xml" \ && ok "(C) --for trimmed by --token-budget: (rule 5, amended: the pair rides on a byte trim too)" \ || no "(C) --for --token-budget=800 did not mark its trimmed payload : $( grep -oE ']*>' "$TMP/sweep_forbudget.xml" | head -1 )" From d4da24e7b660b28f454b079125ae672014abea8d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:08:11 -0400 Subject: [PATCH 086/229] chore(lint): register --lint-max-per-rule on every surface that counts flags - docs/COMMANDS.md and docs/LIMITS.md regenerated (the new flag's entry; lintrules.h now lists the count_capped disclosure it has). - test/printf_parity.manifest re-pinned: exactly help and help_all moved (the new flag's help lines), 40 labels unchanged. - README and the deck generator: 185 -> 186 long flags (deckclaimcheck derives the count). - ripwire-fresh-eyes names count_capped / findings_next= / --lint-max-per-rule (agentsurfacecheck). - validateLintSelectionModifierGuards becomes one table of modifier refusals instead of a third copied branch (same messages, same order); builtInLintCaptures' budget parameter is defaulted. - lintbudgetcheck: one-line verdicts rewritten as if/then/else (gateexitcheck G2). Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++--- docs/COMMANDS.md | 16 ++++++++++++++-- docs/LIMITS.md | 6 +++--- present/deck5_ripwire_build.js | 6 +++--- skills/ripwire-fresh-eyes/SKILL.md | 3 +++ src/cli.h | 28 ++++++++++++++++++---------- src/verbs_lint.h | 2 +- test/lintbudgetcheck.sh | 2 +- test/printf_parity.manifest | 4 ++-- 9 files changed, 48 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 26dd08715..801afe48a 100644 --- a/README.md +++ b/README.md @@ -945,9 +945,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-185 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin +186 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin -Around the core sit 185 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 186 long flags advertised in `--help`, across seven families — plus an MCP server, so a coding agent can call any of them mid-task instead of grepping and reading whole files. `--help` prints one line per flag (~4.5K tokens); `--help=--FLAG` prints that flag's full entry with every caveat, `--help=SECTION` one family, and `--help=all` the whole catalog. @@ -2644,7 +2644,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 185 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 186 long flags advertised in `--help` into seven families. The `--help=` column below is the argument that prints one family: `ripwire --help=navigate`. `ripwire --help=--FLAG` prints one flag's full text — the caveats, the units, what it refuses and why. `ripwire --help=all` is the whole catalog, about 46,000 tokens. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e2b0a2fe2..e7d960d34 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -44,7 +44,7 @@ Two limits apply to nearly everything here and are not repeated in every section **zoom the detail ladder** — [`--detail`](#--detailn) · [`--pack-signatures`](#--pack-signatures) · [`--outline`](#--outlineab) · [`--expand`](#--expandab) · [`--compress`](#--compress) · [`--pack-top-n`](#--pack-top-nn) · [`--no-redact`](#--no-redact) -**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--lint-max-per-rule`](#--lint-max-per-rulen) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) **self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) @@ -2449,7 +2449,7 @@ $ ./build/ripwire . --lint-rules=test/lintrulesfix/rules ``` -**Shaped by:** `--lint-catalog`, `--lint-select`, `--lint-ignore`, `--sarif` +**Shaped by:** `--lint-catalog`, `--lint-select`, `--lint-ignore`, `--lint-max-per-rule`, `--sarif` ### `--lint-select=PREFIX[,...]` @@ -2502,6 +2502,18 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- ... [17 more line(s); run it to see the whole thing] ``` +### `--lint-max-per-rule=N` + +**Answers:** (with --lint) raise each rule's match budget above the default 5000 (with --lint / --lint-rules) each rule's raw-capture budget; + +the default (5000) is a runaway guard, not a target. A rule that spends it carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext) re-runs only the floored rules under a 10x budget; a re-run still floored names its own. Raising it costs time and memory in proportion to the matches kept + +**Caveats (stated by the binary):** + +- the default (5000) is a runaway guard, not a target. +- A rule that spends it carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext) re-runs only the floored rules under a 10x budget; +- a re-run still floored names its own. + ### `--sarif` **Answers:** (with --lint / --lint-rules) the SAME findings as SARIF 2.1.0 instead of the native XML block — the shape github/codeql-action/upload-sarif consumes for code scanning. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index fbeb13a73..da7ecbf87 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,7 +17,7 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 236 | 94 | 135 | **101** | +| 236 | 94 | 136 | **100** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they are not counted as caps, and 236 + 7 is the 243 constants this generator parses out of `src/`. @@ -134,7 +134,7 @@ Discloses: **none** ### `src/cli.h` -Discloses: `capped`, `bridges_capped`, `files_capped`, `inc_capped`, `mention_idents_capped`, `mention_syms_capped`, `modules_capped`, `rows_capped`, `sibs_capped`, `syms_capped`, `tests_capped`, `unflagged_capped`, `untested_capped` +Discloses: `capped`, `bridges_capped`, `count_capped`, `files_capped`, `inc_capped`, `mention_idents_capped`, `mention_syms_capped`, `modules_capped`, `rows_capped`, `sibs_capped`, `syms_capped`, `tests_capped`, `unflagged_capped`, `untested_capped` | constant | value | class | note | | --- | --- | --- | --- | @@ -571,7 +571,7 @@ Discloses: **none** ### `src/lintrules.h` -Discloses: **none** +Discloses: `count_capped` | constant | value | class | note | | --- | --- | --- | --- | diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..862c96f65 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -195,7 +195,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["byte-identical", "two runs, same bytes — a gate on every push, not a tendency; warm equals cold"], ["zero runtime deps", "CMake + a C++23 compiler; builds with the network off — vendored everything"], ["the languages", "Rust · C++ · ObjC/C++ · C · Metal · CUDA · Python · Go · Swift · TypeScript · JavaScript · Java · Kotlin · Ruby · PHP · Lua · Elixir · Dart · GDScript · Bash · C# · JSON · TOML · YAML · Markdown — 25 vendored grammars, Astro frontmatter on the TypeScript one; markdown headings are real symbols"], // GDScript: CHANGELOG [0.6.2] (#233); Astro: CHANGELOG [0.6.4] (#320) - ["agent-native", "an MCP server and 185 long flags behind one `--help` that is always the authority"], + ["agent-native", "an MCP server and 186 long flags behind one `--help` that is always the authority"], ]; // Row height carries the LONGEST body (the language line, which wraps to three at this width), // not the shortest — a fixed 0.68 clipped its last line off the bottom of the card. @@ -230,7 +230,7 @@ function storyCards(s, { kick, head, stories, footText }){ s.addText(flags, { x: MX+6.3, y: y+0.04, w: 5.65, h: 0.58, fontFace: MONO, fontSize: 9, color: CYAN, valign: "middle", margin: 0 }); y += 0.74; } - foot(s, "--help is generated from the binary's own flag table — 185 long flags; docs/COMMANDS.md carries an entry for every one of them, 162 with a recorded invocation and its output"); + foot(s, "--help is generated from the binary's own flag table — 186 long flags; docs/COMMANDS.md carries an entry for every one of them, 162 with a recorded invocation and its output"); } /* ── S5 · the moments ───────────────────────────────────────────────────── */ @@ -1537,7 +1537,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// do not take any of it on trust", AMBER); title(s, "Every claim, and the command that re-derives it"); const claims = [ - ["185 long flags · 36 slides", "bash test/deckclaimcheck.sh"], + ["186 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"], ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount diff --git a/skills/ripwire-fresh-eyes/SKILL.md b/skills/ripwire-fresh-eyes/SKILL.md index dcd8f239e..8f61444ee 100644 --- a/skills/ripwire-fresh-eyes/SKILL.md +++ b/skills/ripwire-fresh-eyes/SKILL.md @@ -225,6 +225,9 @@ sections your question needs. (comma-separated, `*` for "every rule"): `--lint-select=cache-` runs only the `cache-*` family, disclosing `selected="K of N"` on the root so a filtered zero is never confusable with an unfiltered one; an unresolvable PREFIX refuses with a near-miss rather than silently matching nothing. + **A rule row with `count_capped="1"`** spent its per-rule match budget (5000 by default), so its `count=` is a FLOOR; + the root then carries `findings_next=` — paste it: it re-runs only those rules under a 10x `--lint-max-per-rule=N` + (SARIF: `runs[0].properties.findingsNext`), and a re-run still floored names its own. **The one naming lens that proposes a FIX, not just evidence — `ripwire --naming-consistency --legend=compact`.** Every other pass on this page tells you WHAT is wrong; this one is the deliberate exception, and only for diff --git a/src/cli.h b/src/cli.h index aef45193f..0c7419bae 100644 --- a/src/cli.h +++ b/src/cli.h @@ -4242,20 +4242,28 @@ inline void validateSarifModifierGuards( Config& c ) noexcept // is validated inside runLint, not here — see resolveLintSelection's own header for why. inline void validateLintSelectionModifierGuards( Config& c ) noexcept { - if( !c.lintSelect.empty() && !c.lint && c.lintRulesDir.empty() ) + if( c.lint || !c.lintRulesDir.empty() ) { - rw::emitRaw( stderr, "ripwire: --lint-select=PREFIX modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-select=cache-)\n" ); - c.ok = false; + return; // every modifier below has its verb } - if( !c.lintIgnore.empty() && !c.lint && c.lintRulesDir.empty() ) + // one row per lint modifier, refused in this order (knob-honesty-068 folded the third into a table, not a third copy) + struct LintModifierGuard { - rw::emitRaw( stderr, "ripwire: --lint-ignore=PREFIX modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-ignore=naming-)\n" ); - c.ok = false; - } - if( c.lintMaxPerRule > 0 && !c.lint && c.lintRulesDir.empty() ) + bool given; + const char* refusal; + }; + const LintModifierGuard guards[] = { + { !c.lintSelect.empty(), "ripwire: --lint-select=PREFIX modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-select=cache-)\n" }, + { !c.lintIgnore.empty(), "ripwire: --lint-ignore=PREFIX modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-ignore=naming-)\n" }, + { c.lintMaxPerRule > 0, "ripwire: --lint-max-per-rule=N modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-max-per-rule=50000)\n" }, + }; + for( const LintModifierGuard& g : guards ) { - rw::emitRaw( stderr, "ripwire: --lint-max-per-rule=N modifies --lint or --lint-rules=DIR — pass one (e.g. ripwire --lint --lint-max-per-rule=50000)\n" ); - c.ok = false; + if( g.given ) + { + rw::emitRaw( stderr, g.refusal ); + c.ok = false; + } } } diff --git a/src/verbs_lint.h b/src/verbs_lint.h index 3676494de..b555aaea0 100644 --- a/src/verbs_lint.h +++ b/src/verbs_lint.h @@ -770,7 +770,7 @@ inline std::size_t lintPackQueryBudget( std::size_t packDefault, std::size_t max } std::vector> builtInLintCaptures( const rw::IngestResult& ing, const std::vector& checks, - std::vector& keptBytes, std::size_t maxPerRule ) + std::vector& keptBytes, std::size_t maxPerRule = rw::kLintMaxPerRule ) { PROFILE_SCOPE_DESCRIBE( "lint: astQueryGrouped (built-in + atoms + cache + unreachable)" ); const std::vector atomChecks = rw::atoms::atomsSpecs(); diff --git a/test/lintbudgetcheck.sh b/test/lintbudgetcheck.sh index 2cc80f27a..835e77cff 100755 --- a/test/lintbudgetcheck.sh +++ b/test/lintbudgetcheck.sh @@ -190,7 +190,7 @@ U2="$( [ -n "$UNEXT" ] && "$BIN" "$GF" $UNEXT --no-cache 2>/dev/null )" URow="$( printf '%s' "$U2" | grep -oE '' | head -1 )" case "$URow" in *'count="6000"'*) ! printf '%s' "$URow" | grep -q 'count_capped' ;; *) false ;; esac \ && ok "user rules: pasting it counts all 6000, and that row is no longer floored" || no "user rules: pasted run: $URow" -printf '%s' "$G1" | xmllint --noout - 2>/dev/null && ok "floored answer is well-formed XML" || no "floored answer is not well-formed XML" +if printf '%s' "$G1" | xmllint --noout - 2>/dev/null; then ok "floored answer is well-formed XML"; else no "floored answer is not well-formed XML"; fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 41c8cfe8b..f19a55682 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 c74a677dde09eb76a71bfd0b9e2c75d074c47ecfac838654f84df80af6725b02 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 dadaf0250be6587d0fe44fb5db1aebad9659e6761c2c8857d4183a453138d88d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 5e82bb6e129da4086c1e64f5939be191964e5db6e7c49154edfee9d1cfdc44e6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 c9d925cdd682bdb5d109fe0868294db0e073e198e47ce14004aa4a141f39085b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 7880d16cdfb2ab5734930b58e9d43d92004b88ef Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:08:12 -0400 Subject: [PATCH 087/229] docs(changelog): every disclosed cut names the call that recovers it Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..a90d55ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,31 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — every disclosed cut names the call that recovers it: `--for`'s ``, `--lint`'s per-rule floor, `--impact`'s import tier + +Three cuts were disclosed and still dead ends — counted, with no pasteable call that serves the rest: + +- **`--for` (and the MCP `for` twin, and `--for --json`).** A `` block cut by the payload + ceiling or an explicit `--token-budget` now carries `next=` — the same ranked lens re-run with the signatures given the + whole ceiling, sized from the untrimmed block so nothing is cut (`--for=TASK --signatures-only [ranking flags] + [--pack-top-n=N] --token-budget=T`) — and, when rows were dropped, `next_offset=`: the candidate index the cut starts at + (the last printed row's rank, so a slot that prints no row cannot skew it). `--json` carries `"sigs_next"` and + `"sigs_next_offset"`. The MCP twin carries no CLI argv (its ranking pipeline is its own): `next_budget_tokens=T` names + the `budget_tokens` a re-call needs, beside the same `next_offset=`. Present-only legend clauses define each. At the + default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, it is exempt + from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) it is + charged like `shown=`/`total=`, so `est_tokens` stays within the budget; measured at `--token-budget=2000`, that costs one + row on 11 of 20 queries — a row the `next=` itself serves. No budget, ceiling or token conversion changed. +- **`--lint` / `--lint-rules` and `--sarif`.** A rule that spends its per-rule match budget (`count_capped="1"`) now makes + the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`): only the floored rules, under a 10x budget, + through the new `--lint-max-per-rule=N` (the default stays 5000). A re-run still floored names its own. +- **`--impact`.** A cut import tier dropped `importers_next=` when `--impact=SYM --limit=N` passed 120 bytes, so a long + selector lost its only continuation with no marker. It is now always emitted in full, like every other `next=`. + +Also: the `ripwire-handoff` skill passed `--top-k=20` to `--for`, which does not read it; it now spells +`--signatures-only`. Gates: `forrankordercheck` (9), `lintbudgetcheck` (5), `impactimportcheck` (#9d). + + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) The MCP `grep` hit row was `{file, line, in}`: no matched text, so an agent re-read every file it had just searched, and From a9d8a0a94492b31dc3ab448d594f882d776a4015 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:20:07 -0400 Subject: [PATCH 088/229] refactor(for): one report-based reading of a cut's legend clauses sigsCutLegendNotes had grown to seven parameters to carry the continuation's clauses. The clauses are now read off the SigsCutReport itself (sigsCutReportLegend, same order: shrunk, the continuation's, docs_dropped); the four-number form stays for the callers that hold no continuation (--from-trace, --pack-task) and builds a report. Output bytes unchanged. Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 66 ++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index a9008118f..e0fb9f834 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -874,34 +874,8 @@ inline constexpr std::string_view kForSigsNextBudgetNote = inline constexpr std::string_view kForSigsNextOffsetNote = " [sigs next_offset=: the candidate index the cut starts at; a resume must match the root at=]"; -// The clauses a cut report owes, concatenated in a fixed order ("" when it owes none). `hasNext`: the tag carries -// the cut's continuation (SigsCutReport::next). -inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped, bool hasNext = false, - bool hasNextBudget = false, bool hasNextOffset = false ) -{ - std::string notes; - if( isCapped && shown == total ) - { - notes += kForSigsShrunkNote; - } - if( isCapped && hasNext ) - { - notes += kForSigsNextNote; - } - if( isCapped && hasNextBudget ) - { - notes += kForSigsNextBudgetNote; - } - if( isCapped && hasNextOffset ) - { - notes += kForSigsNextOffsetNote; - } - if( docsDropped > 0 ) - { - notes += kForDocsDroppedNote; - } - return notes; -} +// The clauses a cut report owes, concatenated in a fixed order ("" when it owes none) — defined below SigsCutReport +// (sigsCutReportLegend), because the continuation's clauses read the report itself. // ── B0 round 2 (H1): GLOBAL deterministic payload budget for the ranked --for bundle ───────────────── // The rank tiers above cut only ~1% of the measured LocBench payload: the worst bundles are dominated by @@ -4670,11 +4644,41 @@ inline bool sigsCutHasNextOffset( const SigsCutReport& cut ) noexcept { return cut.hasContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; } -// the legend clauses a cut report owes, every reading included (sigsCutLegendNotes, from the report itself) +// the legend clauses a cut report owes, in a fixed order: shrunk-not-dropped, the continuation's (knob-honesty-068), docs_dropped inline std::string sigsCutReportLegend( const SigsCutReport& cut ) { - return sigsCutLegendNotes( cut.isCapped, cut.shown, cut.total, cut.docsDropped, !cut.next.empty(), cut.nextBudgetTokens > 0, - sigsCutHasNextOffset( cut ) ); + std::string notes; + if( cut.isCapped && cut.shown == cut.total ) + { + notes += kForSigsShrunkNote; + } + if( cut.isCapped && !cut.next.empty() ) + { + notes += kForSigsNextNote; + } + if( cut.isCapped && cut.nextBudgetTokens > 0 ) + { + notes += kForSigsNextBudgetNote; + } + if( sigsCutHasNextOffset( cut ) ) + { + notes += kForSigsNextOffsetNote; + } + if( cut.docsDropped > 0 ) + { + notes += kForDocsDroppedNote; + } + return notes; +} +// the same clauses from the four numbers a caller without a continuation holds (--from-trace, --pack-task) +inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped ) +{ + SigsCutReport cut; + cut.isCapped = isCapped; + cut.shown = shown; + cut.total = total; + cut.docsDropped = docsDropped; + return sigsCutReportLegend( cut ); } // The bytes a CHARGED continuation adds, an upper bound fixed before the plan: the attribute(s) at their widest digits, and // every clause they may bring. `dialectAttrBytes` is the dialect's own spelling of the value-bearing attribute(s). From 91b2d93f12f491cc35f7fb469800080cb1176448 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:32:52 -0400 Subject: [PATCH 089/229] skillscan: cut a finding's excerpt on a UTF-8 boundary, ellipsis only when bytes were dropped The 120-byte excerpt cap resized at byte 117, splitting a multibyte character of the untrusted skill text. Back off to the code-point boundary (the serialize.h truncateUtf8WithEllipsis rule) and keep a line of exactly the cap whole. New skillscan.sh arm: 2-byte and 4-byte characters straddling the cap, exactly 120 bytes, 121 ASCII bytes. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 8 ++++++++ src/skillscan.h | 29 ++++++++++++++++++++++++--- test/skillscan.sh | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..27596edbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — a `--scan-skills` / `wrap` finding's excerpt is cut on a character boundary + +A skill line longer than the 120-byte excerpt cap was cut at a byte offset, so a multibyte UTF-8 character straddling the +cap left an invalid tail in the excerpt (the text is arbitrary and untrusted). The cut now backs off to the code-point +boundary, the same rule as the other text caps, and the `...` is added only when bytes were really dropped; a line of +exactly the cap is shown whole. `test/skillscan.sh` pins a 2-byte and a 4-byte character at the cap, a line of exactly +120 bytes, and a plain overlong line. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) The MCP `grep` hit row was `{file, line, in}`: no matched text, so an agent re-read every file it had just searched, and diff --git a/src/skillscan.h b/src/skillscan.h index 246276256..0b07d193a 100644 --- a/src/skillscan.h +++ b/src/skillscan.h @@ -93,6 +93,31 @@ inline std::string_view trimRight( std::string_view s ) noexcept return s; } +// A finding's excerpt cap: at most kSkillExcerptMaxBytes bytes in all, "..." included. +inline constexpr std::size_t kSkillExcerptMaxBytes = 120; + +// The trimmed, clipped excerpt of one skill line. The text is UNTRUSTED and arbitrary UTF-8, so the cut never lands inside a +// multibyte character: it backs off to the code-point boundary, the same rule as serialize.h's truncateUtf8WithEllipsis +// (that header is not reachable from here), and the ASCII "..." is added only when bytes were really dropped — a line of +// exactly the cap is kept whole. +inline std::string clipSkillExcerpt( std::string_view lineText ) +{ + std::string excerpt( trimRight( lineText ) ); + if( excerpt.size() <= kSkillExcerptMaxBytes ) + { + return excerpt; + } + std::size_t cut = kSkillExcerptMaxBytes - 3; // room for the "..." + while( cut > 0 && ( static_cast( excerpt[cut] ) & 0xC0 ) == 0x80 ) + { + --cut; + } + excerpt.resize( cut ); + excerpt += "..."; + ENSURES( excerpt.size() <= kSkillExcerptMaxBytes, "the clipped excerpt fits the cap, ellipsis included" ); + return excerpt; +} + // ── INJECTION phrase table (CRITICAL, with a generic-word WARN fallback) ──────────────────────── // // A4-F12 fix: bare substrings ("disregard", "you are now", "new persona") false-positive on @@ -1210,9 +1235,7 @@ inline std::vector scanSkillTextOn( std::string_view text, std::si // helper: add a finding with a clipped excerpt const auto addFinding = [ & ]( SkillSeverity sev, int lineNum, const char* rule, std::string_view lineText, const char* why = nullptr ) { - std::string excerpt( trimRight( lineText ) ); - if( excerpt.size() > 120 ) { excerpt.resize( 117 ); excerpt += "..."; } - findings.push_back( { sev, lineNum, rule, std::move( excerpt ), why } ); + findings.push_back( { sev, lineNum, rule, clipSkillExcerpt( lineText ), why } ); }; // helper: the regex boundary's answer as a plain hit, failing CLOSED on an undecided match — the line gets a // CRITICAL scan-incomplete finding (deduped per line below), so an unscannable skill can never read "clean". diff --git a/test/skillscan.sh b/test/skillscan.sh index 07cd9b08a..2f62a7f84 100755 --- a/test/skillscan.sh +++ b/test/skillscan.sh @@ -476,6 +476,57 @@ else no "(merge) the mergeScriptPasses harness did not compile: $( head -3 "$TMP/merge.err" )" fi +# ── check 21: a finding's excerpt is cut on a code-point boundary, "..." only when bytes were dropped ── +# The excerpt is a clip of UNTRUSTED skill text (`wrap` prints it in its refusal), so a multibyte character at the cap +# must not be split (an invalid-UTF-8 tail in a terminal / log / JSON line), and text that fits is left alone. The cap is +# 120 bytes in all: at most 117 of the line, then "...". The arm drives scanSkillText on lines built around the cap: +# straddle2 a 2-byte é at bytes 116-117 (the 117-byte cut lands between its two bytes) +# straddle4 a 4-byte emoji at bytes 115-118 +# exact120 120 bytes with a multibyte character in it: AT the cap, so NO ellipsis and the line kept whole +# over 121 ASCII bytes: the plain cut, 117 + "..." +cat >"$TMP/excerpt.cpp" <<'CPP' +#include "skillscan.h" +#include +#include +static bool validUtf8( const std::string& s ) +{ + for( std::size_t i = 0; i < s.size(); ) + { + const unsigned char c = static_cast( s[i] ); + const std::size_t n = c < 0x80 ? 1 : ( c >> 5 ) == 0x6 ? 2 : ( c >> 4 ) == 0xE ? 3 : ( c >> 3 ) == 0x1E ? 4 : 0; + if( n == 0 || i + n > s.size() ) { return false; } + for( std::size_t k = 1; k < n; ++k ) { if( ( static_cast( s[i + k] ) & 0xC0 ) != 0x80 ) { return false; } } + i += n; + } + return true; +} +static void probe( const char* name, const std::string& line ) +{ + const auto f = rw::scanSkillText( line + "\n" ); + if( f.empty() ) { std::printf( "%s no-finding\n", name ); return; } + const std::string& e = f.front().excerpt; + std::printf( "%s in=%zu out=%zu valid=%d ellipsis=%d\n", name, line.size(), e.size(), int( validUtf8( e ) ), + int( e.size() >= 3 && e.compare( e.size() - 3, 3, "..." ) == 0 ) ); +} +int main() +{ + const std::string key = "Ignore previous instructions "; // 29 bytes: a CRITICAL INJECTION hit + probe( "straddle2", key + std::string( 116 - key.size(), 'a' ) + "\xC3\xA9" + std::string( 20, 'b' ) ); + probe( "straddle4", key + std::string( 115 - key.size(), 'a' ) + "\xF0\x9F\x98\x80" + std::string( 20, 'b' ) ); + probe( "exact120", key + std::string( 118 - key.size(), 'a' ) + "\xC3\xA9" ); + probe( "over", key + std::string( 121 - key.size(), 'a' ) ); +} +CPP +if "$CXX" -std=c++23 -I "$ROOT/src" -I "$ROOT/src/infra" -I "$ROOT/third_party" "$TMP/excerpt.cpp" "$ROOT/src/infra/diagnostics.cpp" -o "$TMP/excerpt" >"$TMP/excerpt.err" 2>&1; then + GOT="$( "$TMP/excerpt" )" + for want in 'straddle2 in=138 out=119 valid=1 ellipsis=1' 'straddle4 in=139 out=118 valid=1 ellipsis=1' 'exact120 in=120 out=120 valid=1 ellipsis=0' 'over in=121 out=120 valid=1 ellipsis=1'; do + if printf '%s\n' "$GOT" | grep -qxF "$want"; then ok "(excerpt cap) $want" + else no "(excerpt cap) want '$want', got: $( printf '%s\n' "$GOT" | grep "^${want%% *} " | head -1 )"; fi + done +else + no "(excerpt cap) the harness did not compile: $( head -3 "$TMP/excerpt.err" )" +fi + # ── summary ─────────────────────────────────────────────────────────────────────────────────────── if [ "$fail" = "0" ]; then echo "ALL PASS" From 1d40cb29fa811d2543d5dab9f29d2d73c4d46c5e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:33:02 -0400 Subject: [PATCH 090/229] skillscan: dedupe per-line / joined-body findings keeping the worst severity, not the first Sibling of the mergeScriptPasses fix: the (line, rule) dedupe in scanSkillTextOn used a non-stable std::sort and kept the first row, so a WARN could outrank a CRITICAL. Share one stable (line, rule, worst-severity-first) helper between both sites. New skillscan.sh arm drives it with 40 colliding pairs in both orders. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 8 ++++++++ src/skillscan.h | 47 +++++++++++++++++++++++++---------------------- test/skillscan.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27596edbe..fd6b35412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — the skill scanner's per-line / joined-body dedupe keeps the worst row of a (line, rule) + +The scanner's two passes can report the same (line, rule), and the dedupe sorted with a non-stable sort on (line, rule) +and kept the first row, so which severity survived was left to the sort. It now sorts stably with the worst severity first +(the rule the shell-script pass merge already follows), as one shared helper. No input reaches the collision today (a +rule's severity is a function of its line), so output is unchanged; `test/skillscan.sh` drives the helper with colliding +pairs in both orders. + ### Fixed — a `--scan-skills` / `wrap` finding's excerpt is cut on a character boundary A skill line longer than the 120-byte excerpt cap was cut at a byte offset, so a multibyte UTF-8 character straddling the diff --git a/src/skillscan.h b/src/skillscan.h index 0b07d193a..8d3a62955 100644 --- a/src/skillscan.h +++ b/src/skillscan.h @@ -1178,6 +1178,28 @@ static_assert( kSkillInjectionSpanBytes < kSkillJoinedOverlapBytes && kSkillJoin "a joined-body window overlap must exceed the longest INJECTION match, or a match across a window edge is lost" ); inline constexpr std::size_t kSkillScanStackBytes = 256 * 1024 * 1024; // the grep scan threads' size (search.h kGrepScanStackBytes) +// Sort findings (line, rule, severity WORST first) STABLY, then keep the first row per (line, rule): so a (line, rule) reported +// twice keeps its worst severity, and the earlier row on a tie. Keeping the first row after a non-stable sort on (line, rule) +// alone left the survivor to whatever the sort put first — a WARN could hide a CRITICAL and quiet the exit from 2 to 1. +// Today every rule's severity is a function of the line alone, so the severity key only ever breaks ties; it is the invariant, +// not a behaviour change, and the skillscan gate's dedupe arm holds it. Used by the per-line / joined-body dedupe in +// scanSkillTextOn and by mergeScriptPasses. +inline void dedupeFindingsKeepWorst( std::vector& findings ) +{ + std::stable_sort( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept + { + if( a.line != b.line ) + { + return a.line < b.line; + } + const int byRule = std::string_view( a.rule ).compare( b.rule ); + return byRule != 0 ? byRule < 0 : a.sev > b.sev; + } ); + findings.erase( std::unique( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept + { return a.line == b.line && std::string_view( a.rule ) == std::string_view( b.rule ); } ), + findings.end() ); +} + // Scan the raw text of a skill markdown file line by line, on a thread settled at `stackBytes`. Findings are sorted // (line, rule). // `wholeFileIsCode` (a shell script, see SkillFileKind): there is no frontmatter, every line is command context and no @@ -1578,19 +1600,11 @@ inline std::vector scanSkillTextOn( std::string_view text, std::si addFinding( p.sev, joinedLineOf( pos ), p.rule, m.str( 0 ) ); } - // ── sort by (line, rule) for deterministic output, then dedupe on (line, rule) ──────────────── + // ── sort by (line, rule, worst severity first) for deterministic output, then dedupe on (line, rule) ──────────────── // The per-line pass and the joined-body pass can both find the SAME instance (e.g. a phrase that sits // entirely on one line is found per-line, then found again — same line, same rule — in the joined // buffer); collapse those to a single finding so output stays deterministic and non-redundant. - std::sort( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept - { - if( a.line != b.line ) { return a.line < b.line; -} - return std::string_view( a.rule ) < std::string_view( b.rule ); } ); - findings.erase( std::unique( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept - { - return a.line == b.line && std::string_view( a.rule ) == std::string_view( b.rule ); - } ), findings.end() ); + dedupeFindingsKeepWorst( findings ); return findings; } @@ -1718,18 +1732,7 @@ inline SkillFileKind skillFileKindOf( std::string_view path, std::string_view te inline void mergeScriptPasses( std::vector& findings, std::vector code ) { findings.insert( findings.end(), std::make_move_iterator( code.begin() ), std::make_move_iterator( code.end() ) ); - std::stable_sort( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept - { - if( a.line != b.line ) - { - return a.line < b.line; - } - const int byRule = std::string_view( a.rule ).compare( b.rule ); - return byRule != 0 ? byRule < 0 : a.sev > b.sev; - } ); - findings.erase( std::unique( findings.begin(), findings.end(), []( const SkillFinding& a, const SkillFinding& b ) noexcept - { return a.line == b.line && std::string_view( a.rule ) == std::string_view( b.rule ); } ), - findings.end() ); + dedupeFindingsKeepWorst( findings ); } inline std::vector scanSkillText( std::string_view text, SkillFileKind kind = SkillFileKind::Markdown ) diff --git a/test/skillscan.sh b/test/skillscan.sh index 2f62a7f84..22670bf1a 100755 --- a/test/skillscan.sh +++ b/test/skillscan.sh @@ -527,6 +527,46 @@ else no "(excerpt cap) the harness did not compile: $( head -3 "$TMP/excerpt.err" )" fi +# ── check 22: the per-line / joined-body dedupe keeps the WORST row of a (line, rule), whatever order they arrive in ── +# scanSkillTextOn's per-line pass and its joined-body pass can report the same (line, rule); the dedupe keeps one. It sorted +# with a non-stable std::sort on (line, rule) and kept the first, so which row survived was whatever the sort left first: +# a WARN could hide a CRITICAL (exit 2 -> 1, and wrap stops refusing) — the same class mergeScriptPasses was fixed for +# (check 20). Every rule's severity is a function of the line today, so no CLI input reaches the collision; the arm drives +# dedupeFindingsKeepWorst directly. 40 pairs, WARN pushed before CRITICAL in half of them and after in the other half, plus +# a lone row each side: past std::sort's insertion-sort cutoff, equal keys do not keep their arrival order. +cat >"$TMP/dedupe.cpp" <<'CPP' +#include "skillscan.h" +#include +int main() +{ + using rw::SkillFinding; using rw::SkillSeverity; + std::vector v; + for( int line = 1; line <= 40; ++line ) + { + const bool warnFirst = ( line % 2 ) != 0; + v.push_back( { warnFirst ? SkillSeverity::Warn : SkillSeverity::Critical, line, "INJECTION:disregard", warnFirst ? "w" : "c" } ); + v.push_back( { warnFirst ? SkillSeverity::Critical : SkillSeverity::Warn, line, "INJECTION:disregard", warnFirst ? "c" : "w" } ); + } + v.push_back( { SkillSeverity::Warn, 41, "SCOPE-CREEP:bash", "only" } ); + v.push_back( { SkillSeverity::Info, 3, "X:y", "other-rule" } ); + rw::dedupeFindingsKeepWorst( v ); + int rows = 0, nonCritical = 0, strayCritical = 0; + for( const SkillFinding& f : v ) + { + ++rows; + if( std::string_view( f.rule ) == "INJECTION:disregard" ) { nonCritical += f.sev != SkillSeverity::Critical; strayCritical += f.excerpt != "c"; } + } + std::printf( "rows=%d non_critical_kept=%d wrong_excerpt=%d\n", rows, nonCritical, strayCritical ); +} +CPP +if "$CXX" -std=c++23 -I "$ROOT/src" -I "$ROOT/src/infra" -I "$ROOT/third_party" "$TMP/dedupe.cpp" "$ROOT/src/infra/diagnostics.cpp" -o "$TMP/dedupe" >"$TMP/dedupe.err" 2>&1; then + GOT="$( "$TMP/dedupe" )" + if [ "$GOT" = "rows=42 non_critical_kept=0 wrong_excerpt=0" ]; then ok "(dedupe) 40 colliding (line, rule) pairs keep the CRITICAL row whichever came first (42 rows: 40 + the two lone ones)" + else no "(dedupe) want 'rows=42 non_critical_kept=0 wrong_excerpt=0', got: $GOT"; fi +else + no "(dedupe) the harness did not compile (no dedupeFindingsKeepWorst?): $( head -3 "$TMP/dedupe.err" )" +fi + # ── summary ─────────────────────────────────────────────────────────────────────────────────────── if [ "$fail" = "0" ]; then echo "ALL PASS" From efd8f60d74eab2e232c4593b4294f8c6bbcfb0bc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:37:30 -0400 Subject: [PATCH 091/229] rubyrecvnarrowcheck: absence arms read only off a run that succeeded `--callers=Radar::ping` (and Motor::run, Tally::add, the mutation's Calc::add) did `grep -q ROW && no || ok` on a capture that is empty when the binary crashes, so a crash passed. Route them through callersLacks, which requires rc 0 and a root first. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 6 ++++++ test/rubyrecvnarrowcheck.sh | 25 ++++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd6b35412..366907c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — test infrastructure: `rubyrecvnarrowcheck` absence arms no longer pass on a crashed run + +The four "`--callers=X` does not list the row" arms read an empty capture, so a binary that crashed or refused passed +them. They now go through one helper that reads the absence only off a run that exited 0 and produced its `` +root, and fails by name otherwise. + ### Fixed — the skill scanner's per-line / joined-body dedupe keeps the worst row of a (line, rule) The scanner's two passes can report the same (line, rule), and the dedupe sorted with a non-stable sort on (line, rule) diff --git a/test/rubyrecvnarrowcheck.sh b/test/rubyrecvnarrowcheck.sh index e23982cd1..a2683760a 100755 --- a/test/rubyrecvnarrowcheck.sh +++ b/test/rubyrecvnarrowcheck.sh @@ -208,6 +208,20 @@ command -v xmllint >/dev/null 2>&1 && { if xmllint --noout "$MAP"; then ok "xmll SPLIT="$DIR/split"; sed 's/>\n"$SPLIT" rowOf(){ awk -v pat="$1" '$0 ~ pat{f=1;print;next} /^ row + its children edgesTo(){ echo "$1" | grep -c " rows of that name +# An ABSENCE arm on a --callers answer: read only off a run that succeeded and produced its root. A crash or a +# refusal leaves the capture empty, and a bare `grep -q ROW && no || ok` reads that as a PASS (checklist 14). +# callersLacks DIR SYMBOL ROW-PATTERN FAIL-MESSAGE PASS-MESSAGE +callersLacks(){ + local out + if ! out="$( "$BIN" "$1" --no-cache --callers="$2" 2>"$DIR/absent.err" )"; then + no "--callers=$2 exited non-zero, so its absence arm proves nothing: $( head -3 "$DIR/absent.err" )"; return + fi + case "$out" in + *' root, so its absence arm proves nothing"; return ;; + esac + if echo "$out" | grep -q "$3"; then no "$4"; else ok "$5"; fi +} echo "=== the fixture parsed the way this gate assumes ===" for want in 'n="add" sc="Calc"' 'n="add" sc="Tally"' 'n="run" sc="Engine"' 'n="run" sc="Motor"' \ @@ -231,7 +245,7 @@ CL="$( "$BIN" "$FIX" --no-cache --callers=Calc::add 2>/dev/null )" echo "$CL" | grep -q 'n="qualified_call"' && ok "--callers=Calc::add lists qualified_call" \ || no "--callers=Calc::add does not list qualified_call: $( echo "$CL" | grep -o ']*' )" CT="$( "$BIN" "$FIX" --no-cache --callers=Tally::add 2>/dev/null )" -echo "$CT" | grep -q 'n="qualified_call"' && no "--callers=Tally::add STILL lists qualified_call — the wrong half of the split survived" || ok "--callers=Tally::add does not list qualified_call" +callersLacks "$FIX" Tally::add 'n="qualified_call"' "--callers=Tally::add STILL lists qualified_call — the wrong half of the split survived" "--callers=Tally::add does not list qualified_call" echo "$CT" | grep -q 'count="0"' && ok "…and reports count=\"0\" (Tally::add has no caller in this tree)" \ || no "--callers=Tally::add did not report count=0: $( echo "$CT" | grep -o ']*' )" @@ -242,8 +256,7 @@ S="$( rowOf 'n="scoped_call" ' )" CE="$( "$BIN" "$FIX" --no-cache --callers=Engine::run 2>/dev/null )" echo "$CE" | grep -q 'n="scoped_call"' && ok "--callers=Engine::run lists scoped_call" \ || no "--callers=Engine::run does not list scoped_call" -CM="$( "$BIN" "$FIX" --no-cache --callers=Motor::run 2>/dev/null )" -echo "$CM" | grep -q 'n="scoped_call"' && no "--callers=Motor::run lists scoped_call — Outer::Engine reached Other::Motor" || ok "--callers=Motor::run does not list scoped_call" +callersLacks "$FIX" Motor::run 'n="scoped_call"' "--callers=Motor::run lists scoped_call — Outer::Engine reached Other::Motor" "--callers=Motor::run does not list scoped_call" A="$( rowOf 'n="absolute_call" ' )" [ "$( edgesTo "$A" ping )" -eq 1 ] && ok "::Top.ping → exactly one edge named ping (the absolute spelling has no scope: child)" \ @@ -251,8 +264,7 @@ A="$( rowOf 'n="absolute_call" ' )" CP="$( "$BIN" "$FIX" --no-cache --callers=Top::ping 2>/dev/null )" echo "$CP" | grep -q 'n="absolute_call"' && ok "--callers=Top::ping lists absolute_call" \ || no "--callers=Top::ping does not list absolute_call" -CR="$( "$BIN" "$FIX" --no-cache --callers=Radar::ping 2>/dev/null )" -echo "$CR" | grep -q 'n="absolute_call"' && no "--callers=Radar::ping lists absolute_call — ::Top reached Radar" || ok "--callers=Radar::ping does not list absolute_call" +callersLacks "$FIX" Radar::ping 'n="absolute_call"' "--callers=Radar::ping lists absolute_call — ::Top reached Radar" "--callers=Radar::ping does not list absolute_call" echo "=== a MODULE is a receiver too (Util.format, and the :: spelling of the same call) ===" M="$( rowOf 'n="module_call" ' )" @@ -327,8 +339,7 @@ MQ="$( "$BIN" "$MUT" --no-cache 2>/dev/null | sed 's/>\n/dev/null )" echo "$MCL" | grep -q 'n="qualified_call"' && ok "mutation: Tally.add → --callers=Tally::add now lists qualified_call" \ || no "mutation: the edge did not follow the receiver: $MQ" -MCC="$( "$BIN" "$MUT" --no-cache --callers=Calc::add 2>/dev/null )" -echo "$MCC" | grep -q 'n="qualified_call"' && no "mutation: --callers=Calc::add still lists qualified_call — the pin is not reading the receiver" || ok "mutation: Calc::add no longer lists it" +callersLacks "$MUT" Calc::add 'n="qualified_call"' "mutation: --callers=Calc::add still lists qualified_call — the pin is not reading the receiver" "mutation: Calc::add no longer lists it" echo [ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } From 9a14723ba7fbf89b615827792fbb5a9a87b8eb59 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:41:25 -0400 Subject: [PATCH 092/229] docs: label --help-task 0.907/0.918/1.000 as tuning-corpus figures; state held-out result The coverage (0.907 test / 0.918 all) and precision (1.000) figures in docs/EVALS.md were measured on the tuning corpus. State the held-out result plainly: 28/214 = 0.131 on held-out positive prompts, below the pre-registered >= 0.60 coverage bar; the held-out gate that registration called for is not yet built. Same label on the route hook's header comment. CHANGELOG Fixed line. 28/214 is the lens-3 audit's figure (2026-10-02, main 40c83e4f; script join_coverage.py in the audit session scratch, not recoverable). Independent re-run with the same argv (--help-task per row of test/skillevalfix/prompts.tsv, --exclude=bench/external) on main 255dc199: 29/214 = 0.136, 0/68 false recommends; the doc quotes the audit's figure with its date. Docs/comment only; gates run: routehookcheck, readmedriftcheck, deckclaimcheck, docdriftcheck, docdriftcommentcheck, docscommandscheck, textdocscheck. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 4 ++++ docs/EVALS.md | 12 +++++++----- hooks/ripwire-claude-route.sh | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..2071be729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — `--help-task` routing coverage figures are labelled as tuning-corpus numbers + +The 0.907 / 0.918 coverage and 1.000 precision figures in `docs/EVALS.md` were measured on the tuning corpus. On the held-out prompts `--help-task` recommended on 28 of 214 positive prompts (0.131, measured 2026-10-02), below the pre-registered ≥ 0.60 coverage bar, and the held-out gate that registration called for is not yet built. Docs only; no behaviour change. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..f495764af 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -1104,7 +1104,7 @@ exists so that the next negative is as publishable as the last one. **The mechanism under test.** `hooks/ripwire-claude-route.sh`, a Claude Code `UserPromptSubmit` hook. On every submitted prompt it asks the deterministic `--help-task` classifier — the same one gated by -`test/taskroutecheck.sh` at precision 1.000 / harmful 0.000 — and, **only** when that classifier +`test/taskroutecheck.sh` at precision 1.000 / harmful 0.000 on the tuning corpus — and, **only** when that classifier returns `status="recommend"`, injects ONE paste-ready command as `additionalContext`. At `status="abstain"` it injects nothing at all. It is the Claude Code port of `hooks/ripwire-codex-route.sh`, which has shipped for Codex since 2026-08-28 and has never seen a @@ -2301,12 +2301,14 @@ verb, so a composed expression the verb would refuse fails the gate rather than | --- | ---: | ---: | | skills the router can name (of 16, `ripwire-router` excluded) | **8** | **16** | | audit's 39 surface phrasings, recommends | **3** | **9** | -| corpus `split=test` (n=114) accuracy / coverage | 0.754 / 0.627 | **0.939 / 0.907** | -| corpus `split=dev` (n=111) accuracy / coverage | — | 0.946 / 0.929 | -| corpus `split=all` (n=225) accuracy / coverage | 0.809 / 0.730 | **0.942 / 0.918** | -| precision / harmful / neg-specificity, every split | 1.000 / 0.000 / 1.000 | 1.000 / 0.000 / 1.000 | +| tuning corpus `split=test` (n=114) accuracy / coverage | 0.754 / 0.627 | **0.939 / 0.907** | +| tuning corpus `split=dev` (n=111) accuracy / coverage | — | 0.946 / 0.929 | +| tuning corpus `split=all` (n=225) accuracy / coverage | 0.809 / 0.730 | **0.942 / 0.918** | +| tuning corpus precision / harmful / neg-specificity, every split | 1.000 / 0.000 / 1.000 | 1.000 / 0.000 / 1.000 | | the 189 rows that predate this tier, (status, intent, resolved_symbols) | — | **0 differing** | +The accuracy, coverage and precision figures in this table (0.907, 0.918, 1.000) were measured on the tuning corpus, `test/taskroutefix/prompts.tsv`, the corpus the intents were written against. On the held-out prompts, `test/skillevalfix/prompts.tsv`, `--help-task` recommended on 28 of 214 positive prompts (28/214 = 0.131, measured 2026-10-02), below the pre-registered coverage bar of ≥ 0.60; the held-out gate that registration called for is not yet built. + The remaining 30 of 39 are declined by design and are gated as such: six SHAPING flags (`--scope`, `--slice-depth`, `--slice-flow`, `--allow-dirty`, `--no-ignore`, `--no-post-check`) are modifiers on other verbs and not commands a one-command router can recommend alone; `--pin-census` is eval-only; and diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index cc315c8a1..7b266d8a2 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -12,7 +12,7 @@ # 1. THE MOMENT. This runs before the agent has chosen a tool, not after it already reached for one. # 2. THE PAYLOAD. A runnable command with its arguments already filled in from the prompt, not a verb # name and an ellipsis the agent has to finish. -# 3. THE GATE. `--help-task` has a measured precision (1.000 / harmful 0.000 on its corpus, +# 3. THE GATE. `--help-task` has a measured precision (1.000 / harmful 0.000 on its tuning corpus, # test/taskroutecheck.sh), so this is silent on the prompts it cannot route rather than firing on # everything. Silence is the common case and is not a failure. # The round cannot separate the three, and the registration says so. From aef1683b14cbdfa0a1cfd9340f078e0bd2f8f208 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 11:55:10 -0400 Subject: [PATCH 093/229] test: the tag shapes and the fixed-buffer census admit the continuation attributes detailcheck/truncvocabcheck: the tag-shape regexes admit next_offset= and next_budget_tokens= beside next= (the asserted counts and the shown/total/capped pair are unchanged). fixedbufsweep: sigsOpenTag's nb[96] gains two count-only formatTo sites (35 B / 42 B worst case); the row and the pinned population move by exactly those two. Co-Authored-By: Claude Opus 5.5 --- test/detailcheck.sh | 2 +- test/fixedbufsweep.sh | 9 +++++++-- test/truncvocabcheck.sh | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/detailcheck.sh b/test/detailcheck.sh index 608e827a0..36371d773 100755 --- a/test/detailcheck.sh +++ b/test/detailcheck.sh @@ -41,7 +41,7 @@ bodies(){ grep -o ' block, so the shape admits the attribute; the count it asserts is unchanged. -sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } +sigblocks(){ grep -oE '' "$1" | wc -l | tr -d ' '; } # ── #1: --detail=0 == no --detail (byte-identical, golden-neutral) ────────────────────────────────────── "$BIN" src --for="$TASK" --no-cache >"$TMP/plain" 2>/dev/null diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index ddf94a920..7468e94a6 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -274,10 +274,12 @@ NUMERIC_ONLY = { # "{}" of one std::uint32_t, ten digits worst case against 15 usable + NUL, no %s and # nothing escaped, so it does not join the string-interpolating population ( "src/serialize.h", "lineAttr" ): 1, - ( "src/serialize.h", "nb" ): 3, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. + ( "src/serialize.h", "nb" ): 5, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. # cut-fix lane A (2026-09-23): +2, sigsOpenTag's nb[96] — ' shown="{}" total="{}" capped="1"' # (31 B literal + two size_t at 20 digits = 71 B worst case, against 95 usable + NUL) and # ' docs_dropped="{}"' (16 B + 20 digits = 36 B). Counts only, no %s, nothing escaped. + # knob-honesty-068 (2026-10-07): +2 in the same nb[96] — ' next_offset="{}"' (15 B + 20 + # digits = 35 B) and ' next_budget_tokens="{}"' (22 B + 20 digits = 42 B). Counts only. ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, ( "src/serialize.h", "rc" ): 1, @@ -594,7 +596,10 @@ if not bad: # 238 -> 239 calls/sites, 104 -> 105 rows). noteCacheReject's new `char detail[192]` names both version # numbers on a refused cache blob; one formatTo of a literal and two std::uint32_t, rowed 'not-markup' # (a stderr notice). Re-derived on the train 20 merged tree; no other train lane moves the population. -EXPECTED = { "mentions": 351, "calls": 241, "sites": 241, "rows": 105, "widthforms": 0 } +# 2026-10-07 (lane/knob-honesty-068): +2 calls/+2 mentions/+2 sites, rows unchanged — sigsOpenTag's existing nb[96] +# formats the cut's resume index (' next_offset="{}"') and the MCP machine continuation (' next_budget_tokens="{}"'): +# one size_t each, no string argument (35 B / 42 B worst case against 95 usable + NUL). +EXPECTED = { "mentions": 353, "calls": 243, "sites": 243, "rows": 105, "widthforms": 0 } # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the diff --git a/test/truncvocabcheck.sh b/test/truncvocabcheck.sh index d2f0f1c1e..503aaf4c8 100755 --- a/test/truncvocabcheck.sh +++ b/test/truncvocabcheck.sh @@ -320,7 +320,7 @@ grep -q '' "$TMP/tiny_for.xml" \ || no "(C) --for on a two-symbol corpus is not the bare untrimmed " # knob-honesty-068: a trimmed tag also carries its continuation, next= (the call that serves it uncut) — the pair is # still asserted immediately after the tag name; only the closing `>` admits the trailing attributes the tag may carry. -grep -qE '' "$TMP/sweep_forbudget.xml" \ +grep -qE '' "$TMP/sweep_forbudget.xml" \ && ok "(C) --for trimmed by --token-budget: (rule 5, amended: the pair rides on a byte trim too)" \ || no "(C) --for --token-budget=800 did not mark its trimmed payload : $( grep -oE ']*>' "$TMP/sweep_forbudget.xml" | head -1 )" From f526358d0e8a07789266512d33e3c817f207eb12 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 12:39:49 -0400 Subject: [PATCH 094/229] Detect a hanging ASan runtime at configure time; ASan-harness gates prefer llvm@22 or SKIP by name On macOS 26.7 with Command Line Tools 26.3 (Apple clang 17.0.0) every -fsanitize=address binary deadlocks in the sanitizer runtime before main, so ASan gates ended as rc=124 timeouts. - CMake: an Apple -DRIPWIRE_ASAN=ON configure builds and runs an empty ASan program under a 10 s timeout and stops with the fix (update CLT/Xcode, or configure with Homebrew llvm@22). A pass is cached; no probe off Apple or without ASan. - scripts/asanprobe.sh: compiler choice (explicit CXX, else llvm@22, else c++) and the same empty-binary probe for gates. - connectcorecheck, radixsimdcheck, dynmapsimdcheck prefer llvm@22; with no working ASan toolchain these, oswin32logiccheck arm (B) and strkerncheck print a named SKIP and exit 0. Assertions are unchanged. - CONTRIBUTING sanitizer section and CHANGELOG updated. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 12 +++++++ CMakeLists.txt | 65 +++++++++++++++++++++++++++++++++---- CONTRIBUTING.md | 5 ++- scripts/asanprobe.sh | 68 +++++++++++++++++++++++++++++++++++++++ test/connectcorecheck.sh | 25 +++++++++++--- test/dynmapsimdcheck.sh | 14 ++++++-- test/oswin32logiccheck.sh | 18 ++++++++--- test/radixsimdcheck.sh | 16 +++++++-- test/strkerncheck.sh | 14 +++++++- 9 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 scripts/asanprobe.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..0d1db5d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -567,6 +567,18 @@ left it behind when a suite timeout killed the gate. `scripts/gatebound.sh` now also arm an `alarm` of their own, and a sanitized harness that will not start is a SKIP, like one that will not link. `test/pargatescheck.sh` arm (O) stops each gate mid-harness (TERM and KILL) and asserts no harness survives. +### Fixed — an ASan runtime that hangs before `main` is reported at configure time, and the ASan-harness gates no longer time out on it + +On macOS 26.7 with Command Line Tools 26.3 (Apple clang 17.0.0), every `-fsanitize=address` binary deadlocks in the +sanitizer runtime's own start-up, so each ASan gate ended as an `rc=124` timeout and an `-DRIPWIRE_ASAN=ON` tree built +binaries that never reached `main`. An Apple `-DRIPWIRE_ASAN=ON` configure now builds and runs an empty ASan program +under a 10 s timeout (`RIPWIRE_ASAN_PROBE_TIMEOUT`) and stops with the two fixes (update the Command Line Tools/Xcode, or +configure with Homebrew `llvm@22`); a pass is cached and prints nothing, and non-ASan or non-Apple configures run no probe. +The gates that compile their own ASan harness (`connectcorecheck`, `radixsimdcheck`, `dynmapsimdcheck`) prefer +`llvm@22`'s `clang++` when installed (an explicit `CXX` wins); with no working ASan toolchain they, and +`oswin32logiccheck` arm (B) and `strkerncheck`, print a named `SKIP` with the reason and exit 0. No assertion changed. +`scripts/asanprobe.sh` holds the compiler choice and the probe. + ### Fixed — C/C++: a body-less `enum X` in a function signature no longer poses as the function's encloser tmux writes `static enum cmd_retval` on one line and the function name on the next. The tags query indexes every named diff --git a/CMakeLists.txt b/CMakeLists.txt index 64b843521..be8c1c439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1015,13 +1015,64 @@ if(WIN32) endif() if(RIPWIRE_ASAN) - # AppleClang 17's ASan runtime hangs in its own start-up on macOS 26 (Darwin 25), before main, so every - # sanitizer run times out. CI's Xcode 26.6 AppleClang 21 is unaffected. A warning, not an error: the - # tree still builds, and CONTRIBUTING's sanitizer section has the Homebrew LLVM 22 recipe. - if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 21 - AND CMAKE_HOST_SYSTEM_VERSION VERSION_GREATER_EQUAL 25) - message(WARNING "RIPWIRE_ASAN: AppleClang ${CMAKE_CXX_COMPILER_VERSION} on macOS 26 is known to hang in " - "ASan start-up before main. Configure this tree with Homebrew LLVM 22 (CONTRIBUTING.md, Sanitizer build).") + # Some macOS toolchains ship an ASan runtime that deadlocks in its own start-up, before main (Apple clang 17.0.0 / + # Command Line Tools 26.3 on macOS 26.7: libSystem's malloc init enters AsanInitFromRtl, whose shadow-memory + # set-up allocates through dyld and re-enters it). Every sanitizer binary and every ASan gate then just hangs, and + # the contributor sees only timeouts. So on Apple, with ASan on, run an empty -fsanitize=address program once, under + # a timeout, and stop with the fix if it does not finish. Nothing here runs without RIPWIRE_ASAN or off Apple, and a + # pass is cached per (compiler, version, host OS) so a reconfigure does not pay for it again and prints nothing new. + if(APPLE AND CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND NOT CMAKE_CROSSCOMPILING + AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(RIPWIRE_ASAN_PROBE_TIMEOUT 10 CACHE STRING "seconds the configure-time ASan start-up probe may take (macOS)") + set(_ripwire_asan_probe_key + "${CMAKE_CXX_COMPILER}|${CMAKE_CXX_COMPILER_VERSION}|${CMAKE_HOST_SYSTEM_VERSION}|${CMAKE_OSX_SYSROOT}|${CMAKE_EXE_LINKER_FLAGS}") + if(NOT "${_ripwire_asan_probe_key}" STREQUAL "${_RIPWIRE_ASAN_PROBE_PASSED}") + set(_ripwire_asan_probe_dir "${CMAKE_BINARY_DIR}/CMakeFiles/ripwire_asan_probe") + file(MAKE_DIRECTORY "${_ripwire_asan_probe_dir}") + file(WRITE "${_ripwire_asan_probe_dir}/probe.cpp" "int main() { return 0; }\n") + set(_ripwire_asan_probe_cmd ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1}) + if(CMAKE_OSX_SYSROOT) + list(APPEND _ripwire_asan_probe_cmd -isysroot "${CMAKE_OSX_SYSROOT}") + endif() + if(CMAKE_OSX_ARCHITECTURES) + foreach(_ripwire_asan_probe_arch IN LISTS CMAKE_OSX_ARCHITECTURES) + list(APPEND _ripwire_asan_probe_cmd -arch "${_ripwire_asan_probe_arch}") + endforeach() + endif() + separate_arguments(_ripwire_asan_probe_ldflags NATIVE_COMMAND "${CMAKE_EXE_LINKER_FLAGS}") + execute_process( + COMMAND ${_ripwire_asan_probe_cmd} ${_ripwire_asan_probe_ldflags} -fsanitize=address + "${_ripwire_asan_probe_dir}/probe.cpp" -o "${_ripwire_asan_probe_dir}/probe" + RESULT_VARIABLE _ripwire_asan_probe_build_rc + OUTPUT_VARIABLE _ripwire_asan_probe_build_out ERROR_VARIABLE _ripwire_asan_probe_build_out) + if(NOT _ripwire_asan_probe_build_rc EQUAL 0) + message(FATAL_ERROR "RIPWIRE_ASAN: ${CMAKE_CXX_COMPILER} could not build an empty -fsanitize=address program " + "(${_ripwire_asan_probe_build_rc}):\n${_ripwire_asan_probe_build_out}\n" + "This toolchain has no usable AddressSanitizer runtime. Install the Command Line Tools/Xcode that ship one, or " + "configure with Homebrew LLVM (CONTRIBUTING.md, Sanitizer build):\n" + " cmake -S . -B asan -DRIPWIRE_ASAN=ON -DCMAKE_C_COMPILER=$(brew --prefix llvm@22)/bin/clang " + "-DCMAKE_CXX_COMPILER=$(brew --prefix llvm@22)/bin/clang++") + endif() + execute_process( + COMMAND ${CMAKE_COMMAND} -E env ASAN_OPTIONS=detect_leaks=0 "${_ripwire_asan_probe_dir}/probe" + TIMEOUT ${RIPWIRE_ASAN_PROBE_TIMEOUT} + RESULT_VARIABLE _ripwire_asan_probe_run_rc + OUTPUT_QUIET ERROR_QUIET) + if(NOT _ripwire_asan_probe_run_rc EQUAL 0) + message(FATAL_ERROR "RIPWIRE_ASAN: an empty program built with ${CMAKE_CXX_COMPILER} -fsanitize=address did not " + "run to completion within ${RIPWIRE_ASAN_PROBE_TIMEOUT} s (${_ripwire_asan_probe_run_rc}). This toolchain's " + "AddressSanitizer runtime deadlocks in its own start-up, before main, on this macOS (seen with Apple clang " + "17.0.0 / Command Line Tools 26.3 on macOS 26.7), so every sanitizer binary and gate would hang. Either " + "update the Command Line Tools / Xcode (softwareupdate, or Xcode's Settings > Components), or configure with " + "Homebrew LLVM, which carries a runtime that starts:\n" + " brew install llvm@22\n" + " cmake --fresh -S . -B asan -DRIPWIRE_ASAN=ON -DCMAKE_C_COMPILER=$(brew --prefix llvm@22)/bin/clang " + "-DCMAKE_CXX_COMPILER=$(brew --prefix llvm@22)/bin/clang++\n" + "(CONTRIBUTING.md, Sanitizer build, has the matching linker flags.) Raise RIPWIRE_ASAN_PROBE_TIMEOUT only if " + "this host is simply very busy.") + endif() + set(_RIPWIRE_ASAN_PROBE_PASSED "${_ripwire_asan_probe_key}" CACHE INTERNAL "ASan start-up probe passed for this toolchain") + endif() endif() set(_ripwire_windows_clangcl_asan OFF) set(_ripwire_no_omit_frame_pointer -fno-omit-frame-pointer) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 316a49837..0038007fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,10 @@ LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./asan/ripwire >/dev/null **macOS 26 with the Command Line Tools' AppleClang 17: ASan hangs before `main`.** Every `-fsanitize=address` binary, even an empty `main`, hangs in ASan's start-up (shadow-memory set-up walking the dyld shared cache), so each ASan gate just times out. CI's Xcode 26.6 AppleClang 21 is -not affected, and CMake warns when it sees the affected pair. Configure the sanitizer tree with +not affected, and an `-DRIPWIRE_ASAN=ON` configure on macOS now runs an empty ASan program once under a +10 s timeout and stops with the fix if it hangs (the two fixes: update the Command Line Tools/Xcode, or use +Homebrew LLVM 22 as below), while the gates that compile their own ASan harness prefer `llvm@22`'s `clang++` +when it is installed (an explicit `CXX` wins) and otherwise SKIP by name. Configure the sanitizer tree with Homebrew LLVM 22 instead, linking its own libc++ so the headers and the dylib are one release (Homebrew clang otherwise links the system libc++ against its libc++ 22 headers): diff --git a/scripts/asanprobe.sh b/scripts/asanprobe.sh new file mode 100644 index 000000000..d71efb042 --- /dev/null +++ b/scripts/asanprobe.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# asanprobe.sh — pick a C++ compiler whose AddressSanitizer runtime can start, for the gates that compile their own +# ASan harness. Sourced, not executed (it lives in scripts/ for the reason scripts/cxxstd.sh does): +# +# . "$ROOT/scripts/asanprobe.sh" # also sources scripts/gatebound.sh +# EXPLICIT_CXX="${CXX:-}" # BEFORE the gate defaults CXX +# CXX="${CXX:-c++}" +# ASAN_CXX="$( ripwire_asan_pick_cxx "$EXPLICIT_CXX" )" +# if ! ripwire_asan_probe "$ASAN_CXX" "$WORK"; then echo " SKIP ... $RIPWIRE_ASAN_PROBE_WHY"; exit 0; fi +# +# WHY. On macOS 26.7 with Command Line Tools 26.3 (Apple clang 17.0.0, clang-1700.6.4.2) EVERY binary built with +# -fsanitize=address deadlocks before main(), even `int main(){return 0;}`: libSystem's malloc init wraps the default +# zone, which enters the ASan runtime's AsanInitFromRtl; its shadow-memory set-up calls get_dyld_hdr -> +# dyld_shared_cache_iterate_text -> _Block_copy -> malloc, which re-enters AsanInitFromRtl while the first init is +# unfinished, and the second entry waits on a lock the first holds. Nothing in ripwire runs yet. A newer Apple +# toolchain (or Homebrew LLVM) carries a runtime that does not allocate on that path. Without this probe such a gate +# only times out (rc=124) and the contributor learns nothing. +# +# TWO STEPS, both here so every ASan-harness gate says the same thing: +# ripwire_asan_pick_cxx an explicit CXX is respected as given; otherwise Homebrew's keg-only llvm@22 clang++ when it +# is installed (its runtime starts on the affected macOS); otherwise c++. +# ripwire_asan_probe builds an empty `int main(){return 0;}` with -fsanitize=address and runs it under a 10 s +# cap (scripts/gatebound.sh). Returns 0 when it ran to completion. Returns 1 and sets +# RIPWIRE_ASAN_PROBE_WHY to a one-line, gate-printable reason when it could not be built or +# did not finish; the gate then SKIPs BY NAME with that reason (checklist 14/17: a named, +# expected missing premise) and never edits its own assertions. +# RIPWIRE_ASAN_PROBE_SEC overrides the 10 s cap (a very busy host); RIPWIRE_GATE_HARNESS_CAP_SEC still wins, as it +# does for every capped harness. + +. "$(dirname "${BASH_SOURCE[0]}")/gatebound.sh" + +RIPWIRE_ASAN_PROBE_WHY="" + +ripwire_asan_llvm22_cxx() # prints the Homebrew llvm@22 clang++ and returns 0 when it exists; returns 1 otherwise +{ + local d + for d in /opt/homebrew/opt/llvm@22 /usr/local/opt/llvm@22; do + if [ -x "$d/bin/clang++" ]; then printf '%s\n' "$d/bin/clang++"; return 0; fi + done + return 1 +} + +ripwire_asan_pick_cxx() # $1 = the caller's explicit CXX (empty when none); prints the compiler to build the harness with +{ + local explicit="${1:-}" l22 + if [ -n "$explicit" ]; then printf '%s\n' "$explicit"; return 0; fi + # Only Darwin has the affected runtime; elsewhere the default compiler is the right one and stays untouched. + if [ "$( uname -s )" = "Darwin" ] && l22="$( ripwire_asan_llvm22_cxx )"; then printf '%s\n' "$l22"; return 0; fi + printf '%s\n' "c++" +} + +ripwire_asan_probe() # $1 = compiler, $2 = writable scratch dir; extra args = extra compile/link flags +{ + local cxx="$1" dir="$2" cap="${RIPWIRE_ASAN_PROBE_SEC:-10}" rc + shift 2 + RIPWIRE_ASAN_PROBE_WHY="" + printf 'int main(){return 0;}\n' > "$dir/asanprobe.cpp" + if ! "$cxx" "$@" -fsanitize=address "$dir/asanprobe.cpp" -o "$dir/asanprobe" >"$dir/asanprobe.log" 2>&1; then + RIPWIRE_ASAN_PROBE_WHY="$cxx cannot build an -fsanitize=address program on this host" + return 1 + fi + ASAN_OPTIONS=detect_leaks=0 gate_bounded "$cap" "$dir/asanprobe" >/dev/null 2>&1; rc=$? + if [ "$rc" -ne 0 ]; then + RIPWIRE_ASAN_PROBE_WHY="an empty -fsanitize=address program built by $cxx did not run to completion within ${cap} s (rc=$rc; the sanitizer runtime hangs before main on this macOS, fixed by a newer Command Line Tools/Xcode or by Homebrew llvm@22)" + return 1 + fi + return 0 +} diff --git a/test/connectcorecheck.sh b/test/connectcorecheck.sh index 9ae1ae4c6..c65f73db4 100755 --- a/test/connectcorecheck.sh +++ b/test/connectcorecheck.sh @@ -20,7 +20,13 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +# The ASan/UBSan pass below builds with ASAN_CXX: an explicit CXX as given, else Homebrew llvm@22 when installed, else c++ +# (scripts/asanprobe.sh: Apple clang 17 / Command Line Tools 26.3 on macOS 26.7 deadlocks every ASan binary before main). +# The plain harness keeps using CXX, so its toolchain is the one the caller asked for. +. "$ROOT/scripts/asanprobe.sh" +EXPLICIT_CXX="${CXX:-}" CXX="${CXX:-c++}" +ASAN_CXX="$( ripwire_asan_pick_cxx "$EXPLICIT_CXX" )" # §CI-P3: ask THIS front end how it spells C++23 rather than assuming the Clang-17 spelling — an # AppleClang 15 (LLVM 16) macos-14 runner rejects `-std=c++23` outright and took this gate with it @@ -28,7 +34,7 @@ CXX="${CXX:-c++}" . "$ROOT/scripts/cxxstd.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" HARNESS="$ROOT/test/connectcore_harness.cpp" -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( mktemp -d )"; trap 'gate_bounded_reap; rm -rf "$WORK"' EXIT; gate_bounded_arm BIN="$WORK/connectcoreharness" echo "connectcorecheck: CXX=$CXX" @@ -51,11 +57,16 @@ fi # ── ASan/UBSan pass: the same harness under the G1 sanitizer stack (integer BFS/MST must be clean) ─────────── ASAN_BIN="$WORK/connectcoreharness_asan" -if "$CXX" "$CXXSTD" -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all \ +ASAN_SKIPPED="" +if ! ripwire_asan_probe "$ASAN_CXX" "$WORK"; then + # A named, expected missing premise (checklist 14/17): a toolchain whose ASan runtime cannot start a program. + echo " SKIP ASan/UBSan pass: $RIPWIRE_ASAN_PROBE_WHY. Set CXX to Homebrew llvm@22's clang++, or update the Command Line Tools/Xcode (CONTRIBUTING.md, Sanitizer build)." + ASAN_SKIPPED=1 +elif "$ASAN_CXX" "$( ripwire_cxx_std_flag "$ASAN_CXX" )" -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all \ -I"$ROOT/src/infra" -I"$ROOT/third_party" -I"$ROOT/src" \ "$HARNESS" "$ROOT/src/infra/diagnostics.cpp" -o "$ASAN_BIN" 2> "$WORK/asan_cc.log"; then - if "$ASAN_BIN" > /dev/null; then - echo " PASS ASan/UBSan run clean" + if gate_bounded 120 "$ASAN_BIN" > /dev/null; then + echo " PASS ASan/UBSan run clean ($ASAN_CXX)" else echo " FAIL ASan/UBSan run failed"; exit 2 fi @@ -63,5 +74,9 @@ else echo " WARN sanitizer build unavailable on this toolchain (skipped)"; sed 's/^/ /' "$WORK/asan_cc.log" fi -echo "connectcorecheck: ALL PASS" +if [ -n "$ASAN_SKIPPED" ]; then + echo "connectcorecheck: PASS (plain harness only; the ASan/UBSan pass was skipped above)" +else + echo "connectcorecheck: ALL PASS" +fi exit 0 diff --git a/test/dynmapsimdcheck.sh b/test/dynmapsimdcheck.sh index 6010b8d24..1bd4a2362 100755 --- a/test/dynmapsimdcheck.sh +++ b/test/dynmapsimdcheck.sh @@ -22,17 +22,27 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" -CXX="${CXX:-c++}" +# The whole gate is an ASan harness: an explicit CXX is respected, else Homebrew llvm@22 when installed, else c++ +# (scripts/asanprobe.sh: Apple clang 17 / Command Line Tools 26.3 on macOS 26.7 deadlocks every ASan binary before main). +. "$ROOT/scripts/asanprobe.sh" +CXX="$( ripwire_asan_pick_cxx "${CXX:-}" )" # ask THIS front end how it spells C++23 (see scripts/cxxstd.sh — AppleClang 15 rejects -std=c++23) . "$ROOT/scripts/cxxstd.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" HARNESS="$ROOT/test/dynmapsimd_harness.cpp" -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( mktemp -d )"; trap 'gate_bounded_reap; rm -rf "$WORK"' EXIT; gate_bounded_arm ARCH="$( uname -m )" echo "dynmapsimdcheck: CXX=$CXX arch=$ARCH" +# A named, expected missing premise (checklist 14/17): a toolchain whose ASan runtime cannot start a program. The harness +# and every assertion below are unchanged; on such a host they cannot run, so the gate says so and stops. +if ! ripwire_asan_probe "$CXX" "$WORK"; then + echo "dynmapsimdcheck: SKIP — $RIPWIRE_ASAN_PROBE_WHY. Use a toolchain whose ASan runtime starts: set CXX to Homebrew llvm@22's clang++, or update the Command Line Tools/Xcode (CONTRIBUTING.md, Sanitizer build)." + exit 0 +fi + # G1's 'integer' / float-cast groups are Clang spellings; GCC only has the address,undefined core. # Probe THIS front end rather than guessing from its name (same posture as scripts/cxxstd.sh). SAN="-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow" diff --git a/test/oswin32logiccheck.sh b/test/oswin32logiccheck.sh index 0551cff24..86bc2eeb7 100755 --- a/test/oswin32logiccheck.sh +++ b/test/oswin32logiccheck.sh @@ -32,9 +32,11 @@ ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write th no(){ printf ' FAIL %s\n' "$*"; fail=1; } . "$ROOT/scripts/cxxstd.sh" +. "$ROOT/scripts/asanprobe.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" SRC="$ROOT/test/verify_os_win32_logic.cpp" -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( mktemp -d )"; trap 'gate_bounded_reap; rm -rf "$WORK"' EXIT; gate_bounded_arm +ASAN_SKIPPED="" MIN_ASSERTIONS=4000000 # the full-range UTF round trip alone is 3.3 M assertions; far below this, something did not run CM_ARGS=() if [ "${OS:-}" = Windows_NT ]; then @@ -75,8 +77,14 @@ SAN="-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-poin if "$CXX" -fsanitize=integer -x c++ -o /dev/null - <<<'int main(){}' >/dev/null 2>&1; then SAN="-fsanitize=address,undefined,integer -fno-sanitize-recover=all -fno-omit-frame-pointer" fi -if "$CXX" "$CXXSTD" -O1 -g $SAN -I"$ROOT/src" -I"$ROOT/third_party/deps/doctest" "$SRC" -o "$WORK/san" > "$WORK/san.log" 2>&1; then - ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$WORK/san" > "$WORK/san.out" 2>&1 +# Deliberately NOT switched to llvm@22 (unlike connectcorecheck): its libc++ 22 trips an -fsanitize=integer report in +# itself (CONTRIBUTING.md, Sanitizer build), which would turn a hang into a red that is not ripwire's. Probe the +# compiler the caller chose; a start-up hang is a named missing premise, so this arm SKIPs and (A) and (C) still gate. +if ! ripwire_asan_probe "$CXX" "$WORK"; then + echo " SKIP (B) sanitizer run: $RIPWIRE_ASAN_PROBE_WHY. Set CXX to a toolchain whose ASan runtime starts, or update the Command Line Tools/Xcode (CONTRIBUTING.md, Sanitizer build)." + ASAN_SKIPPED=1 +elif "$CXX" "$CXXSTD" -O1 -g $SAN -I"$ROOT/src" -I"$ROOT/third_party/deps/doctest" "$SRC" -o "$WORK/san" > "$WORK/san.log" 2>&1; then + ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 gate_bounded 300 "$WORK/san" > "$WORK/san.out" 2>&1 rc=$? read_counts "$WORK/san.out" if [ "$rc" -eq 0 ] && [ "$FAILED" = 0 ] && ! grep -qE 'runtime error|AddressSanitizer' "$WORK/san.out"; then @@ -102,7 +110,9 @@ else no "(C) the mutant build failed to compile"; head -20 "$WORK/mut.log" | sed 's/^/ /' fi -if [ "$fail" = 0 ]; then +if [ "$fail" = 0 ] && [ -n "$ASAN_SKIPPED" ]; then + echo "PASS (arms A and C; arm B was skipped above)" +elif [ "$fail" = 0 ]; then echo "ALL PASS" else echo "SOME CHECKS FAILED" diff --git a/test/radixsimdcheck.sh b/test/radixsimdcheck.sh index 6b5675938..24da1d46f 100755 --- a/test/radixsimdcheck.sh +++ b/test/radixsimdcheck.sh @@ -23,17 +23,27 @@ set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" -CXX="${CXX:-c++}" +# The whole gate is an ASan harness: an explicit CXX is respected, else Homebrew llvm@22 when installed, else c++ +# (scripts/asanprobe.sh: Apple clang 17 / Command Line Tools 26.3 on macOS 26.7 deadlocks every ASan binary before main). +. "$ROOT/scripts/asanprobe.sh" +CXX="$( ripwire_asan_pick_cxx "${CXX:-}" )" # ask THIS front end how it spells C++23 (see scripts/cxxstd.sh — AppleClang 15 rejects -std=c++23) . "$ROOT/scripts/cxxstd.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" HARNESS="$ROOT/test/radixsimd_harness.cpp" -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( mktemp -d )"; trap 'gate_bounded_reap; rm -rf "$WORK"' EXIT; gate_bounded_arm ARCH="$( uname -m )" echo "radixsimdcheck: CXX=$CXX arch=$ARCH" +# A named, expected missing premise (checklist 14/17): a toolchain whose ASan runtime cannot start a program. The harness +# and every assertion below are unchanged; on such a host they cannot run, so the gate says so and stops. +if ! ripwire_asan_probe "$CXX" "$WORK"; then + echo "radixsimdcheck: SKIP — $RIPWIRE_ASAN_PROBE_WHY. Use a toolchain whose ASan runtime starts: set CXX to Homebrew llvm@22's clang++, or update the Command Line Tools/Xcode (CONTRIBUTING.md, Sanitizer build)." + exit 0 +fi + # G1's 'integer' / float-cast groups are Clang spellings; GCC only has the address,undefined core. # Probe THIS front end rather than guessing from its name (same posture as scripts/cxxstd.sh). SAN="-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow" @@ -90,7 +100,7 @@ run_pass baseline "$WANT" if [ "$( uname -s )" = "Darwin" ] && [ "$ARCH" = "arm64" ]; then printf 'int main(){return 0;}\n' > "$WORK/rosetta_probe.cpp" if "$CXX" "$CXXSTD" -arch x86_64 $SAN -fno-sanitize-recover=all "$WORK/rosetta_probe.cpp" -o "$WORK/rosetta_probe" 2>/dev/null \ - && "$WORK/rosetta_probe" 2>/dev/null; then + && gate_bounded 10 "$WORK/rosetta_probe" 2>/dev/null; then run_pass rosetta-x86_64 "SSE" -arch x86_64 else echo " SKIP [rosetta-x86_64] Rosetta or the x86_64 sanitizer runtime is unavailable — SSE2 kernels NOT gated on this machine" diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 2628d90bd..e31b5360e 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -42,9 +42,10 @@ CXX="${CXX:-c++}" # ask THIS front end how it spells C++23 (see scripts/cxxstd.sh — AppleClang 15 rejects -std=c++23) . "$ROOT/scripts/cxxstd.sh" +. "$ROOT/scripts/asanprobe.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" SRC="$ROOT/test/verify_strkern.cpp" -WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +WORK="$( mktemp -d )"; trap 'gate_bounded_reap; rm -rf "$WORK"' EXIT; gate_bounded_arm ARCH="$( uname -m )" fail=0 @@ -57,6 +58,17 @@ MIN_ASSERTIONS=19 echo "strkerncheck: CXX=$CXX arch=$ARCH target=ripwire_test_strkern" +# ── 0: can this toolchain start an ASan program at all? ─────────────────────────────────────────────────── +# The CMake target below is built with RIPWIRE_ASAN=ON, whose configure step refuses (and says why) on a macOS whose +# sanitizer runtime deadlocks before main, and every arm after it reads that target's output. A named, expected missing +# premise (checklist 14/17), so the gate SKIPs here; nothing it asserts changes. Probe-only on purpose: the CMake leg +# takes its compiler from CC/CXX/LDFLAGS (CONTRIBUTING.md, Sanitizer build), so a caller who exports Homebrew llvm@22 +# there gets the real run. +if ! ripwire_asan_probe "$CXX" "$WORK"; then + echo "strkerncheck: SKIP — $RIPWIRE_ASAN_PROBE_WHY. Export CC/CXX/LDFLAGS for Homebrew llvm@22 or update the Command Line Tools/Xcode (CONTRIBUTING.md, Sanitizer build)." + exit 0 +fi + # ── 1: the CMake target, under the complete G1 sanitizer stack ──────────────────────────────────────── # FETCHCONTENT_FULLY_DISCONNECTED=ON because every dependency is vendored: a gate must not reach the # network, and if one ever tries, this is where it fails loudly instead of hanging. From 9f1dee4c090169b5dabe8ef268c489a284a8172e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 12:58:02 -0400 Subject: [PATCH 095/229] fix(for): under a hard ceiling the continuation is terse and paid from an honest ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two measured failures of the charged regime (a tight explicit --token-budget): - with the bracket clauses, the compact dialect landed past the ceiling where the full one fitted (compactlegendcheck P4: 1064 against 962 at --token-budget=1000), because its sig ledger charges less header than it emits and leans on the header rungs — which cannot shed a continuation; - the full dialect over-reserved for clauses it then carried (fornotesbudgetcheck's 800-token pin). Now, charged and pasteable, the continuation is TERSE: next= alone (no next_offset=, no clause — the bundle's own next= reading defines it), and the compact dialect also reserves the sig room its charge was given beyond full's, only on a block the ladder already trims: a capped compact answer serves the rows the full one does (rows(default) ⊇ rows(full) holds as equality) and lands no further over. At the default ceiling, and when the sig side is frozen at the default's, nothing changes: exempt, with next_offset= and the clauses. MCP keeps its machine form and clauses. Gate: forrankordercheck.sh arm 9 (the 1500-token run is terse and within budget). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 35 +++++++++++++++++++++++------------ src/verbs_for.h | 13 +++++++++++-- test/forrankordercheck.sh | 8 ++++---- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index e0fb9f834..099d498c6 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4480,6 +4480,8 @@ struct SigsCutReport std::size_t lastShownRank = 0; // the 1-based candidate rank of the last row printed (rows are a rank PREFIX: the ladder // drops tail-first) — the resume index; next_offset= when rows were dropped bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) + bool terseContinuation = false; // a CHARGED pasteable one under a hard ceiling: next= alone (no next_offset=, no + // clause — the bundle's own next= reading defines it); see SigsCutContinuation }; // ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── @@ -4500,10 +4502,13 @@ struct SigsCutReport // leftover, the tail funded last); est_tokens measures it. // • CHARGED (an explicit ceiling whose sig side is ALL of it: a --token-budget at or under the default sig share, any // explicit one beside --signatures-only, MCP budget_tokens, --json --token-budget): nothing downstream can pay there, and -// an explicit --token-budget is a HARD ceiling (est_tokens <= budget, fornotesbudgetcheck), so the attribute AND its -// legend clause are reserved inside the block like shown=/total=. Cost, measured (knob-honesty-068 report): at the wrap -// recipe's --token-budget=2000 one row on 11 of 20 queries — a row the next= itself serves. Exempting it there instead -// kept every row and pushed est_tokens over the stated ceiling (disclosed by over_ceiling=, but past a hard ceiling). +// an explicit --token-budget is a HARD ceiling (est_tokens <= budget, fornotesbudgetcheck), so the attribute is reserved +// inside the block like shown=/total=, and it rides TERSE there: next= alone — no next_offset=, no bracket clause; the +// bundle's own next= reading defines it ("the follow-up to paste"). Measured why (knob-honesty-068 report): with the +// clauses, the compact dialect — whose sig ledger leans on the header rungs — landed past the ceiling where the full +// one fitted (compactlegendcheck P4), and the rungs cannot shed a clause that is the cut's only disclosure. The e= +// ruling's shape: under a tight ceiling the extras go, the one thing that must stay (here the call) stays and is paid +// for. MCP keeps its clause (next_budget_tokens= has no other reading). // THE RESUME INDEX, next_offset= (present when rows were DROPPED, not only shrunk): the candidate index the cut starts at — // the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no // row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation @@ -4525,6 +4530,8 @@ struct SigsCutContinuation bool json = false; // the --json dialect: the re-run is --json too bool pasteHandle = true; // false on MCP: no pasteable CLI argv (the CLI ranks its own list) — the machine form, // next_budget_tokens=, is what a re-call of the same tool needs + std::size_t ledgerGapBytes = 0; // a CHARGED one also reserves this: the sig room the caller's dialect was given beyond what + // its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes) }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -4630,7 +4637,8 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua { return; } - cut.hasContinuation = true; + cut.hasContinuation = true; + cut.terseContinuation = req->charged && req->pasteHandle; if( req->pasteHandle ) { cut.next = invocation; @@ -4642,7 +4650,7 @@ inline void sigsCutAttachContinuation( SigsCutReport& cut, const SigsCutContinua } inline bool sigsCutHasNextOffset( const SigsCutReport& cut ) noexcept { - return cut.hasContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; + return cut.hasContinuation && !cut.terseContinuation && cut.isCapped && cut.shown < cut.total && cut.lastShownRank > 0; } // the legend clauses a cut report owes, in a fixed order: shrunk-not-dropped, the continuation's (knob-honesty-068), docs_dropped inline std::string sigsCutReportLegend( const SigsCutReport& cut ) @@ -4652,7 +4660,7 @@ inline std::string sigsCutReportLegend( const SigsCutReport& cut ) { notes += kForSigsShrunkNote; } - if( cut.isCapped && !cut.next.empty() ) + if( cut.isCapped && !cut.next.empty() && !cut.terseContinuation ) { notes += kForSigsNextNote; } @@ -4680,8 +4688,8 @@ inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::si cut.docsDropped = docsDropped; return sigsCutReportLegend( cut ); } -// The bytes a CHARGED continuation adds, an upper bound fixed before the plan: the attribute(s) at their widest digits, and -// every clause they may bring. `dialectAttrBytes` is the dialect's own spelling of the value-bearing attribute(s). +// The bytes a CHARGED continuation adds, an upper bound fixed before the plan: the pasteable form is terse (the call alone); +// the MCP machine form keeps its attribute, the resume index at its widest digits, and the clauses that define them. inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, std::size_t valueAttrBytes, std::size_t offsetAttrBytes, bool withClauses ) { @@ -4689,9 +4697,12 @@ inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, s { return 0; } - const std::size_t clauses = withClauses ? ( req->pasteHandle ? kForSigsNextNote.size() : kForSigsNextBudgetNote.size() ) + kForSigsNextOffsetNote.size() - : 0u; - return valueAttrBytes + offsetAttrBytes + clauses; + if( req->pasteHandle ) + { + return valueAttrBytes + req->ledgerGapBytes; // terse: next= alone (sigsCutAttachContinuation) + } + const std::size_t clauses = withClauses ? kForSigsNextBudgetNote.size() + kForSigsNextOffsetNote.size() : 0u; + return valueAttrBytes + offsetAttrBytes + clauses + req->ledgerGapBytes; } // The open tag: `` untrimmed; ` shown= total= capped="1"` when rows were cut or shrunk; ` docs_dropped=` diff --git a/src/verbs_for.h b/src/verbs_for.h index 292cea6ab..3ff6d902c 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2935,6 +2935,7 @@ std::optional runForLens( const MainDispatch& d ) // tokens: 995 vs 816). So the compact charge is capped at the FULL dialect's charge for the same header, computed by the // same rule: rows(default) ⊇ rows(full) by construction, and a default that pays more header than full never gets more // room for rows than full had. + std::size_t compactLedgerGapBytes = 0; // knob-honesty-068: how much MORE sig room this compact header was given than full's if( compactLegendOn ) { ForLensHeaderParts fullParts = headerParts; @@ -2944,7 +2945,8 @@ std::optional runForLens( const MainDispatch& d ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes(); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; - chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + compactLedgerGapBytes = fullCharged - chargedHeaderBytes; // >= 0 by the min above } const std::size_t fixedBytes = chargedHeaderBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" // the auto bundle's SECTION SPLIT — the sig side's claim is capped so an explicit ceiling wider @@ -3008,9 +3010,16 @@ std::optional runForLens( const MainDispatch& d ) // two regimes are argued): CHARGED exactly when an explicit ceiling's sig side is the whole ceiling (nothing // downstream can pay, and the ceiling is hard); exempt at the default and when the sig side is frozen at the // default's share (forSigSideCeiling), so those ranked sets — and forbudgetmonotoncheck's identity — are untouched. + // THE COMPACT DIALECT PAYS FROM FULL'S ROOM WHEN IT CHARGES ONE: its sig ledger charges less header than it emits and + // leans on the header rungs to land inside the ceiling; a continuation's bytes are not a clause those rungs can shed, + // so a charged one also reserves the room the compact charge was given beyond full's (compactLedgerGapBytes). Only a + // block the ladder already trims pays it (the plan reserves only when it fires): a capped compact answer then serves + // the rows the full one does — rows(default) ⊇ rows(full) still holds, as equality — and lands no further over its + // ceiling than full (compactlegendcheck P4). An uncapped answer is untouched. const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; const std::string forRankFlags = forRankShapingFlags( cfg ); - const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false }; + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, + /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below sigsPreRendered = preRender( [ & ]( std::FILE* sm ) diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index ad035f8fd..354c61358 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -418,7 +418,7 @@ esac # next= was the r=1 row's (a body, or the widening page). Fixture: 40 matching C functions with long docs and long # parameter lists, so the DEFAULT ceiling drops rows. The tag must carry next= (+ its legend clause), pasting it must # serve all total= rows uncut in the same rank order, the shown rows must be its prefix. Under a tight explicit (hard) -# ceiling it is CHARGED: still there, and est_tokens stays within budget_tokens. At an explicit ceiling wide enough that +# ceiling it is CHARGED and TERSE: next= alone, and est_tokens stays within budget_tokens. At an explicit ceiling wide enough that # the sig side is frozen at the default's it is EXEMPT: the block is byte-identical to the default's. JSON + MCP. # Negatives: an uncapped tag (arm 8's fixture) carries none; --no-route is echoed. RED on 255dc199 (no next= on ). mkdir -p "$TMP/fxcut" @@ -487,9 +487,9 @@ python3 -c 'import json,sys; a=json.loads(sys.argv[1])["names"]; b=json.loads(sy # tight explicit ceiling: CHARGED — still named, and est_tokens stays within the hard ceiling the root names C3="$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=1500 --no-cache 2>/dev/null | cut_arm )" NX3="$( cfield "$C3" next )" -{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] \ - && [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ]; } \ - && ok "(9) --token-budget=1500: next= present and charged (est_tokens $( cfield "$C3" est_tokens ) <= budget_tokens 1500)" \ +{ [ "$( cfield "$C3" capped )" = true ] && [ -n "$NX3" ] && [ "$( cfield "$C3" over )" = false ] && [ "$( cfield "$C3" next_offset )" = null ] \ + && [ "$( cfield "$C3" clause )" = false ] && [ "$( cfield "$C3" est_tokens )" -le "$( cfield "$C3" budget_tokens )" ]; } \ + && ok "(9) --token-budget=1500: charged and terse — next= alone (no next_offset=, no clause), est_tokens $( cfield "$C3" est_tokens ) <= budget_tokens 1500" \ || no "(9) --token-budget=1500: $C3" # wide explicit ceiling (sig side frozen at the default's): EXEMPT — the block, next= included, is the default's byte for byte sigsblk(){ python3 -c 'import re,sys; m=re.search(r"]*>.*?",sys.stdin.read(),re.S); print(m.group(0) if m else "")'; } From d85013698d81a5d4a5145caa693637986f64a4e7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:07:16 -0400 Subject: [PATCH 096/229] docs(changelog): the hard-ceiling continuation is terse and paid from the full row budget Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a90d55ed3..983600748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,10 +27,11 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal (the last printed row's rank, so a slot that prints no row cannot skew it). `--json` carries `"sigs_next"` and `"sigs_next_offset"`. The MCP twin carries no CLI argv (its ranking pipeline is its own): `next_budget_tokens=T` names the `budget_tokens` a re-call needs, beside the same `next_offset=`. Present-only legend clauses define each. At the - default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, it is exempt - from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) it is - charged like `shown=`/`total=`, so `est_tokens` stays within the budget; measured at `--token-budget=2000`, that costs one - row on 11 of 20 queries — a row the `next=` itself serves. No budget, ceiling or token conversion changed. + default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, they are + exempt from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) + `next=` rides alone and is charged inside the block, and a compact answer pays from the full dialect's row budget, so + `est_tokens` stays within the budget; measured at `--token-budget=2000` on 20 queries, that costs one row on 11 — a row + the `next=` itself serves. No budget, ceiling or token conversion changed. - **`--lint` / `--lint-rules` and `--sarif`.** A rule that spends its per-rule match budget (`count_capped="1"`) now makes the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`): only the floored rules, under a 10x budget, through the new `--lint-max-per-rule=N` (the default stays 5000). A re-run still floored names its own. From 75f0e0f8a8ae60e99473168dae1eb40042baa860 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:41:32 -0400 Subject: [PATCH 097/229] =?UTF-8?q?test(countfloorcheck):=20R=20=E2=80=94?= =?UTF-8?q?=20a=20recursive=20function's=20self-call=20is=20not=20evidence?= =?UTF-8?q?=20of=20a=20miss=20(red)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The graph drops self-loops, so a recursive call commits no edge, and the unbound-call scan read it as a call that bound nowhere: a static recursive C function with no other caller printed callers_floor beside dead_code_candidate="1". Negatives: C static recursion (safe-delete and grep ), a Python method recursing through self.walk() and a recursive module function, and bound mutual recursion. Near miss: a recursive TS method with an outside same-named call bound nowhere must still floor. Co-Authored-By: Claude Opus 5.5 --- test/countfloorcheck.sh | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index 3b1369759..b1a1085e0 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -21,6 +21,8 @@ # and the clue LEADS to the uses (the grep names the file that declares a `struct cell`). # B (negative) a static C function nobody calls: a provably complete zero stays callers="0" risk="none-found", # dead_code_candidate="1", no floor marker, no next=. +# R (negatives) recursion: a self-call is bound to the definition itself (C static fn, Python self.m() and module fn, +# mutual recursion); (near miss) a recursive TS method with an outside same-named call bound nowhere still floors. # C CALLSITE-LINE: --safe-delete=width_of rows carry sites_l= (the call lines) beside p= (the definition line): # a caller with three calls on three lines lists all three; one call split over two lines lists its line. # D (near-miss) a single-definition symbol called once still prints its call site (sites_l= present, one line). @@ -183,6 +185,35 @@ R_B="$( root_tag "$( run c --safe-delete=helper_dead )" safe-delete )" && ok "(B) callers=\"0\" risk=\"none-found\" dead_code_candidate=\"1\" (unchanged)" || no "(B) the complete zero changed: $R_B" case "$R_B" in *callers_floor=*|*uses_floor=*|*' next='*) no "(B) a provably complete zero carries a floor marker: $R_B" ;; *) ok "(B) no floor marker, no next= on a complete zero" ;; esac +# ── R: recursion — a self-call bound to the definition itself is not evidence of a miss ───────────────── +# graph.h drops self-loops (CallDisposition::Self), so a recursive call commits no edge: the unbound scan must still +# count it as bound (it bound to this very definition). Negatives: a static recursive C function with no other caller, +# a Python method recursing through self.m() and a recursive module function, mutual recursion (both edges bound). +# Near miss: a recursive TS method that ALSO has a same-named call bound nowhere still floors. +echo "=== R: a recursive function's own call keeps a plain count; an unbound outside call still floors ===" +mkdir -p "$W/crec" "$W/pyrec" "$W/tsrec" +printf 'static int fact(int n) { return n ? n * fact(n - 1) : 1; }\nstatic int pong(int n);\nstatic int ping(int n) { return n ? pong(n - 1) : 0; }\nstatic int pong(int n) { return n ? ping(n - 1) : 1; }\nint entry(void) { return ping(3); }\n' > "$W/crec/r.c" +printf 'class Walker:\n def walk(self, n):\n return self.walk(n - 1) if n else 0\n\ndef walk_tree(n):\n return walk_tree(n - 1) if n else 0\n' > "$W/pyrec/w.py" +printf 'export class Tree { walkDown(n: number): number { return n ? this.walkDown(n - 1) : 0 } }\nexport function outside() { return walkDown(3) }\n' > "$W/tsrec/t.ts" +R_R1="$( root_tag "$( run crec --safe-delete=fact )" safe-delete )" +[ "$( attr "$R_R1" callers )" = 0 ] && [ "$( attr "$R_R1" dead_code_candidate )" = 1 ] || no "(R) premise: fact: $R_R1" +case "$R_R1" in *callers_floor=*|*' next='*) no "(R) a static recursive C function with no caller is floored: $R_R1" ;; *) ok "(R) --safe-delete=fact (self-call only): no callers_floor, no next=" ;; esac +ROW_R1="$( enc_row "$( run crec --grep='n - 1) : 1' )" fact )" +[ -n "$ROW_R1" ] || no "(R) premise: no row for fact" +case "$ROW_R1" in *callers_floor=*) no "(R) grep fact (self-call only) is floored: $ROW_R1" ;; *) ok "(R) grep stays a plain zero" ;; esac +OUT_R2="$( run pyrec --grep='if n else 0' )" +ROW_R2="$( enc_row "$OUT_R2" 'Walker::walk' )"; ROW_R3="$( enc_row "$OUT_R2" walk_tree )" +[ -n "$ROW_R2" ] && [ -n "$ROW_R3" ] || no "(R) premise: Python rows: [$ROW_R2] [$ROW_R3]" +case "$ROW_R2" in *callers_floor=*) no "(R) Python self.walk() recursion is floored: $ROW_R2" ;; *) ok "(R) Python method recursing via self.walk(): no callers_floor" ;; esac +case "$ROW_R3" in *callers_floor=*) no "(R) Python recursive module function is floored: $ROW_R3" ;; *) ok "(R) Python recursive module function walk_tree: no callers_floor" ;; esac +R_R3="$( root_tag "$( run pyrec --safe-delete=walk_tree )" safe-delete )" +case "$R_R3" in *callers_floor=*|*' next='*) no "(R) --safe-delete=walk_tree is floored: $R_R3" ;; *) ok "(R) --safe-delete=walk_tree: no callers_floor, no next=" ;; esac +OUT_RM="$( run crec --grep='n ? p' )"; ROW_PING="$( enc_row "$OUT_RM" pong )"; ROW_PONG="$( enc_row "$OUT_RM" ping )" +[ "$( attr "$ROW_PING" callers )" = 1 ] && [ "$( attr "$ROW_PONG" callers )" = 2 ] || no "(R) premise: mutual recursion counts: [$ROW_PING] [$ROW_PONG]" +case "$ROW_PING$ROW_PONG" in *callers_floor=*) no "(R) bound mutual recursion is floored: $ROW_PING $ROW_PONG" ;; *) ok "(R) mutual recursion ping<->pong (both bound): no callers_floor" ;; esac +ROW_R4="$( enc_row "$( run tsrec --grep='this.walkDown' )" walkDown )" +if [ "$( attr "$ROW_R4" callers_floor )" = 1 ]; then ok "(R) near miss: recursive walkDown with an outside call bound nowhere still floors"; else no "(R) near miss lost its floor: $ROW_R4"; fi + # ── C/D: the call-site line ────────────────────────────────────────────────────────────────────────────── echo "=== C/D: --safe-delete caller rows carry the CALL lines beside the definition line ===" OUT_C="$( run c --safe-delete=width_of )" From d974bce6e8f5feea26364460b417e57b62665b97 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:50:45 -0400 Subject: [PATCH 098/229] fix(count-floor): a recursive call is bound to the definition itself, not evidence of a miss The graph drops self-loops, so a recursive call commits no edge, and callerFloors' unbound-call scan read it as a call spelled like the row that bound nowhere: every recursive function printed callers_floor="1" and a next=, including a static C function nobody else calls (beside dead_code_candidate="1"). Each definition of the name now counts among its own bound callers. By name, like the rest of the scan. Co-Authored-By: Claude Opus 5.5 --- src/countfloor.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/countfloor.h b/src/countfloor.h index 273a616be..6bda0bf56 100644 --- a/src/countfloor.h +++ b/src/countfloor.h @@ -268,11 +268,17 @@ inline std::vector callerFloors( const IngestResult& ing, const Gra if( !pending.empty() ) { // A call from a symbol with an edge into ANY definition of the name was bound somewhere — to this row or to a - // same-named sibling — so it is not evidence of a miss. + // same-named sibling — so it is not evidence of a miss. Nor is a call from a definition of the name ITSELF: the + // graph drops self-loops (CallDisposition::Self), so recursion commits no edge, yet the call bound to this very + // definition. By name, like the rest: a definition calling a same-named function elsewhere is read as recursion. const auto* inRo = g.inEdges.rowOffsets(); const auto* inCi = g.inEdges.colIndices(); const countfloor::IdsOfName boundCallers = countfloor::relatedByName( ing, rowsOfName, g.wOutDeg.size(), - [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), inCi + inRo[id], inCi + inRo[id + 1] ); } ); + [ & ]( NodeId id, std::vector& into ) + { + into.push_back( id ); + into.insert( into.end(), inCi + inRo[id], inCi + inRo[id + 1] ); + } ); countfloor::markUnboundSpellings( ing, rowsOfName, boundCallers, countfloor::isCallSpelling, countfloor::fromIsBound, [ & ]( std::uint32_t row ) { out[row].isFloor = true; } ); From 36a9dfdde394d8ea13eb0fd754b1bd2a5951b182 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:50:46 -0400 Subject: [PATCH 099/229] =?UTF-8?q?docs(changelog):=20count-floor=20?= =?UTF-8?q?=E2=80=94=20recursion=20counts=20as=20bound;=20MCP=20for=20now?= =?UTF-8?q?=20serves=20the=20lego=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f867267..8162944cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,14 +29,16 @@ implementor rows it LISTED after narrowing to its own files (`implementors="1"` bound to no definition, a use as a value, or a kind used by reading or naming it (a variable, class, struct, interface). `floor_next=` (rows) / `next=` (`--safe-delete`) is the call that lists the rest — the uses verb, or the literal grep where the language's reads are never indexed. A count with no such evidence stays a plain total: a - static function nobody calls still reads `callers="0" risk="none-found" dead_code_candidate="1"`. + static function nobody calls still reads `callers="0" risk="none-found" dead_code_candidate="1"`, and a recursive + function's call to itself counts as bound (the graph records no self-edge). - **`--safe-delete`.** `uses_floor="1"` when the definition's uses are reads or type mentions this run did not index, and `risk="unmodelled"` (new value) when nothing was found for such a kind — never `none-found`. Each caller row carries `sites_l=`, the ascending call-site lines spelled like the symbol (the edit-check verb's own pass), beside `p=`, which stays the line where the caller is defined. - **`--for` ``.** `implementors=` is now the lego verb's own count; a shorter list adds `implementors_shown=` and `implementors_next="--lego=FILE:NAME"`. The MCP `for` twin now narrows its `` block with the root prefix the - CLI passes; before, it compared root-relative rows against absolute paths and served no `` at all. + CLI passes; before, it compared root-relative rows against absolute paths and served no `` at all, so MCP + `for` answers on a tree with interfaces now carry that block (up to 12 rows), as the CLI's always did. - Every new attribute is defined present-only in the answer that carries it (compact and full dialects). Answers without one are byte-identical (34 argv across three corpora). Gate: `countfloorcheck`. From 2ea8929c95420492a0745d33df8e1296fe1fa84f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:52:20 -0400 Subject: [PATCH 100/229] serialize: packSignatures takes its row spelling as a named SigRowSpelling, not a trailing bool A trailing defaulted bool would let a parameter another change appends after it bind positionally without a compile error; a bool argument does not convert to the SigRowSpelling aggregate, so a mis-bound call fails to compile instead. Output is unchanged (callers pass SigRowSpelling{ .elideZeroMetrics = true } where they passed true). Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 2 +- src/serialize.h | 13 ++++++++++--- src/verbs_for.h | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 3a8723f8b..d58654d86 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2227,7 +2227,7 @@ inline std::optional forTaskText( const std::string& root, const st &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below mcpTopRowNext, // L-W: the widening page on a thin answer, else the body &mcpSigsCut, // cut-fix lane A: docs_dropped= / shrunk readings - /*elideZeroMetrics=*/true ); // lean-answers lane: the CLI twin's row spelling + SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: the CLI twin's row spelling } ); // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare diff --git a/src/serialize.h b/src/serialize.h index 96bbdcaa3..77966fb9b 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4630,6 +4630,13 @@ inline void pushShownSigId( std::vector* shownIdsOut, const std::vector< } } +// lean-answers lane: packSignatures' row spelling, passed as a NAMED type rather than a trailing bool, so a parameter +// another change appends after it cannot bind to it positionally (a bool argument does not convert to this aggregate). +struct SigRowSpelling +{ + bool elideZeroMetrics = false; // omit a zero cx=/ccx=/in= on a row (SigRowFacts::elideZero); the legend says absent = 0 +}; + inline void packSignatures( std::FILE* out, const IngestResult& ing, const std::vector& rank, int topN, std::size_t budgetBytes, bool metrics = false, const std::vector* fanIn = nullptr, @@ -4678,8 +4685,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: SigsCutReport* cutOut = nullptr, // cut-fix lane A: the tag's shown/total/docs_dropped/capped, // for the caller's legend splices. Lens path only; zeroed // (nothing cut) on every other path. - bool elideZeroMetrics = false ) // lean-answers lane: the --for lens's zero cx=/ccx=/in= omitted - // (SigRowFacts::elideZero); its legend says absent = 0. Off ⇒ + SigRowSpelling spelling = {} ) // lean-answers lane: the --for lens's zero cx=/ccx=/in= omitted + // (SigRowFacts::elideZero); its legend says absent = 0. Default ⇒ // byte-identical (pack-task, from-trace and the map stay as they were). { if( cutOut ) @@ -4902,7 +4909,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } } - const SigRowFacts rowFacts{ metrics, fanIn, qbuf, pure, globalRank, topRowNext, elideZeroMetrics }; + const SigRowFacts rowFacts{ metrics, fanIn, qbuf, pure, globalRank, topRowNext, spelling.elideZeroMetrics }; std::string head = sigRowHead( ing, id, rowFacts, esc, rootArg ); // d1: rank fact (ladder path) std::string doc = docCommentBefore( src, a ); diff --git a/src/verbs_for.h b/src/verbs_for.h index 5b6aabd74..610c266fc 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2998,7 +2998,7 @@ std::optional runForLens( const MainDispatch& d ) &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then forTopRowNext, // L-W: the widening page on a thin answer, else the body &forSigsCut, // cut-fix lane A: which cut readings the tag owes - /*elideZeroMetrics=*/true ); // lean-answers lane: zero cx=/ccx=/in= omitted (legend: absent = 0) + SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: zero cx=/ccx=/in= omitted (legend: absent = 0) }, sigsStr ); if( !sigsPreRendered ) @@ -3481,7 +3481,7 @@ std::optional runForLens( const MainDispatch& d ) &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, /*hasRelevanceFloor=*/true, nullptr, nullptr, nullptr, // LB-A: the direct-emission degrade path selects identically forTopRowNext, // L-W: same next= rule on the degrade path - nullptr, /*elideZeroMetrics=*/true ); // lean-answers lane: the same row spelling + nullptr, SigRowSpelling{ .elideZeroMetrics = true } ); // lean-answers lane: the same row spelling } if( legoPreRendered ) { From a0b8e511969966204e8f3dc49a40cd599369337d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:57:10 -0400 Subject: [PATCH 101/229] refactor(for): the continuation request rides SigsCutReport, not a new trailing parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit packSignatures / packSignaturesJson took the request as one more defaulted trailing parameter. Another lane in the same train adds a defaulted trailing parameter to packSignatures too, and two positional defaults on a 20+-parameter function can silently bind a value to the wrong one after a merge (a pointer converts to bool). The request is now SigsCutReport::continuationRequest, an INPUT the packers read before resetting the report; the signatures are back to origin/main's. Output bytes unchanged. Also: the MCP machine form's stated reason is that an MCP client continues by re-calling the tool (not that the CLI ranks a different list — measured identical). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- src/mcpverbs.h | 11 ++++++----- src/serialize.h | 33 +++++++++++++++++++++------------ src/verbs_for.h | 14 +++++++++----- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 983600748..7dcb20fd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal whole ceiling, sized from the untrimmed block so nothing is cut (`--for=TASK --signatures-only [ranking flags] [--pack-top-n=N] --token-budget=T`) — and, when rows were dropped, `next_offset=`: the candidate index the cut starts at (the last printed row's rank, so a slot that prints no row cannot skew it). `--json` carries `"sigs_next"` and - `"sigs_next_offset"`. The MCP twin carries no CLI argv (its ranking pipeline is its own): `next_budget_tokens=T` names + `"sigs_next_offset"`. The MCP twin carries no CLI argv (an MCP client re-calls the tool): `next_budget_tokens=T` names the `budget_tokens` a re-call needs, beside the same `next_offset=`. Present-only legend clauses define each. At the default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, they are exempt from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 3d420d0fa..d58d38cb3 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2186,9 +2186,9 @@ inline std::optional forTaskText( const std::string& root, const st + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, - // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO pasteable CLI argv here - // (the CLI ranks its own list, so a CLI re-run could continue a different one): the machine form next_budget_tokens=T — - // re-call `for` with budget_tokens=T — plus next_offset=, the resume index. + // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO CLI argv here — an + // MCP client continues by re-calling the tool: the machine form next_budget_tokens=T (re-call `for` with budget_tokens=T) + // plus next_offset=, the resume index. const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false }; @@ -2204,6 +2204,7 @@ inline std::optional forTaskText( const std::string& root, const st std::size_t mcpDroppedPositive = 0; bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does + mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) { @@ -2218,8 +2219,8 @@ inline std::optional forTaskText( const std::string& root, const st &mcpShownIds, // lane 2: see verbs_for.h shownSigIds &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below mcpTopRowNext, // L-W: the widening page on a thin answer, else the body - &mcpSigsCut, // cut-fix lane A: docs_dropped= / shrunk readings - &mcpSigsNext ); // knob-honesty-068: a capped block's own next= + &mcpSigsCut ); // cut-fix lane A: docs_dropped= / shrunk readings; + // knob-honesty-068: carries mcpSigsNext IN } ); // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare diff --git a/src/serialize.h b/src/serialize.h index 099d498c6..32f3e01ad 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4467,8 +4467,13 @@ inline std::size_t gateSigRowsRankFirst( std::vector& entries, std::vect // What the block cut, for the callers that splice its legend clauses (the block's own tag carries the numbers). // shown/total are the tag's shown=/total= (total = rows handed to the gate); docsDropped counts SHOWN rows whose doc // comment the rank tiers or the ladder removed (the tag's docs_dropped=). isCapped mirrors the tag's capped="1". +struct SigsCutContinuation; // below: the caller's request for a cut's continuation (knob-honesty-068) struct SigsCutReport { + // IN (knob-honesty-068): the caller's continuation request, read by packSignatures / packSignaturesJson BEFORE they reset + // the report — carried here rather than as one more defaulted trailing parameter, so a train merge that adds another + // defaulted parameter to these 20+-parameter signatures cannot silently bind a value to the wrong one. nullptr ⇒ none. + const SigsCutContinuation* continuationRequest = nullptr; std::size_t shown = 0; std::size_t total = 0; std::size_t docsDropped = 0; @@ -4513,8 +4518,8 @@ struct SigsCutReport // the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no // row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation // whose at= differs was served by a different index (re-run from the start). -// MCP: no pasteable argv (the CLI's ranking pipeline is not the twin's, so a CLI re-run could continue a different list) — -// the tag carries the machine form instead: next_budget_tokens=T, the budget_tokens a re-call of the same tool needs. +// MCP: the client continues by re-calling the TOOL, so the continuation is that tool's own argument, not a CLI argv — the +// tag carries the machine form: next_budget_tokens=T, the budget_tokens a re-call of the same tool needs. // COMPATIBLE WITH #362 (the budgeted-bundle candidate page, issue #294): when that page lands, the resumable form is // `--for=TASK --token-budget=N --limit=L --offset=`, which serves ONLY the cut rows (walking pages until // has_more="0"); it replaces the VALUE of next= built here, not the attributes — next_offset= is already that page's @@ -4528,8 +4533,8 @@ struct SigsCutContinuation std::size_t fixedBytes = 0; // what the re-run's ceiling must carry besides the block: the charged header + sibling blocks bool charged = false; // reserve it (and its legend clause) INSIDE the block's budget — see the regime rule above bool json = false; // the --json dialect: the re-run is --json too - bool pasteHandle = true; // false on MCP: no pasteable CLI argv (the CLI ranks its own list) — the machine form, - // next_budget_tokens=, is what a re-call of the same tool needs + bool pasteHandle = true; // false on MCP: the client re-calls the tool, so no CLI argv — the machine form, + // next_budget_tokens=, is the argument a re-call of the same tool needs std::size_t ledgerGapBytes = 0; // a CHARGED one also reserves this: the sig room the caller's dialect was given beyond what // its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes) }; @@ -4813,15 +4818,16 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: std::string_view topRowNext = {}, // L-W (forpage.h): the r=1 row's next= when the caller // judged the answer THIN (the widening page); "" ⇒ the // --expand body follow-up, byte-identical to before. - SigsCutReport* cutOut = nullptr, // cut-fix lane A: the tag's shown/total/docs_dropped/capped, + SigsCutReport* cutOut = nullptr ) // cut-fix lane A: the tag's shown/total/docs_dropped/capped, // for the caller's legend splices. Lens path only; zeroed - // (nothing cut) on every other path. - const SigsCutContinuation* cutNext = nullptr ) // knob-honesty-068: a capped block's next= (the - // --for lens and its MCP twin); nullptr ⇒ none, byte-identical + // (nothing cut) on every other path. knob-honesty-068: its + // continuationRequest is an INPUT, read before the reset. { + const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; if( cutOut ) { *cutOut = SigsCutReport {}; + cutOut->continuationRequest = cutNext; } if( droppedPositiveOut ) { @@ -5160,6 +5166,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: *cappedOut = plan.ladderFires; } SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + cut.continuationRequest = cutNext; sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -9437,12 +9444,12 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s std::size_t* droppedPositiveOut = nullptr, // A2: the XML sibling's own out-param — // see packSignatures for the full contract. std::vector* shownIdsOut = nullptr, // lane 2: the emitted rows' ids — see packSignatures - SigsCutReport* cutOut = nullptr, // cut-fix lane A: the XML tag's shown/total/docs_dropped/ + SigsCutReport* cutOut = nullptr ) // cut-fix lane A: the XML tag's shown/total/docs_dropped/ // capped, for the caller's root keys (sigs_shown/ - // sigs_total/docs_dropped); see packSignatures - const SigsCutContinuation* cutNext = nullptr ) // knob-honesty-068: the XML twin's next=, as the - // caller's "sigs_next" root key; see packSignatures + // sigs_total/docs_dropped); see packSignatures. + // knob-honesty-068: continuationRequest is an INPUT { + const SigsCutContinuation* const cutNext = cutOut != nullptr ? cutOut->continuationRequest : nullptr; const bool rankAdaptivePayload = lens.rankAdaptivePayload; if( outCapped ) { @@ -9451,6 +9458,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s if( cutOut ) { *cutOut = SigsCutReport {}; + cutOut->continuationRequest = cutNext; // knob-honesty-068: the request survives the reset } if( droppedPositiveOut ) { @@ -9552,6 +9560,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s if( cutOut ) { *cutOut = sigsCutReportOf( entries, totalRows, plan ); + cutOut->continuationRequest = cutNext; sigsCutAttachContinuation( *cutOut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for } if( droppedPositiveOut ) diff --git a/src/verbs_for.h b/src/verbs_for.h index 3ff6d902c..ad271ba1d 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1468,8 +1468,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For in.rootArg, /*hasRelevanceFloor=*/true, // LB-A: same admission rule as the XML twin (R-R: root-relative p/id) outDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) outShownIds, // lane 2: the emitted rows' ids — the tail excludes these files - outCut, // cut-fix lane A: the XML tag's shown/total/docs_dropped - &cutNext ); }; // knob-honesty-068: "sigs_next" on a capped array + outCut ); }; // cut-fix lane A: the XML tag's shown/total/docs_dropped; + // knob-honesty-068: carries cutNext IN ("sigs_next") // §B1.4: built once, used on both the degrade path below and the normal return — these three are plain // size_t values already computed by the caller (no rendering, no redaction seam), so unlike est_tokens @@ -1496,6 +1496,7 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For bool sigsCapped = false; // the LADDER's verdict: decides the budget_bytes= stanza rw::SigsCutReport sigsCut; // cut-fix lane A: what the array cut (gate or ladder) — "capped" and the sigs_* keys + sigsCut.continuationRequest = &cutNext; // knob-honesty-068: IN — "sigs_next" / "sigs_next_offset" on a capped array std::size_t sigsDroppedPositive = 0; // A2: set only by the memstream-buffered render below (nullptr on the ENOMEM degrade path) std::vector jsonShownIds; // lane 2: the sigs rows actually emitted (the XML twin's shownSigIds) std::string sigsJson; @@ -3022,6 +3023,7 @@ std::optional runForLens( const MainDispatch& d ) /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below + forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation sigsPreRendered = preRender( [ & ]( std::FILE* sm ) { packSignatures( sm, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, @@ -3035,8 +3037,8 @@ std::optional runForLens( const MainDispatch& d ) &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then forTopRowNext, // L-W: the widening page on a thin answer, else the body - &forSigsCut, // cut-fix lane A: which cut readings the tag owes - &forSigsNext ); // knob-honesty-068: a capped block's own next= + &forSigsCut ); // cut-fix lane A: which cut readings the tag owes; + // knob-honesty-068: carries forSigsNext IN }, sigsStr ); if( !sigsPreRendered ) @@ -3515,11 +3517,13 @@ std::optional runForLens( const MainDispatch& d ) } else { + rw::SigsCutReport degradeSigsCut; // knob-honesty-068: carries the continuation request IN, as on the buffered path + degradeSigsCut.continuationRequest = &forSigsNext; packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, /*hasRelevanceFloor=*/true, nullptr, nullptr, nullptr, // LB-A: the direct-emission degrade path selects identically forTopRowNext, // L-W: same next= rule on the degrade path - nullptr, &forSigsNext ); // knob-honesty-068: and the same + °radeSigsCut ); // knob-honesty-068: and the same } if( legoPreRendered ) { From e8cf79a80f6726528d04d9da3facd7a472549722 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 14:16:13 -0400 Subject: [PATCH 102/229] WIP: a charged continuation is paid from the rows, byte for byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the plan-side reservation (which the ladder's row-granularity slack could absorb, letting the bundle grow) with a second ladder pass that sheds exactly the continuation's bytes from the trimmed block. Drops the compact ledger-gap reservation. Open (resume notes in the lane report): compactlegendcheck P4 at --token-budget=850 (compact 892 vs full 844 — the gap reservation fixed it, this version does not); estchargecheck #11 A7 (5446 B vs the 5428 B allowance: the block is at its 4-row floor, so nothing can pay the 81 B). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 21 +++++++++++++++------ src/verbs_for.h | 15 ++++----------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 32f3e01ad..8cd40748c 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4535,8 +4535,6 @@ struct SigsCutContinuation bool json = false; // the --json dialect: the re-run is --json too bool pasteHandle = true; // false on MCP: the client re-calls the tool, so no CLI argv — the machine form, // next_budget_tokens=, is the argument a re-call of the same tool needs - std::size_t ledgerGapBytes = 0; // a CHARGED one also reserves this: the sig room the caller's dialect was given beyond what - // its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes) }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -4704,10 +4702,10 @@ inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, s } if( req->pasteHandle ) { - return valueAttrBytes + req->ledgerGapBytes; // terse: next= alone (sigsCutAttachContinuation) + return valueAttrBytes; // terse: next= alone (sigsCutAttachContinuation) } const std::size_t clauses = withClauses ? kForSigsNextBudgetNote.size() + kForSigsNextOffsetNote.size() : 0u; - return valueAttrBytes + offsetAttrBytes + clauses + req->ledgerGapBytes; + return valueAttrBytes + offsetAttrBytes + clauses; } // The open tag: `` untrimmed; ` shown= total= capped="1"` when rows were cut or shrunk; ` docs_dropped=` @@ -5121,7 +5119,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: : sizeof( " next_budget_tokens=\"\"" ) - 1 + sigsDecimalDigits( cutNextTokens ), sizeof( " next_offset=\"\"" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/true ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1 + cutNextCost, sizeof( " docs_dropped=\"\"" ) - 1 ); + sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); if( plan.ladderFires ) { // one ladder ACTION on one entry, tail-first; every action re-checks the budget so the ladder @@ -5129,6 +5127,13 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // ladder itself is trimSigLadder() above, shared verbatim with the JSON sibling (§A4a). trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, entryCost ); } + // knob-honesty-068: a CHARGED continuation is paid FROM THE ROWS, byte for byte: the same ladder runs on past the cut + // until the block has given up its bytes, so block + continuation never exceeds the block the cut alone left — in + // either dialect, whatever the plan's slack (reserving it in the plan let that slack absorb it and the bundle grow). + if( plan.capped && cutNextCost > 0 ) + { + trimSigLadder( entries, sigFiles, total, total > cutNextCost ? total - cutNextCost : 0u, entryCost ); + } // A2: the exact count, computed AFTER the ladder has made its final drop decisions (droppedPositiveCount // above — the shared arithmetic with the JSON sibling). Pure bookkeeping, no output bytes either way. @@ -9548,11 +9553,15 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s sizeof( ",\"sigs_next_offset\":" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/false ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1 + cutNextCost, sizeof( ",\"docs_dropped\":" ) - 1 ); + sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); if( plan.ladderFires ) { trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost ); } + if( plan.capped && cutNextCost > 0 ) // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule) + { + trimSigLadder( entries, sigFiles, total, total > cutNextCost ? total - cutNextCost : 0u, jsonSigEntryCost ); + } if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped diff --git a/src/verbs_for.h b/src/verbs_for.h index ad271ba1d..632940192 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2936,7 +2936,6 @@ std::optional runForLens( const MainDispatch& d ) // tokens: 995 vs 816). So the compact charge is capped at the FULL dialect's charge for the same header, computed by the // same rule: rows(default) ⊇ rows(full) by construction, and a default that pays more header than full never gets more // room for rows than full had. - std::size_t compactLedgerGapBytes = 0; // knob-honesty-068: how much MORE sig room this compact header was given than full's if( compactLegendOn ) { ForLensHeaderParts fullParts = headerParts; @@ -2946,8 +2945,7 @@ std::optional runForLens( const MainDispatch& d ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes(); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; - chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); - compactLedgerGapBytes = fullCharged - chargedHeaderBytes; // >= 0 by the min above + chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } const std::size_t fixedBytes = chargedHeaderBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" // the auto bundle's SECTION SPLIT — the sig side's claim is capped so an explicit ceiling wider @@ -3011,16 +3009,11 @@ std::optional runForLens( const MainDispatch& d ) // two regimes are argued): CHARGED exactly when an explicit ceiling's sig side is the whole ceiling (nothing // downstream can pay, and the ceiling is hard); exempt at the default and when the sig side is frozen at the // default's share (forSigSideCeiling), so those ranked sets — and forbudgetmonotoncheck's identity — are untouched. - // THE COMPACT DIALECT PAYS FROM FULL'S ROOM WHEN IT CHARGES ONE: its sig ledger charges less header than it emits and - // leans on the header rungs to land inside the ceiling; a continuation's bytes are not a clause those rungs can shed, - // so a charged one also reserves the room the compact charge was given beyond full's (compactLedgerGapBytes). Only a - // block the ladder already trims pays it (the plan reserves only when it fires): a capped compact answer then serves - // the rows the full one does — rows(default) ⊇ rows(full) still holds, as equality — and lands no further over its - // ceiling than full (compactlegendcheck P4). An uncapped answer is untouched. + // A charged one is paid from the rows byte for byte (serialize.h packSignatures), so a capped bundle never grows + // past the bytes the cut alone left it — in the compact dialect too, whose sig ledger leans on the header rungs. const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; const std::string forRankFlags = forRankShapingFlags( cfg ); - const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, - /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u }; + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation From 67a5dcc675ef9c79c78caf7485d2c589eaee4d77 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:44:17 -0400 Subject: [PATCH 103/229] =?UTF-8?q?test:=20drop=20forspinecheck=20?= =?UTF-8?q?=E2=80=94=20the=20caller=20spine=20fails=20its=20own=20offline?= =?UTF-8?q?=20kill=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upward caller spine was measured before any code was written: on the in-sample --for rows whose answer misses a rubric item, a spine of up to 2 caller hops from the top 3 seeds named a missed item on 1 of 7 rows (2 of 7 counting file names; the uncapped variant also 1 of 7), and the value-reference owners of the top seeds named none. The misses are callees, data tables and files, not callers, so the change is dropped. The shared fixture stays for forsigspancheck. Gate count 666 -> 665. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 - README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/forcompletefix/README.md | 6 +- test/forspinecheck.sh | 387 ---------------------------- test/regression.sh | 3 +- 7 files changed, 13 insertions(+), 400 deletions(-) delete mode 100755 test/forspinecheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 446c99c33..44953e78f 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -198,7 +198,6 @@ "forrootlegendcheck.sh": 8.6, "forsectioncollapsecheck.sh": 5.2, "forsigspancheck.sh": 6.0, -"forspinecheck.sh": 8.0, "freshclonecheck.sh": 170.8, "freshnesscheck.sh": 4.4, "g1configcheck.sh": 0.8, diff --git a/README.md b/README.md index 295421fbe..1e1af4329 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-666 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +665 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **666 gate scripts** and is the authoritative list; +`test/regression.sh` names **665 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index f8c5be3f8..30956ac0c 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` | 666 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 665 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **666 gate scripts**, all of which exist on disk. +naming **665 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 666. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 665. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index aad58c82a..3868b0a5c 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 = [ - ["666 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["665 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "666", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "665", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["666 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["665 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/forcompletefix/README.md b/test/forcompletefix/README.md index d81351ce8..acfd608f1 100644 --- a/test/forcompletefix/README.md +++ b/test/forcompletefix/README.md @@ -1,3 +1,3 @@ -Fixture roots for test/forspinecheck.sh (the --for caller spine and value-reference owners) and -test/forsigspancheck.sh (the e= end line on rows, hop-slot choice, code ranked above docs). -Each subdirectory (c/, js/, py/, ts/) is indexed on its own. The code is minimal, written for the gates, and never built. +Fixture roots for test/forsigspancheck.sh (the e= end line on --for rows, hop-slot choice, code ranked above +docs). Each subdirectory (c/, js/, py/, ts/) is indexed on its own. The code is minimal, written for the gate, and never +built. Some shapes (callers, value tables) are kept as ranking context for the questions the gate asks. diff --git a/test/forspinecheck.sh b/test/forspinecheck.sh deleted file mode 100755 index aec8b79fc..000000000 --- a/test/forspinecheck.sh +++ /dev/null @@ -1,387 +0,0 @@ -#!/usr/bin/env bash -# forspinecheck.sh — a --for answer names who DRIVES its top seeds: the nearest proven callers (the upward spine) and the -# tables a seed is stored in, as names and file:line only, in the answer's tail. -# -# test/forspinecheck.sh # uses build/ripwire on test/forcompletefix -# RIPWIRE_BIN=asan/ripwire test/forspinecheck.sh -# test/forspinecheck.sh build_base/ripwire # the RED run (a pre-change binary) -# -# THE GAP. A --for answer ranks the symbols that MATCH the question and, in , what its top rows CALL. Nothing in -# it says what calls them. On a "how does X happen" question the driver is often the missing half: a scan routine is -# named but not the refresh check that runs it from the main loop; a key action is named but not the dispatcher loop -# that reaches it; draw functions are named but not the struct-array table of function pointers that selects them. -# Measured on graded answers (paraphrased here as minimal code): the gold item was the CALLER of a seed, one or two -# hops up, toward a loop or the program entry; a peer that walked call edges upward from its seeds named it. -# -# THE CONTRACT (one element in the --for answer's tail, after the ranked rows; absent when it has no row): -# with rows : -# * n= p= — a caller's name and its definition's file:line (the same spelling --callers prints). -# * of= — the symbol it calls: a top seed (one of the answer's top 1-3 callable rows) or another row's n=. -# A chain is at most 2 hops from a seed. -# * PROVEN EDGES ONLY. A row with no via= is a call edge --callers lists for of=. A call bound by NAME ALONE (no -# receiver evidence: an untyped parameter, a C function-pointer field spelled like a free function) is never a -# plain row: it is absent, or carries via="name". -# * via="value": n= STORES of= as a value (a table slot, an initializer, a field) — REFVAL's row: n= is the -# 's in_id= (the enclosing definition) when it has one, else the root of its into= (the table), at= its -# bind= file:line. Not a call. A module export (module.exports.X / exports.X) is not a table that runs X and is never such a row. -# * NAMES ONLY: no callee claim, no body, no doc, no signature inside ; no attribute outside n p of via at. -# * a caller in a TEST file is not on the runtime path and is never a row. -# The rows of a seed that has none are simply absent (no row says "none"). -# -# ARMS (one fixture root per language under test/forcompletefix/; each indexed on its own): -# (C) C: (C1) scan chain: Sampler_collectAll <- Monitor_refresh <- Monitor_loop (2 hops, the main loop), and the -# test caller test_collect_all is never a row. (C2) dispatcher loop: Board_onKey <- Monitor_loop. -# (C3) struct-array fn-pointer table: Gauge_drawBar/Text/Dots are stored in Gauge_modes[] -> a via="value" -# row n=Gauge_modes at=src/gauge.c:2x. (C4) key table: actionCycleStyle is stored into keyTable[] by -# Keys_bind -> a via="value" row n=Keys_bind (or n=keyTable) at=src/keys.c:22. -# (C5) name-only near miss: `sink->flush( s )` (a function-pointer field) is never a plain caller row of the -# free function flush; the bare same-file call in Monitor_flushSamples IS one (the near miss that must stay). -# (J) JS: (J1) runJob <- JobQueue.drain (the while loop) <- Worker.tick (a constructor-assigned field: proven). -# (J2) relay's `target.drain()` (an untyped parameter) is never a plain row of drain. -# (J3) resizeImage is stored in the `handlers` object literal -> via="value" n=handlers at=lib/queue.js:14; -# runJob's module.exports binding is never a via="value" row. -# (P) Python: (P1) fetch_entry <- Resolver.resolve (self.cache = LookupCache(): proven) <- App.on_lookup. -# (P2) Downloader.pull's `source.fetch_entry( url )` (an untyped parameter) is never a plain row. -# (P3) dispatcher loop: deliver <- _drain <- run_forever. -# (T) TS: (T1) createContext <- Application.handle. -# (S) shape, on EVERY answer above: every plain row is an edge the same binary's --callers= lists (n= and p=), -# every via="value" row is a of --callers= (at= == bind=); rows carry only n p of via at; no , -# , , CDATA inside ; of= chains reach a non- name within 2 hops; shown <= total; -# capped == (shown < total); shown <= 6; the element sits after . -# (D) dialect parity: --for --json carries the same rows (name, path:line, of, via) as the XML answer of (C1). -# (O) outside --for: --callers, --impact, --pack-signatures and the default map never carry / "spine". -# (L) legend: --legend=full defines , of=, via="value" and says what they do NOT mean (not every caller; a -# value row is not a call). -# (K) the predicates can fail: a synthetic answer with a plain name-only row, an answer with a row --callers does not -# list, and an answer with no root are each caught; every binary run must exit 0. -# -# FLOORS — named, not gated here: -# * loops are not detected: "toward a loop or entry" is the 2-hop bound, not a recognised loop statement. -# * a call inside a nested closure/arrow is attributed as the extractor attributes it today (no arm). -# * value rows exist only where REFVAL extracts a binding (its own floors: JS object literals of IMPORTED names). -# * which caller a fan-in > row cap keeps is not pinned (capped=1 discloses the cut). -# -# Exits non-zero on any failure. - -set -u -export PYTHONDONTWRITEBYTECODE=1 -ROOT="$( cd "$( dirname "$0" )/.." && pwd )" -. "$ROOT/test/lib/clean-env.sh" # a gate that indexes a repo must not inherit GIT_DIR/GIT_WORK_TREE (gitenvhermeticcheck D) -BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" -[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative binary -CORPUS="$ROOT/test/forcompletefix" -TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT -fail=0 -ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } -no(){ printf ' FAIL %s\n' "$*"; fail=1; } - -[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } -[ -d "$CORPUS" ] || { echo "fixture missing: $CORPUS"; exit 2; } -command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } -echo "forspinecheck: BIN=$BIN CORPUS=$CORPUS" - -# run ROOT OUTFILE ARGS… — one binary run against a fixture root; stdout to OUTFILE, its exit status to OUTFILE.rc -run(){ local r="$1" f="$2"; shift 2; ( cd "$CORPUS/$r" && "$BIN" . --no-cache "$@" >"$f" 2>"$f.err" ); printf '%s' "$?" >"$f.rc"; } -# ran_ok FILE LABEL — the run behind FILE exited 0 (a non-zero exit FAILs the arm, whatever it printed) -ran_ok(){ local rc; rc="$( cat "$1.rc" 2>/dev/null )"; [ "$rc" = 0 ] && return 0; no "$2: the binary exited rc=${rc:-missing}"; return 1; } - -# the predicate library — every check reads ONE answer file and prints PASS/FAIL lines itself -cat >"$TMP/spine.py" <<'PY' -import json, re, sys - -def attrs( tag ): - return dict( re.findall( r'\s([a-z_]+)="([^"]*)"', tag ) ) - -def load( path ): - return open( path, encoding="utf-8", errors="replace" ).read() - -def ctx_ok( doc ): - # an answer is only judged when it is a --for document with ranked rows; anything else is NOROOT - return re.search( r"]*>", doc ) is not None and " element, or None when absent.""" - m = re.search( r"]*)?>(.*?)", doc, re.S ) - if not m: - return None - inner = m.group( 2 ) - rows = [ attrs( t ) for t in re.findall( r"]*/>", inner ) ] - return attrs( "" ), rows, inner - -def callers_rows( path ): - """--callers answer -> ({(n, p)}, [vr attrs]) or None when the answer has no matched root.""" - doc = load( path ) - root = re.search( r"]*>", doc ) - if not root or ' defs="' not in root.group( 0 ): - return None - rows = { ( a.get( "n" ), a.get( "p" ) ) for a in ( attrs( t ) for t in re.findall( r"]*/>", doc ) ) } - vrs = [ attrs( t ) for t in re.findall( r"]*/>", doc ) ] - return rows, vrs -PY - -# spinecheck ANSWER LABEL MODE ARGS… — MODE has one of: -# has N OF [P] a plain row n=N of=OF (and p=P when given) is present -# value N OF ATPFX a via="value" row n=N of=OF whose at= starts with ATPFX is present -# notplain N OF no row n=N of=OF without via="name" (absent or hedged) -# absent N no row with n=N at all -# novalue_into PFX no via="value" row whose at=/n= came from an into= starting with PFX (module.exports, exports) -spinecheck(){ - local f="$1" label="$2"; shift 2 - python3 - "$TMP/spine.py" "$f" "$label" "$@" <<'PY' -import sys -exec( open( sys.argv[ 1 ] ).read() ) -path, label, mode = sys.argv[ 2 ], sys.argv[ 3 ], sys.argv[ 4 ] -args = sys.argv[ 5: ] -doc = load( path ) -if not ctx_ok( doc ): - print( " FAIL %s: NOROOT (no --for / answer)" % label ); sys.exit( 1 ) -sp = spine( doc ) -rows = sp[ 1 ] if sp else [] -def plain( r ): return r.get( "via" ) is None -if mode == "has": - n, of = args[ 0 ], args[ 1 ]; p = args[ 2 ] if len( args ) > 2 else None - hit = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and plain( r ) and ( p is None or r.get( "p" ) == p ) ] - good = bool( hit ) - why = "plain " % ( n, of, ( " p=%s" % p ) if p else "" ) -elif mode == "value": - n, of, at = args - hit = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and r.get( "via" ) == "value" and r.get( "at", "" ).startswith( at ) ] - good = bool( hit ) - why = "" % ( n, of, at ) -elif mode == "notplain": - n, of = args - bad = [ r for r in rows if r.get( "n" ) == n and r.get( "of" ) == of and r.get( "via" ) not in ( "name", ) ] - good = not bad - why = "no unhedged (name-only edge)" % ( n, of ) -elif mode == "absent": - n = args[ 0 ] - good = not [ r for r in rows if r.get( "n" ) == n ] - why = "no " % n -elif mode == "novalue_into": - pfx = args[ 0 ] - bad = [ r for r in rows if r.get( "via" ) == "value" and ( r.get( "n", "" ).startswith( pfx ) or r.get( "n" ) in ( "module", "exports" ) ) ] - good = not bad - why = "no via=value row from an %s… binding" % pfx -else: - print( " FAIL %s: unknown mode %s" % ( label, mode ) ); sys.exit( 1 ) -state = "PASS" if good else "FAIL" -print( " %s %s: %s%s" % ( state, label, why, "" if sp or good else " [no element]" ) ) -sys.exit( 0 if good else 1 ) -PY - [ $? -eq 0 ] || { fail=1; return 1; } -} - -# shapecheck ROOT ANSWER LABEL — arm (S): every row is backed by the same binary's --callers answer for its of=; names -# only; bounded; placed after . Runs --callers= for each distinct of= (recorded, rc checked). -shapecheck(){ - local r="$1" f="$2" label="$3" - python3 - "$TMP/spine.py" "$f" "$label" "$CORPUS/$r" "$BIN" "$TMP" <<'PY' -import os, re, subprocess, sys -exec( open( sys.argv[ 1 ] ).read() ) -path, label, root, binp, tmp = sys.argv[ 2: 7 ] -doc = load( path ) -bad = [] -if not ctx_ok( doc ): - print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) -sp = spine( doc ) -if sp is None: - print( " FAIL %s: no element to check the shape of" % label ); sys.exit( 1 ) -head, rows, inner = sp -if not rows: - bad.append( "an empty element (it must be absent when it has no row)" ) -for k in ( "shown", "total", "capped" ): - if not re.fullmatch( r"[0-9]+", head.get( k, "" ) ): - bad.append( "%s= missing or not numeric on " % k ) -if not bad: - shown, total, capped = int( head[ "shown" ] ), int( head[ "total" ] ), int( head[ "capped" ] ) - if shown != len( rows ): bad.append( "shown=%d but %d rows" % ( shown, len( rows ) ) ) - if shown > total: bad.append( "shown=%d > total=%d" % ( shown, total ) ) - if capped != ( 1 if shown < total else 0 ): bad.append( "capped=%d disagrees with shown=%d total=%d" % ( capped, shown, total ) ) - if shown > 6: bad.append( "shown=%d > 6 rows" % shown ) -for forbidden in ( " (names only)" % forbidden ) -for r in rows: - extra = set( r ) - { "n", "p", "of", "via", "at" } - if extra: bad.append( "row %s carries %s" % ( r.get( "n" ), sorted( extra ) ) ) - if not re.fullmatch( r".+:[0-9]+", r.get( "p", "" ) ): bad.append( "row %s p=%r is not file:line" % ( r.get( "n" ), r.get( "p" ) ) ) - if r.get( "via" ) not in ( None, "value", "name" ): bad.append( "row %s via=%r" % ( r.get( "n" ), r.get( "via" ) ) ) - if r.get( "via" ) == "value" and not re.fullmatch( r".+:[0-9]+", r.get( "at", "" ) ): bad.append( "value row %s has no at=file:line" % r.get( "n" ) ) -# depth: follow of= through rows (plain/hedged call rows only) to a name that is not a row — at most 2 hops -callerOf = {} -for r in rows: - if r.get( "via" ) != "value": - callerOf.setdefault( r[ "n" ], [] ).append( r[ "of" ] ) -def depth( name, seen ): - if name not in callerOf or name in seen: return 0 - return 1 + max( depth( o, seen | { name } ) for o in callerOf[ name ] ) -for r in rows: - if r.get( "via" ) != "value" and 1 + depth( r[ "of" ], { r[ "n" ] } ) > 2: - bad.append( "row %s is more than 2 hops from a seed" % r[ "n" ] ) -# placement: in the tail, after the ranked rows -if doc.find( "" ): - bad.append( " precedes (it belongs in the answer's tail)" ) -# backing: every row is an edge (or value ref) the same binary's --callers lists for of= -cache = {} -for r in rows: - of = r[ "of" ] - if of not in cache: - out = os.path.join( tmp, "callers.%s.%s.xml" % ( os.path.basename( root ), re.sub( r"[^A-Za-z0-9_]", "_", of ) ) ) - with open( out, "w" ) as fh: - rc = subprocess.run( [ binp, ".", "--no-cache", "--callers=" + of ], cwd=root, stdout=fh, stderr=subprocess.DEVNULL ).returncode - cache[ of ] = ( rc, callers_rows( out ) ) - rc, cr = cache[ of ] - if rc != 0 or cr is None: - bad.append( "--callers=%s did not answer (rc=%d) — row %s unverifiable" % ( of, rc, r[ "n" ] ) ); continue - plainRows, vrs = cr - if r.get( "via" ) == "value": - if not [ v for v in vrs if v.get( "bind" ) == r.get( "at" ) and ( v.get( "in_id" ) == r[ "n" ] or v.get( "into", "" ).split( "[" )[ 0 ].split( "." )[ 0 ] == r[ "n" ] ) ]: - bad.append( "value row %s of=%s at=%s is not a of --callers=%s" % ( r[ "n" ], of, r.get( "at" ), of ) ) - elif ( r[ "n" ], r.get( "p" ) ) not in plainRows: - bad.append( "row %s p=%s is not a caller --callers=%s lists" % ( r[ "n" ], r.get( "p" ), of ) ) -if bad: - for b in bad: print( " FAIL %s: %s" % ( label, b ) ) - sys.exit( 1 ) -print( " PASS %s: %d rows, every row backed by --callers, names only, <= 2 hops, after " % ( label, len( rows ) ) ) -PY - [ $? -eq 0 ] || { fail=1; return 1; } -} - -# ask ROOT TAG QUESTION — run --for and echo the answer path -ask(){ local f="$TMP/$1.$2.xml"; run "$1" "$f" "--for=$3"; printf '%s' "$f"; } - -echo "(C) C: scan chain, dispatcher loop, fn-pointer tables, a name-only field call" -C1="$( ask c q1 'How does the sampler collect probe readings?' )" -if ran_ok "$C1" "C1"; then - spinecheck "$C1" "C1 first hop" has Monitor_refresh Sampler_collectAll src/monitor.c:4 - spinecheck "$C1" "C1 second hop" has Monitor_loop Monitor_refresh src/monitor.c:14 - spinecheck "$C1" "C1 a test caller is not on the runtime path" absent test_collect_all - shapecheck c "$C1" "S/C1" -fi -C2="$( ask c q2 'How does a key press reach the focused board widget?' )" -if ran_ok "$C2" "C2"; then - spinecheck "$C2" "C2 dispatcher loop" has Monitor_loop Board_onKey src/monitor.c:14 - shapecheck c "$C2" "S/C2" -fi -C3="$( ask c q3 'How does a gauge get drawn in each display style?' )" -if ran_ok "$C3" "C3"; then - spinecheck "$C3" "C3 struct-array table holds the bar draw fn" value Gauge_modes Gauge_drawBar src/gauge.c: - shapecheck c "$C3" "S/C3" -fi -C4="$( ask c q4 'Which key action cycles the gauge style and how is it bound?' )" -if ran_ok "$C4" "C4"; then - spinecheck "$C4" "C4 key table binding" value Keys_bind actionCycleStyle src/keys.c:22 - shapecheck c "$C4" "S/C4" -fi -C5="$( ask c q5 'How does the monitor flush buffered samples?' )" -if ran_ok "$C5" "C5"; then - spinecheck "$C5" "C5 a fn-pointer field call is not a proven caller" notplain Sink_drainSamples flush - spinecheck "$C5" "C5 near miss: the bare same-file call stays" has Monitor_flushSamples flush src/sink.c:20 - shapecheck c "$C5" "S/C5" -fi - -echo "(J) JS: a job loop, a constructed field, an untyped parameter, an object-literal handler table" -J1="$( ask js q6 'How does a queued job get run?' )" -if ran_ok "$J1" "J1"; then - spinecheck "$J1" "J1 the drain loop" has drain runJob lib/queue.js:37 - spinecheck "$J1" "J1 a constructor-assigned field is proven" has tick drain lib/worker.js:10 - spinecheck "$J1" "J2 an untyped parameter is not proven" notplain relay drain - shapecheck js "$J1" "S/J1" -fi -J3="$( ask js q7 'How does a resize job pick its handler?' )" -if ran_ok "$J3" "J3"; then - spinecheck "$J3" "J3 object-literal handler table" value handlers resizeImage lib/queue.js:14 - spinecheck "$J3" "J3 an export is not a dispatch table" novalue_into module.exports - shapecheck js "$J3" "S/J3" -fi - -echo "(P) Python: a constructed field, an untyped parameter, a message pump loop" -P1="$( ask py q8 'How does the lookup cache fetch an entry?' )" -if ran_ok "$P1" "P1"; then - spinecheck "$P1" "P1 first hop (self.cache = LookupCache())" has resolve fetch_entry src/pump/resolver.py:8 - spinecheck "$P1" "P1 second hop" has on_lookup resolve src/pump/app.py:10 - spinecheck "$P1" "P2 an untyped parameter is not proven" notplain pull fetch_entry - shapecheck py "$P1" "S/P1" -fi -P3="$( ask py q9 'How does the message pump deliver queued messages?' )" -if ran_ok "$P3" "P3"; then - spinecheck "$P3" "P3 the pump loop, hop 1" has _drain deliver src/pump/loop.py:13 - spinecheck "$P3" "P3 the pump loop, hop 2" has run_forever _drain src/pump/loop.py:9 - shapecheck py "$P3" "S/P3" -fi - -echo "(T) TS" -T1="$( ask ts q10 'How is a per-request context created?' )" -if ran_ok "$T1" "T1"; then - spinecheck "$T1" "T1 createContext's caller" has handle createContext src/application.ts:20 - shapecheck ts "$T1" "S/T1" -fi - -echo "(D) dialect parity: --for --json carries the rows the XML answer of C1 carries" -DJ="$TMP/c.q1.json"; run c "$DJ" "--for=How does the sampler collect probe readings?" --json -if ran_ok "$DJ" "D"; then - python3 - "$TMP/spine.py" "$C1" "$DJ" <<'PY' -import json, sys -exec( open( sys.argv[ 1 ] ).read() ) -x = spine( load( sys.argv[ 2 ] ) ) -try: - j = json.loads( load( sys.argv[ 3 ] ) ) -except ValueError as e: - print( " FAIL D: --for --json is not JSON (%s)" % e ); sys.exit( 1 ) -js = j.get( "spine" ) -xr = sorted( ( r.get( "n" ), r.get( "p" ), r.get( "of" ), r.get( "via" ) ) for r in ( x[ 1 ] if x else [] ) ) -jr = sorted( ( r.get( "n" ), r.get( "p" ), r.get( "of" ), r.get( "via" ) ) for r in ( ( js or {} ).get( "rows" ) or [] ) ) -if not xr: - print( " FAIL D: the XML answer has no spine rows to compare" ); sys.exit( 1 ) -if xr != jr: - print( " FAIL D: JSON spine rows %s != XML rows %s" % ( jr, xr ) ); sys.exit( 1 ) -print( " PASS D: JSON \"spine\" rows equal the XML rows (%d)" % len( xr ) ) -PY - [ $? -eq 0 ] || fail=1 -fi - -echo "(O) outside --for nothing changes shape: no on other verbs" -for spec in "callers:--callers=Sampler_collectAll" "impact:--impact=Sampler_collectAll" "packsig:--pack-signatures" "map:"; do - tag="${spec%%:*}"; arg="${spec#*:}"; f="$TMP/c.out.$tag.xml" - if [ -n "$arg" ]; then run c "$f" "$arg"; else run c "$f"; fi - if ran_ok "$f" "O/$tag"; then - if [ -s "$f" ] && ! grep -q -e '", doc, re.S ) ) -need = [ ( r"" ), ( r"\bof=", "defines of=" ), ( r'via="?value', "defines via=value" ), - ( r"(?i)not (every|all|a complete)[^.;]*caller", "says the rows are not every caller" ), - ( r"(?i)(not a (proven )?call|never a call)", "says a value row is not a call" ) ] -bad = [ what for rx, what in need if not re.search( rx, legend ) ] -if bad: - print( " FAIL L: the legend does not %s" % "; ".join( bad ) ); sys.exit( 1 ) -print( " PASS L: legend defines /of=/via=value with its NOT-meaning" ) -PY - [ $? -eq 0 ] || fail=1 -fi - -echo "(K) the predicates can fail" -K1="$TMP/k1.xml" -printf '' >"$K1" -if ( spinecheck "$K1" "K1 (expected FAIL)" notplain relay drain ) >/dev/null 2>&1; then no "K1: a plain name-only row was not caught"; else ok "K1: a plain name-only row is caught"; fi -K2="$TMP/k2.xml" -printf '' >"$K2" -if ( shapecheck c "$K2" "K2 (expected FAIL)" ) >/dev/null 2>&1; then no "K2: a row --callers does not list was not caught"; else ok "K2: a row --callers does not list is caught"; fi -K3="$TMP/k3.xml"; printf '' >"$K3" -if ( spinecheck "$K3" "K3 (expected FAIL)" absent anything ) >/dev/null 2>&1; then no "K3: a NOROOT document passed"; else ok "K3: a document with no --for root is never a pass"; fi -K4="$TMP/k4.xml" -printf '' >"$K4" -if ( shapecheck c "$K4" "K4 (expected FAIL)" ) >/dev/null 2>&1; then no "K4: a callee claim inside was not caught"; else ok "K4: a callee claim inside is caught"; fi - -if [ "$fail" -ne 0 ]; then - echo "forspinecheck: FAIL" - exit 1 -fi -echo "forspinecheck: PASS" -exit 0 diff --git a/test/regression.sh b/test/regression.sh index 2f4ab2b80..b9c0727cd 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -276,8 +276,9 @@ else no "Codex active-surface doctor gate (codexdoctorcheck.sh failed)" RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi +# retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forspinecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubyconstcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 5272733cfcb46ed0de20d8728ff81cd25f379396 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 13:32:53 -0400 Subject: [PATCH 104/229] --for: rows carry e= (where the definition ends); code ranks above the docs that repeat a question e=: every row of a --for answer (XML, --json "e", MCP for) carries the 1-based line of the definition's last line, body-inclusive, right after l=. Absent, never 0, when the extent is not known: a markdown/config row, a module-scope row, an extent_suspect row, or a span the file's bytes cannot hold. It is spliced outside the bytes the signature-row budget charges, so a ceiling admits the rows it admitted before; its legend clause (present-only, both dialects and the MCP twin) is exempt on the same contract. Other verbs (--pack-signatures, --pack-task, --from-trace, the map) keep their bytes: packSignatures' relevance-floor bool becomes SigLensRules, and only --for and MCP for ask for EndLine. Code above docs: on a question that does not ask about docs, every scored markdown row ranks after every scored code row (docs keep their order and stay in the answer), disclosed as " [docs after code: N ...]". A docs, change or translation cue, or a named .md/.rst file, keeps score order. Routed path only. The compact note channel now compacts each note on its own, so a later note in the doc-mention channel is no longer swallowed by its rewrite. Gate: forsigspancheck (the two name-only hop arms SKIP by name until the receiver-evidence hedge lands). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 20 +++++++ src/filter.h | 87 ++++++++++++++++++++++++++++ src/mcpverbs.h | 17 +++++- src/packtask.h | 2 +- src/serialize.h | 124 ++++++++++++++++++++++++++++++++++++++-- src/tracelocus.h | 2 +- src/verbs_for.h | 59 ++++++++++++++++--- test/forsigspancheck.sh | 81 ++++++++++++++++++++++---- 8 files changed, 364 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..86872dd72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,26 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `--for` rows say where a definition ends (`e=`); code ranks above the docs that repeat a question + +- **`e=` on `` rows.** A `--for` signature row carried only `l=`, the line of the definition's name, so "which body + holds line N" was not answerable from the row. Every `` row of a `--for` answer (CLI XML, `--json` as `"e"`, and the + MCP `for` tool) now carries `e=`, the 1-based line where that definition ends, body-inclusive, right after `l=`. It + is ABSENT — never 0, never a guess — when the extent is not known: a markdown or config row, a module-scope row, a + row flagged `extent_suspect=`, or a span the file's current bytes cannot hold. `l=` keeps its meaning, so a + definition can start above `l=` (a return type on the line before, a decorator). The attribute is exempt from the + signature-row byte budget, so a ceiling admits the same rows it admitted before; it costs about 8 bytes per row. + `--pack-signatures`, `--pack-task`, `--from-trace` and the map keep their bytes. +- **Code above docs.** On a question that does not ask about docs, every scored markdown row now ranks after every + scored code row (the doc rows keep their own order and stay in the answer; they fill only slots no code row + competes for). A question that names docs (`doc…`, `readme`, `guide`, `tutorial`, `manual`, `markdown`, `wiki`, + `faq`, `howto`, a change or translation cue, or a named `.md`/`.rst` file) keeps the plain score order. The answer + says when the rule moved anything: ` [docs after code: N doc rows ranked below every code row; …]`. Routed path + only (`--no-route` restores the old order); `--for` and MCP `for` only. + +Gate: `forsigspancheck` (fixture `test/forcompletefix`, C/JS/Python/TS). + + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) The MCP `grep` hit row was `{file, line, in}`: no matched text, so an agent re-read every file it had just searched, and diff --git a/src/filter.h b/src/filter.h index 008f6a03e..2afd3aecb 100644 --- a/src/filter.h +++ b/src/filter.h @@ -881,6 +881,93 @@ inline std::vector docNoiseSymbolMultipliers( const IngestResult& ing, st return mul; } +// ── CODE ABOVE DOCS (--for and MCP `for`, routed path only; gate test/forsigspancheck.sh (R)) ──────────────── +// Markdown headings that repeat a question's words used to fill the ranked rows ABOVE the code that defines the +// answer (on one graded answer about half of the shown rows were headings). A question about CODE now ranks every +// positive-score doc row after every positive-score non-doc row: the doc rows keep their own order and stay +// positive (nothing is dropped, they still fill any slot no code row competes for), only their place moves. +// A question that ASKS about docs keeps the plain score order: a docs cue (kDocsQuestionCues), a change or +// translation cue (the two doc-noise lists above: those questions are about prose too), or a named doc file. +// A false cue is the cheap side to be wrong on — it restores the old order. +inline constexpr DocNoiseCue kDocsQuestionCues[] = { { "doc", true }, { "readme", true }, { "guide", true }, { "tutorial", true }, + { "manual", true }, { "markdown", false }, { "wiki", false }, { "faq", false }, + { "howto", false } }; +// The scale that places the best doc row strictly below the weakest code row: half of it, so float rounding can +// never land a doc row ON a code row's score (the (score desc, id asc) order would then interleave them by id). +inline constexpr float kDocsAfterCodeGap = 0.5f; +static_assert( kDocsAfterCodeGap > 0.f && kDocsAfterCodeGap < 1.f, "doc rows land strictly below the weakest code row, and stay positive" ); + +inline bool taskAsksAboutDocs( std::string_view task ) +{ + const std::string lowerTask = queryshape::detail::lowerAscii( task ); + if( lowerTask.find( ".md" ) != std::string::npos || lowerTask.find( ".rst" ) != std::string::npos ) + { + return true; // a named doc file (the B8 mention anchor lifts it; this rule must not push it back down) + } + return taskHasCue( lowerTask, kDocsQuestionCues ) || taskHasCue( lowerTask, kChangeQuestionCues ) + || taskHasCue( lowerTask, kTranslationQuestionCues ); +} + +// Apply the rule to a final lens rank. Returns how many doc rows it MOVED (scored at or above the weakest code row +// before; 0 ⇒ nothing changed, byte-identical). The caller discloses a non-zero count in the bundle's note. +inline std::size_t applyCodeAboveDocs( const IngestResult& ing, std::string_view task, std::vector& rank ) +{ + EXPECTS( rank.size() <= ing.symbols.size(), "one score per symbol at most" ); + if( taskAsksAboutDocs( task ) ) + { + return 0; + } + float minCode = 0.f; + float maxDoc = 0.f; + bool anyCode = false; + for( std::size_t i = 0; i < rank.size(); ++i ) + { + if( rank[i] <= 0.f ) + { + continue; + } + if( ing.symbols[i].lang == Lang::Markdown ) + { + maxDoc = std::max( maxDoc, rank[i] ); + } + else if( !anyCode || rank[i] < minCode ) + { + minCode = rank[i]; + anyCode = true; + } + } + if( !anyCode || maxDoc <= 0.f || maxDoc < minCode ) + { + return 0; // no doc row, no code row, or every doc row already below every code row + } + const float scale = minCode * kDocsAfterCodeGap / maxDoc; // maxDoc >= minCode > 0 ⇒ 0 < scale <= kDocsAfterCodeGap + std::size_t moved = 0; + for( std::size_t i = 0; i < rank.size(); ++i ) + { + if( rank[i] > 0.f && ing.symbols[i].lang == Lang::Markdown ) + { + moved += rank[i] >= minCode ? 1u : 0u; + rank[i] = std::max( rank[i] * scale, std::numeric_limits::min() ); // stays positive: still a scored row + } + } + ENSURES( moved > 0, "maxDoc >= minCode, so the best doc row moved" ); + return moved; +} + +// The bundle note for a moved count (" [docs after code: N …]"), "" at zero — the same free-text channel the doc-mention +// note rides, so both dialects and the compact legend carry it verbatim. +inline std::string docsAfterCodeNote( std::size_t moved ) +{ + if( moved == 0 ) + { + return {}; + } + char nb[ 140 ]; + rw::formatTo( nb, sizeof( nb ), " [docs after code: {} doc row{} ranked below every code row; a question naming docs keeps score order]", + moved, moved == 1 ? "" : "s" ); + return nb; +} + // Order a file-id list by a per-id KEY descending, PATH ascending as the tiebreak — the shared // "most-consequential-first, and deterministically so" ordering the first-screen verbs need (§P11.7 // --pr-context by blast radius, §P11.8 --tree by best-symbol rank). Templated on the key because one of diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..2376a73bc 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1945,6 +1945,9 @@ inline std::optional forTaskText( const std::string& root, const st } absorbCapDisclosure( docMentionInfo.caps, docMentionNote, capAttrs ); } + // code above docs — the CLI twin's rule on the final rank (filter.h applyCodeAboveDocs); its note is sig-charge exempt below + const std::string mcpDocsAfterCode = noRoute ? std::string() : docsAfterCodeNote( applyCodeAboveDocs( ing, task, lensRank ) ); + docMentionNote += mcpDocsAfterCode; // LB-A (r10 §5) — THE RELEVANCE FLOOR, the CLI --for's own call (serialize.h relevanceFloorCut): one // bundle-composition contract may not have two behaviours. lensRank is final at this point. @@ -2115,6 +2118,13 @@ inline std::optional forTaskText( const std::string& root, const st { mcpForScPresent = lensRank[i] > 0 && rw::hasScopeAttr( ing.symbols[i] ); } + // e= (serialize.h defEndLine): may a served row carry it? The sc= presence rule's shape (ranked set, over-approximating). + bool mcpForEndLinePresent = false; + for( std::size_t i = 0; i < ing.symbols.size() && !mcpForEndLinePresent; ++i ) + { + mcpForEndLinePresent = lensRank[i] > 0 && rw::mayCarryEndLine( ing.symbols[i] ); + } + const std::string_view mcpEndLineLegend = mcpForEndLinePresent ? rw::kForEndLineLegend : std::string_view(); // ONE decision, read twice below: appended into the header here, subtracted from the sigs charge there. const rw::ForIdRouteLegendParts mcpIdRouteParts = rw::forIdRouteLegendParts( /*legendOn=*/true, mcpForScPresent, mcpForRouteAttrOn ); // R2-AF (round 2, S4): the SAME resolver the CLI twin calls (rw::forNamedHeaderRows, mention.h) — one @@ -2135,6 +2145,7 @@ inline std::optional forTaskText( const std::string& root, const st + std::string( rw::kMcpForLensColumnsLegend ) + std::string( rw::kForFileTailLegend ) // deep-tail: r= + definitions, the CLI twin's exact clause (sigs-charge-exempt below) + ( mcpForHdrRows.empty() ? std::string() : std::string( rw::kForHdrLegend ) ) // R2-AF: present-only + + std::string( mcpEndLineLegend ) // e=: present-only, the CLI twin's full clause (sig-charge exempt below) + " -->" // r2-LO: at= rides this root (mcpForAtAttrStr) and was defined nowhere in the document — the CLI twin passes // its at= condition here and this twin did not. Its 24 B are exempt from the sigs charge below, so the served @@ -2182,8 +2193,10 @@ inline std::optional forTaskText( const std::string& root, const st // (the CLI twin's own idRouteParts ledger, verbs_for.h, for the identical reason). const std::size_t mcpIdRouteExemptBytes = mcpIdRouteParts.bytes(); const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above + // e= and docs-after-code: disclosures on the same contract (the CLI twin exempts the same two) + const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size() + mcpDocsAfterCode.size(); const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" + - mcpEndLineExemptBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" // L3: field-notes surfacing — parity with the CLI --for lens. loadNoteIndex reads root/.ripwire_notes (a @@ -2207,7 +2220,7 @@ inline std::optional forTaskText( const std::string& root, const st sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p=, same argument the CLI twin passes - /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad + rw::SigLensRules::ForLens, // LB-A: shrink past the zero-score tail, never pad; e= rows &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) &mcpShownIds, // lane 2: see verbs_for.h shownSigIds &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below diff --git a/src/packtask.h b/src/packtask.h index 62c7d49a4..035458563 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1039,7 +1039,7 @@ inline RankingSection renderRankingWithFar( const IngestResult& ing, const Ranki /*rankAdaptivePayload=*/true, /*payloadBudgetBytes=*/ri.sigsBudget, /*noteIndex=*/nullptr, // notes are a DEDICATED section (4), never inline here (avoids double-emit) ri.in->rootArg, - /*hasRelevanceFloor=*/false, // R2: eligibleIds is ALREADY the curated set (d0∪d1 depth mask), + rw::SigLensRules::None, // no LB-A floor, no e=. R2: eligibleIds is ALREADY the curated set (d0∪d1 depth mask), // not a floor-narrowed topN — droppedPositiveCount re-checks // rank>0 per symbol regardless, so this is unaffected either way &out.droppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) diff --git a/src/serialize.h b/src/serialize.h index 78f7de82e..eafe10fec 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4221,6 +4221,96 @@ inline std::string sigRowHead( const IngestResult& ing, NodeId id, const SigRowF return head; } +// ── e= — WHERE A DEFINITION ENDS (the --for lens rows; gate test/forsigspancheck.sh) ────────────────── +// l= is the line of a definition's NAME. A reader asking "which body holds line N" also needs the LAST line, and a +// graded answer lost credit for an item inside a function because its row carried only l=. e= is that last line: +// the parser's body-inclusive span end (Symbol::endByte), counted in the file's current bytes. +// +// It is printed ONLY when the extent is known. Unknown — e= absent, never 0 and never a guess — for: +// * a row that is not a code definition: a markdown heading, a config key (isCodeLang), a module-scope owner +// (synthetic: no body by construction); +// * a row extent_suspect= flags (its span may be a parse-recovery artifact: kExtentSuspectRowLegend); +// * a span the file's bytes cannot hold, or one that ends above its own name line. +// What e= does NOT mean: that every line in l..e belongs to this definition alone (a nested definition shares +// them), or that the definition starts at l= (a return type or decorator can sit above the name). +// +// The file's line breaks, built once per file by the caller and only when a row asks for e=. +inline std::vector lineBreaksOf( std::string_view src ) +{ + std::vector breaks; + for( std::size_t at = src.find( '\n' ); at != std::string_view::npos; at = src.find( '\n', at + 1 ) ) + { + breaks.push_back( std::uint32_t( at ) ); + } + return breaks; +} + +// The symbol-table half of defEndLine's rule (no file bytes): may this row carry e= at all? The legend's present-only bit. +inline bool mayCarryEndLine( const Symbol& s ) noexcept +{ + return isCodeLang( s.lang ) && s.kind != SymKind::Section && s.kind != SymKind::ModuleScope && s.extentSuspect == 0 + && s.endByte > s.sigStartByte; +} + +// The 1-based e= value of `s` against its file (`breaks` from lineBreaksOf over `srcSize` bytes), or 0 = unknown. +inline std::uint32_t defEndLine( const Symbol& s, const std::vector& breaks, std::size_t srcSize ) noexcept +{ + if( !mayCarryEndLine( s ) || s.endByte > srcSize ) + { + return 0; // not a code definition with a trusted span, or a span the current bytes cannot hold + } + const std::uint32_t lastByte = s.endByte - 1; // the definition's own last byte; a '\n' there ends ITS line + const std::size_t before = std::size_t( std::lower_bound( breaks.begin(), breaks.end(), lastByte ) - breaks.begin() ); + const std::uint32_t endLine = std::uint32_t( before ) + 1u; + ENSURES( endLine >= 1u, "a line number is 1-based" ); + return endLine >= s.line ? endLine : 0u; +} + +// The legend clauses defining e=, present-only (a document whose rows can carry one), in both --for dialects and the MCP +// twin; gate test/forsigspancheck.sh (L). Each says what the attribute does NOT mean: absent is unknown (never a 0), and +// l= stays the NAME's line. +inline constexpr std::string_view kForCompactEndLineLegend = + "; d e= last line of the definition (absent = extent unknown, never 0; l= is its name's line)"; +inline constexpr std::string_view kForEndLineLegend = + "; e= on a d row: the 1-based line where that definition ends, body-inclusive; absent when the extent is not known " + "(extent_suspect, docs, config), never 0; l= is the line of the definition's name, so a definition can start above l="; + +// The " e=\"N\"" run, spliced into a rendered head right after its l= value (the head always opens `0) within the kept // head never reached the emitted — cut either @@ -4677,7 +4769,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } sortutil::radixSortByScoreDescId( order, rank ); std::size_t keep = std::min( topN > 0 ? std::size_t( topN ) : S, S ); - if( hasRelevanceFloor ) + if( hasSigLensRule( lensRules, SigLensRules::RelevanceFloor ) ) { keep = relevanceFlooredKeep( order, rank, keep ); // LB-A: shrink, never pad } @@ -4750,6 +4842,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: bool positive = false; // A2: rank[id] > 0 at collection time (the disclosure's own definition of "positive") bool hadDoc = false; // the source HAS a doc comment here (before the rank tiers) — docs_dropped= counts // the shown rows where this is true and `doc` is empty at emission + std::uint32_t endLine = 0; // e= (defEndLine), 0 = not printed; spliced at emission, OUTSIDE `head` and so + // outside every byte the gate and the ladder charge (writeSigHeadWithEnd) }; std::vector sigFiles; std::vector entries; @@ -4791,6 +4885,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: std::vector& syms = buckets[f]; std::sort( syms.begin(), syms.end(), [ & ]( NodeId a, NodeId b ) { return ing.symbols[a].sigStartByte < ing.symbols[b].sigStartByte; } ); + const std::vector lineBreaks = hasSigLensRule( lensRules, SigLensRules::EndLine ) ? lineBreaksOf( src ) + : std::vector{}; SigFile sf; sf.fileId = f; @@ -4879,6 +4975,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: e.notes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 key + W3-N2 pre-render e.positive = rank[id] > 0.0f; // A2: this symbol's own score, at collection time e.hadDoc = hadDoc; + e.endLine = hasSigLensRule( lensRules, SigLensRules::EndLine ) ? defEndLine( s, lineBreaks, src.size() ) : 0u; entries.push_back( std::move( e ) ); ++sf.liveCount; } @@ -4997,7 +5094,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: continue; } pushShownSigId( shownIdsOut, order, e.globalRank ); // lane 2 - w.write( e.head.c_str() ); + writeSigHeadWithEnd( w, e.head, e.endLine ); // e= after l=, when the lens asked and the extent is known if( !e.doc.empty() ) { w.write( "" ); w.write( escapeXml( e.doc, esc ) ); w.write( "" ); } w.write( escapeXml( e.sig, esc ) ); w.write( e.notes ); // L3: symbol notes on this (inert when null) @@ -8932,6 +9029,8 @@ struct JsonSigEntry bool dropped = false; bool positive = false; // A2: rank[id] > 0 at collection time — the XML sibling's own field bool hadDoc = false; // the source has a doc comment here (before the rank tiers) — the XML sibling's field + std::uint32_t endLine = 0; // "e" (defEndLine), 0 = no key; spliced at emission, outside `head` and every charged + // byte (the XML sibling's SigEntry::endLine, same exemption) }; // §B1.3: how many notes this array-emitter matched, and how many survived the ladder — the caller pairs @@ -9001,6 +9100,9 @@ struct JsonSigLens // `notes` array on the row/file the XML // sibling hangs children on. // nullptr ⇒ INERT (byte-identical). + bool endLines = false; // the XML sibling's SigLensRules::EndLine: an "e" + // key (defEndLine) on every row whose extent is + // known — --for only; off ⇒ byte-identical. }; // One row's `{"l":…` opening through its flag fields — everything EXCEPT doc/sig, which the ladder mutates @@ -9149,6 +9251,7 @@ inline std::size_t collectJsonSigEntries( const IngestResult& ing, const std::ve std::vector& syms = buckets[f]; std::sort( syms.begin(), syms.end(), [ & ]( NodeId a, NodeId b ) { return ing.symbols[a].sigStartByte < ing.symbols[b].sigStartByte; } ); + const std::vector lineBreaks = lens.endLines ? lineBreaksOf( src ) : std::vector{}; JsonSigFile sf; sf.fileId = f; @@ -9205,6 +9308,7 @@ inline std::size_t collectJsonSigEntries( const IngestResult& ing, const std::ve e.noteCount = appendJsonNoteArray( e.notes, lens.noteIndex, symbolNoteTarget( lens.noteIndex, ing, s ) ); // §B1.3 e.positive = rank && (*rank)[id] > 0.0f; // A2: the XML sibling's own field, same definition e.hadDoc = hadDoc; + e.endLine = lens.endLines ? defEndLine( s, lineBreaks, src.size() ) : 0u; outEntries.push_back( std::move( e ) ); ++sf.liveCount; } @@ -9387,6 +9491,16 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } first = false; std::string row = e.head; + if( e.endLine > 0 ) + { + // "e" right after "l" — the XML sibling's e= position (writeSigHeadWithEnd); the head always opens {"l":N + ASSUME( row.starts_with( "{\"l\":" ) ); + const std::size_t lEnd = row.find( ',' ); + ASSUME( lEnd != std::string::npos ); + char eKey[ 24 ]; + rw::formatTo( eKey, sizeof( eKey ), ",\"e\":{}", e.endLine ); + row.insert( lEnd, eKey ); + } if( !e.doc.empty() ) { appendJsonStrField( row, ",\"doc\":", e.doc ); diff --git a/src/tracelocus.h b/src/tracelocus.h index 9426f8cdc..b505948ac 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -1219,7 +1219,7 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph /*rankAdaptivePayload=*/true, sigsBudget, in.notes, // L3: field-notes surfacing (inert when null) in.rootArg, // R-R: root-relative - /*hasRelevanceFloor=*/false, /*droppedPositiveOut=*/nullptr, /*shownIdsOut=*/nullptr, + rw::SigLensRules::None, /*droppedPositiveOut=*/nullptr, /*shownIdsOut=*/nullptr, /*cappedOut=*/nullptr, /*topRowNext=*/{}, &sigsCut ); // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped), the --for twin's clauses // verbatim, as a comment right after the block they define: this lens's header is priced before the section diff --git a/src/verbs_for.h b/src/verbs_for.h index dfe6a6967..b8981b798 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -543,6 +543,9 @@ struct ForLensHeaderParts bool modScopePresent = false; // #60: does any ranked row name a file's MODULE SCOPE (t="modscope", // n=)? It reaches this bundle as a row and, more often, // as a hop row. Same over-approximation, same reason. + bool endLinePresent = false; // may a row carry e= (serialize.h defEndLine)? A code row with a known + // extent among the row head — read from the same head walk as + // modScopePresent; a file the emitter then fails to read is the residual. // ── THE DROPPABLE LEGEND, as ONE bit ────────────────────────────────────────────────────────────── // confidenceNote / tailLegend / idRouteLegend moved in lock step at every read and every write, and the @@ -780,7 +783,8 @@ inline constexpr std::string_view kForCompactLegendHdr = // The data notes in their compact spelling: the numbers stay, the sentence goes. Unknown shapes pass // through VERBATIM — a note this table does not know is never shortened into something it did not say. -inline std::string compactForNote( std::string_view note ) +// ONE note (" [...]"); compactForNote below splits a channel that carries several. +inline std::string compactOneForNote( std::string_view note ) { // " [relevance floor: kept 7 of 40 - the other 33 scored zero…]" → " [floor: kept 7 of 40]" if( note.starts_with( " [relevance floor: kept " ) ) @@ -815,6 +819,22 @@ inline std::string compactForNote( std::string_view note ) return std::string( note ); } +// A note channel can carry SEVERAL notes back to back (the doc-mention channel also carries its cap note and the +// docs-after-code note, filter.h docsAfterCodeNote). Each " [...]" is compacted on its own, so a later note is never +// swallowed by an earlier note's rewrite (which keeps only the first note's numbers). +inline std::string compactForNote( std::string_view note ) +{ + std::string out; + while( !note.empty() ) + { + const std::size_t next = note.find( "] [" ); + const std::size_t len = next == std::string_view::npos ? note.size() : next + 1; + out += compactOneForNote( note.substr( 0, len ) ); + note.remove_prefix( len ); + } + return out; +} + // Does THIS answer's root carry route=? The reading of a code is present-only in both dialects, so both ask the // question the same way: the ladder's rung (c) may have dropped the attribute (withRouteAttr), and a run the // router never ran for (no-route, or a verb with no route at all) never had one. Read off the built root open @@ -870,6 +890,7 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, { p.composePresent, kForCompactLegendCompose }, { p.legoPresent, kForCompactLegendLego }, { p.layerPresent, kForCompactLegendLayer }, { p.modScopePresent, rw::kForCompactModScopeClause }, // #60 + { p.endLinePresent, rw::kForCompactEndLineLegend }, }; for( const auto& [ on, clause ] : kPresentOnly ) { @@ -1019,6 +1040,10 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout { h.append( rw::kForModScopeClause ); // #60: present-only, on the same bit the compact strip reads } + if( p.endLinePresent ) + { + h.append( rw::kForEndLineLegend ); // e=: present-only, on the same bit the compact strip reads + } if( p.legendDropped ) { // L1: the droppable clauses went to the ceiling — say so, and name exactly the ones THIS document had @@ -1452,7 +1477,7 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, - in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex }; + in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true }; JsonSigNoteCounts noteCounts; const auto packSigs = [ & ]( std::FILE* dst, std::size_t budget, bool* outCapped, std::size_t* outDroppedPositive, std::vector* outShownIds, rw::SigsCutReport* outCut ) @@ -2260,6 +2285,10 @@ std::optional runForLens( const MainDispatch& d ) // --anchor: ROUTE picks the base lens rank, then ANCHOR expands it; mention/co-change run after. LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning + // code above docs (filter.h applyCodeAboveDocs): --for's own rule, routed path only, on the final rank. Its note rides + // the doc-mention channel and is exempt from the sig charge below (a disclosure never costs a row). + const std::string docsAfterCode = cfg.noRoute ? std::string() : docsAfterCodeNote( applyCodeAboveDocs( ing, cfg.forTask, lr.rank ) ); + lr.docMentionNote += docsAfterCode; std::vector lensRank = std::move( lr.rank ); const std::string routeNoteRaw = std::move( lr.routeNote ); // verbatim; lands ONLY in route= (attribute-escaped) + the JSON twin — L1: the comment no longer echoes it const std::string mentionNote( std::move( lr.mentionNote ) ); @@ -2615,10 +2644,16 @@ std::optional runForLens( const MainDispatch& d ) // which is the residual a header built before its payload cannot avoid. const std::size_t forRowHead = std::max( std::size_t( forTopN > 0 ? forTopN : 0 ), rw::kPackTaskBodyCandidates ); bool forModScopePresent = false; - for( std::size_t i = 0; i < lensSurfaceIds.size() && i < forRowHead && !forModScopePresent; ++i ) + bool forEndLinePresent = false; + for( std::size_t i = 0; i < lensSurfaceIds.size() && i < forRowHead && !( forModScopePresent && forEndLinePresent ); ++i ) { const rw::NodeId sid = lensSurfaceIds[i]; - forModScopePresent = lensRank[sid] > 0.0f && ing.symbols[sid].kind == rw::SymKind::ModuleScope; + if( lensRank[sid] <= 0.0f ) + { + continue; + } + forModScopePresent = forModScopePresent || ing.symbols[sid].kind == rw::SymKind::ModuleScope; + forEndLinePresent = forEndLinePresent || rw::mayCarryEndLine( ing.symbols[sid] ); } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, @@ -2626,7 +2661,7 @@ std::optional runForLens( const MainDispatch& d ) cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg, /*hdrLegend=*/!forHdrRows.empty(), forScPresent, forComposePresent, - forLegoPresent, forLayerPresent, forModScopePresent }; + forLegoPresent, forLayerPresent, forModScopePresent, forEndLinePresent }; const auto buildForHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { return forLensHeaderText( headerParts, withRouteAttr, withTaskEcho, extraNotes ); }; std::string headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); @@ -2893,7 +2928,12 @@ std::optional runForLens( const MainDispatch& d ) const rw::ForIdRouteLegendParts idRouteParts = rw::forIdRouteLegendParts( headerParts.idRouteLegend, headerParts.scPresent, !routeNoteRaw.empty() ); const std::size_t idRouteLegendEmitted = compactLegendOn ? idRouteParts.route.size() : idRouteParts.bytes(); - const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted; + // e= (forsigspancheck): its legend clause and the docs-after-code note are disclosures on the same contract — exempt, + // so the ranked rows a ceiling admits are the ones it admitted without them (owner ruling: e= is exempt from the row budget). + const std::size_t endLineLegendEmitted = !headerParts.endLinePresent ? 0u + : ( compactLegendOn ? rw::kForCompactEndLineLegend.size() : rw::kForEndLineLegend.size() ); + const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted + + endLineLegendEmitted + docsAfterCode.size(); if( exemptBytes > headerStr.size() ) { DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -2913,7 +2953,8 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t fullHeaderBytes = forLensHeaderText( fullParts, /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ).size(); const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() - + rw::kForFileTailLegend.size() + idRouteParts.bytes(); + + rw::kForFileTailLegend.size() + idRouteParts.bytes() + + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ) + docsAfterCode.size(); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } @@ -2985,7 +3026,7 @@ std::optional runForLens( const MainDispatch& d ) sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p= - /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad + rw::SigLensRules::ForLens, // LB-A: shrink past the zero-score tail, never pad; e= on every row &forDroppedPositive, // A2: exact count, see droppedPositiveCount &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then @@ -3471,7 +3512,7 @@ std::optional runForLens( const MainDispatch& d ) { packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, - /*hasRelevanceFloor=*/true, nullptr, nullptr, nullptr, // LB-A: the direct-emission degrade path selects identically + rw::SigLensRules::ForLens, nullptr, nullptr, nullptr, // LB-A + e=: the direct-emission degrade path selects identically forTopRowNext ); // L-W: same next= rule on the degrade path } if( legoPreRendered ) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index b5db6e0d3..ccbcccfe0 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -41,10 +41,15 @@ # (H) hop slots: TS — contextHost (only callee: a name-only `bag.lookup()`) has no row; createContext keeps # one. Python — Downloader.pull (only callee: a name-only `source.fetch_entry()`) has no row; fetch_entry # keeps one. C near miss — Sampler_collectAll keeps its row. rows follow the rows' r= order. -# (R) code over docs: TS "How is a per-request context created?" — createContext and the class Context rank above -# every docs/context.md row, and md rows <= 25% of the shown rows. Twin: "Where do the docs describe the -# context lifecycle?" keeps a docs row in its top five. +# The two "no row" arms need the receiver-evidence hedge (a name-only row marked via="name"); on a binary +# without it they SKIP by name ("expects FE-B"), detected on this fixture — never a silent PASS. +# (R) code over docs: TS "How is a per-request context created?" — createContext and the class Context, and every +# other code row, rank above every docs/context.md row; md rows <= 25% of the shown rows unless no code row was +# displaced (the window holds fewer than 40 rows and is not cut); the answer carries the +# " [docs after code: N …]" note. Twin: "Where do the docs describe the context lifecycle?" keeps a docs row in +# its top five. # (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). +# (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. # (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's # line, not the definition's first line). # (K) the predicates can fail: a row with e= < l=, a row with e= on an md row, a missing e= on a code row, and a @@ -88,9 +93,12 @@ def ctx_ok( doc ): return re.search( r"]*>", doc ) is not None and " row, in document order.""" - return [ ( t, attrs( t ) ) for t in re.findall( r"]*>", doc ) ] + return [ ( t, attrs( t ) ) for t in re.findall( r"]*>", strip_comments( doc ) ) ] def hrows( doc ): - return [ attrs( t ) for t in re.findall( r"]*>", doc ) ] + return [ attrs( t ) for t in re.findall( r"]*>", strip_comments( doc ) ) ] +def strip_comments( doc ): + # legend text inside spells row shapes (``); only real elements are rows + return re.sub( r"", "", doc, flags=re.S ) def is_doc_row( a ): return a.get( "p", "" ).endswith( ( ".md", ".markdown", ".rst", ".txt" ) ) PY @@ -243,14 +251,30 @@ sys.exit( 0 if good else 1 ) PY [ $? -eq 0 ] || { fail=1; return 1; } } +# CAPABILITY (checklist 17): the "only name-only edges" premise needs the receiver-evidence hedge — a binary that marks a +# call bound by name alone via="name". Detected here, on this fixture: Downloader.pull's `source.fetch_entry( url )` is +# such a call. A binary without the hedge cannot tell that edge from a proven one, so the two nohop arms SKIP BY NAME; +# every other arm (the near misses, rank order) runs either way. +FEB="$TMP/py.callers.fetch_entry.xml"; run py "$FEB" --callers=fetch_entry +FEB_ON=0 +if ran_ok "$FEB" "H premise"; then + if ! grep -q ']*n="pull"[^>]*via="name"|]*via="name"[^>]*n="pull"' "$FEB"; then + FEB_ON=1 + fi +fi +nohop_or_skip(){ + if [ "$FEB_ON" = 1 ]; then hopcheck "$@"; else printf ' SKIP %s: expects FE-B (this binary marks no name-only row via="name")\n' "$2"; fi +} if ran_ok "$TE" "H/TS"; then - hopcheck "$TE" "H/TS getter with a name-only edge" nohop contextHost + nohop_or_skip "$TE" "H/TS getter with a name-only edge" nohop contextHost hopcheck "$TE" "H/TS near miss" hop createContext hopcheck "$TE" "H/TS order" order - fi PH="$( ask py fetch 'How does the lookup cache fetch or evict an entry?' )" if ran_ok "$PH" "H/PY"; then - hopcheck "$PH" "H/PY untyped-parameter caller" nohop pull + nohop_or_skip "$PH" "H/PY untyped-parameter caller" nohop pull hopcheck "$PH" "H/PY near miss" hop fetch_entry hopcheck "$PH" "H/PY order" order - fi @@ -262,15 +286,24 @@ fi echo "(R) the defining code outranks the docs that describe it" if ran_ok "$TE" "R"; then python3 - "$TMP/rows.py" "$TE" <<'PY' -import sys +import re, sys exec( open( sys.argv[ 1 ] ).read() ) -rows = [ a for _, a in drows( load( sys.argv[ 2 ] ) ) if a.get( "r", "" ).isdigit() ] +doc = load( sys.argv[ 2 ] ) +rows = [ a for _, a in drows( doc ) if a.get( "r", "" ).isdigit() ] code = [ int( a[ "r" ] ) for a in rows if ( a.get( "n" ), a.get( "p" ) ) in ( ( "createContext", "src/application.ts" ), ( "Context", "src/context.ts" ) ) ] docs = [ int( a[ "r" ] ) for a in rows if is_doc_row( a ) ] bad = [] if len( code ) != 2: bad.append( "premise: createContext and class Context are not both shown (%s)" % code ) elif docs and max( code ) > min( docs ): bad.append( "a docs row r=%d ranks above defining code r=%d" % ( min( docs ), max( code ) ) ) -if rows and 4 * len( docs ) > len( rows ): bad.append( "%d of %d shown rows are docs (> 25%%)" % ( len( docs ), len( rows ) ) ) +# docs <= 25% of the shown rows — unless no code row could have been displaced: the window is not full (fewer rows than +# the 40-row lens head) and is not cut, so every positive code row is already shown ABOVE the docs (rule 5: a doc +# row filling a slot no code row competes for is information, never a cut). +sigs = re.search( r"]*>", doc ) +windowOpen = len( rows ) < 40 and sigs is not None and 'capped="1"' not in sigs.group( 0 ) +if code and docs and min( docs ) < max( r for r in ( int( a[ "r" ] ) for a in rows if not is_doc_row( a ) ) ): + bad.append( "a docs row r=%d ranks above a code row" % min( docs ) ) +if rows and 4 * len( docs ) > len( rows ) and not windowOpen: bad.append( "%d of %d shown rows are docs (> 25%%) in a full or cut window" % ( len( docs ), len( rows ) ) ) +if docs and " [docs after code: " not in doc: bad.append( "the docs moved below code but the answer has no [docs after code: N] note" ) if bad: for b in bad: print( " FAIL R: %s" % b ) sys.exit( 1 ) @@ -315,6 +348,34 @@ PY [ $? -eq 0 ] || fail=1 fi +echo "(M) the MCP for twin carries the same e= values, with the same exclusions" +MC="$TMP/mcp.c.json"; mkdir -p "$TMP/mcphome" +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":"'"$CORPUS/c"'","task":"How does the sampler compute the average reading window?"}}}' \ + | ( cd "$CORPUS/c" && TMPDIR="$TMP/mcphome" "$BIN" --mcp >"$MC" 2>/dev/null ); printf '%s' "$?" >"$MC.rc" +if ran_ok "$MC" "M"; then + python3 - "$TMP/rows.py" "$MC" "$CORPUS/c" <<'PY' +import json, os, sys +exec( open( sys.argv[ 1 ] ).read() ) +lines = [ l for l in open( sys.argv[ 2 ], encoding="utf-8", errors="replace" ) if l.strip() ] +try: + text = json.loads( lines[ -1 ] )[ "result" ][ "content" ][ 0 ][ "text" ] +except Exception as err: + print( " FAIL M: no readable MCP for answer (%s)" % err ); sys.exit( 1 ) +rows = { a.get( "n" ) + "@" + a.get( "p", "" ) + ":" + a.get( "l", "" ): a for _, a in drows( text ) } +want = { "Sampler_average@src/window.c:6": "14", "Sampler_scale@src/window.c:3": "3" } +bad = [ "%s e=%r, expected %s" % ( k, rows[ k ].get( "e" ) if k in rows else "(row absent)", v ) for k, v in want.items() if k not in rows or rows[ k ].get( "e" ) != v ] +bad += [ "%s is extent_suspect and carries e=" % k for k, a in rows.items() if "extent_suspect" in a and "e" in a ] +if "e=" not in " ".join( __import__( "re" ).findall( r"", text, __import__( "re" ).S ) ): + bad.append( "the MCP answer defines no e= in its legend" ) +if bad: + for b in bad: print( " FAIL M: %s" % b ) + sys.exit( 1 ) +print( " PASS M: MCP for rows carry the CLI's e= values; suspect rows none; legend defines e=" ) +PY + [ $? -eq 0 ] || fail=1 +fi + echo "(L) legend defines e= and what it does not mean" LG="$TMP/ts.legend.xml"; run ts "$LG" "--for=How is a per-request context created?" --legend=full if ran_ok "$LG" "L"; then From 051613dff0edf7f88aa79dabb1b503e59ab78d1c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 14:07:34 -0400 Subject: [PATCH 105/229] --for: code above docs is a reorder of the shown set, never an eviction The first cut demoted every doc row's SCORE below every code row, so a tight window or byte budget evicted docs rows plain score order would have shown (a docs row was what named the answer's topic). The rule now runs after the score order and the byte ladder chose the shown set: the same rows, code rows first and doc rows after, each group in its own order; the shown rows keep their own r= values, reassigned in the new order (a gap still marks a trimmed row, r=1 and its next= go to the first row shown). (JSON "docs_after_code") counts the moved doc rows, with a present-only reading; uncharged, like e=. RIPWIRE_NO_DOCS_AFTER_CODE=1 is the A/B handle the gate compares the row set against. forsigspancheck (R): same row set and r= multiset with and without the rule, at the default budget and at a budget that cuts , and a question whose topic only a docs row names keeps that row (red on the score-demotion build). The compact e= clause is 26 B shorter (the ten-verb compact legend bill). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 ++-- src/filter.h | 81 +++------------------ src/mcpverbs.h | 11 ++- src/serialize.h | 155 +++++++++++++++++++++++++++++++++++++--- src/verbs_for.h | 34 +++++---- test/forsigspancheck.sh | 98 ++++++++++++++++++------- 6 files changed, 254 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86872dd72..a4c738a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,12 +26,13 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli definition can start above `l=` (a return type on the line before, a decorator). The attribute is exempt from the signature-row byte budget, so a ceiling admits the same rows it admitted before; it costs about 8 bytes per row. `--pack-signatures`, `--pack-task`, `--from-trace` and the map keep their bytes. -- **Code above docs.** On a question that does not ask about docs, every scored markdown row now ranks after every - scored code row (the doc rows keep their own order and stay in the answer; they fill only slots no code row - competes for). A question that names docs (`doc…`, `readme`, `guide`, `tutorial`, `manual`, `markdown`, `wiki`, - `faq`, `howto`, a change or translation cue, or a named `.md`/`.rst` file) keeps the plain score order. The answer - says when the rule moved anything: ` [docs after code: N doc rows ranked below every code row; …]`. Routed path - only (`--no-route` restores the old order); `--for` and MCP `for` only. +- **Code above docs.** On a question that does not ask about docs, the code rows of the SHOWN signature set now come + first and its markdown rows after them. It is a reorder of the same rows: the score order and the byte budget still + choose which rows are shown, so no doc row is ever evicted by it; the shown rows keep their own `r=` values, + reassigned in the new order, and `` (JSON `"docs_after_code"`) says how many doc rows moved. + A question that names docs (`doc…`, `readme`, `guide`, `tutorial`, `manual`, `markdown`, `wiki`, `faq`, `howto`, a + change or translation cue, or a named `.md`/`.rst` file) keeps the plain score order, and so does `--no-route`; + `RIPWIRE_NO_DOCS_AFTER_CODE=1` turns the reorder off everywhere (CLI and MCP `for`). Gate: `forsigspancheck` (fixture `test/forcompletefix`, C/JS/Python/TS). diff --git a/src/filter.h b/src/filter.h index 2afd3aecb..ecc23f3c8 100644 --- a/src/filter.h +++ b/src/filter.h @@ -881,22 +881,17 @@ inline std::vector docNoiseSymbolMultipliers( const IngestResult& ing, st return mul; } -// ── CODE ABOVE DOCS (--for and MCP `for`, routed path only; gate test/forsigspancheck.sh (R)) ──────────────── -// Markdown headings that repeat a question's words used to fill the ranked rows ABOVE the code that defines the -// answer (on one graded answer about half of the shown rows were headings). A question about CODE now ranks every -// positive-score doc row after every positive-score non-doc row: the doc rows keep their own order and stay -// positive (nothing is dropped, they still fill any slot no code row competes for), only their place moves. -// A question that ASKS about docs keeps the plain score order: a docs cue (kDocsQuestionCues), a change or -// translation cue (the two doc-noise lists above: those questions are about prose too), or a named doc file. -// A false cue is the cheap side to be wrong on — it restores the old order. +// ── CODE ABOVE DOCS (--for and MCP `for`, routed path only; serialize.h reorderDocsAfterCode; gate forsigspancheck (R)) ── +// Markdown headings that repeat a question's words sat ABOVE the code that defines the answer in the ranked rows. A +// question about CODE now shows the code rows of its SHOWN set first and the doc rows after them — the same rows, only +// reordered: the score order and the byte ladder still choose which rows are shown, so the reorder never evicts a doc +// row. A question that ASKS about docs keeps the plain score order: a docs cue (kDocsQuestionCues), a change or +// translation cue (the doc-noise lists above: those questions are about prose too), or a named doc file. A false cue +// is the cheap side to be wrong on — it restores the old order. RIPWIRE_NO_DOCS_AFTER_CODE=1 is the A/B handle (the +// RIPWIRE_NO_DOC_MENTION precedent): the same answer in plain score order, which the gate compares the row SET against. inline constexpr DocNoiseCue kDocsQuestionCues[] = { { "doc", true }, { "readme", true }, { "guide", true }, { "tutorial", true }, { "manual", true }, { "markdown", false }, { "wiki", false }, { "faq", false }, { "howto", false } }; -// The scale that places the best doc row strictly below the weakest code row: half of it, so float rounding can -// never land a doc row ON a code row's score (the (score desc, id asc) order would then interleave them by id). -inline constexpr float kDocsAfterCodeGap = 0.5f; -static_assert( kDocsAfterCodeGap > 0.f && kDocsAfterCodeGap < 1.f, "doc rows land strictly below the weakest code row, and stay positive" ); - inline bool taskAsksAboutDocs( std::string_view task ) { const std::string lowerTask = queryshape::detail::lowerAscii( task ); @@ -908,66 +903,6 @@ inline bool taskAsksAboutDocs( std::string_view task ) || taskHasCue( lowerTask, kTranslationQuestionCues ); } -// Apply the rule to a final lens rank. Returns how many doc rows it MOVED (scored at or above the weakest code row -// before; 0 ⇒ nothing changed, byte-identical). The caller discloses a non-zero count in the bundle's note. -inline std::size_t applyCodeAboveDocs( const IngestResult& ing, std::string_view task, std::vector& rank ) -{ - EXPECTS( rank.size() <= ing.symbols.size(), "one score per symbol at most" ); - if( taskAsksAboutDocs( task ) ) - { - return 0; - } - float minCode = 0.f; - float maxDoc = 0.f; - bool anyCode = false; - for( std::size_t i = 0; i < rank.size(); ++i ) - { - if( rank[i] <= 0.f ) - { - continue; - } - if( ing.symbols[i].lang == Lang::Markdown ) - { - maxDoc = std::max( maxDoc, rank[i] ); - } - else if( !anyCode || rank[i] < minCode ) - { - minCode = rank[i]; - anyCode = true; - } - } - if( !anyCode || maxDoc <= 0.f || maxDoc < minCode ) - { - return 0; // no doc row, no code row, or every doc row already below every code row - } - const float scale = minCode * kDocsAfterCodeGap / maxDoc; // maxDoc >= minCode > 0 ⇒ 0 < scale <= kDocsAfterCodeGap - std::size_t moved = 0; - for( std::size_t i = 0; i < rank.size(); ++i ) - { - if( rank[i] > 0.f && ing.symbols[i].lang == Lang::Markdown ) - { - moved += rank[i] >= minCode ? 1u : 0u; - rank[i] = std::max( rank[i] * scale, std::numeric_limits::min() ); // stays positive: still a scored row - } - } - ENSURES( moved > 0, "maxDoc >= minCode, so the best doc row moved" ); - return moved; -} - -// The bundle note for a moved count (" [docs after code: N …]"), "" at zero — the same free-text channel the doc-mention -// note rides, so both dialects and the compact legend carry it verbatim. -inline std::string docsAfterCodeNote( std::size_t moved ) -{ - if( moved == 0 ) - { - return {}; - } - char nb[ 140 ]; - rw::formatTo( nb, sizeof( nb ), " [docs after code: {} doc row{} ranked below every code row; a question naming docs keeps score order]", - moved, moved == 1 ? "" : "s" ); - return nb; -} - // Order a file-id list by a per-id KEY descending, PATH ascending as the tiebreak — the shared // "most-consequential-first, and deterministically so" ordering the first-screen verbs need (§P11.7 // --pr-context by blast radius, §P11.8 --tree by best-symbol rank). Templated on the key because one of diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 2376a73bc..9471f4225 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1945,9 +1945,6 @@ inline std::optional forTaskText( const std::string& root, const st } absorbCapDisclosure( docMentionInfo.caps, docMentionNote, capAttrs ); } - // code above docs — the CLI twin's rule on the final rank (filter.h applyCodeAboveDocs); its note is sig-charge exempt below - const std::string mcpDocsAfterCode = noRoute ? std::string() : docsAfterCodeNote( applyCodeAboveDocs( ing, task, lensRank ) ); - docMentionNote += mcpDocsAfterCode; // LB-A (r10 §5) — THE RELEVANCE FLOOR, the CLI --for's own call (serialize.h relevanceFloorCut): one // bundle-composition contract may not have two behaviours. lensRank is final at this point. @@ -2193,8 +2190,8 @@ inline std::optional forTaskText( const std::string& root, const st // (the CLI twin's own idRouteParts ledger, verbs_for.h, for the identical reason). const std::size_t mcpIdRouteExemptBytes = mcpIdRouteParts.bytes(); const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above - // e= and docs-after-code: disclosures on the same contract (the CLI twin exempts the same two) - const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size() + mcpDocsAfterCode.size(); + // e=: a disclosure on the same contract (the CLI twin exempts the same clause) + const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size(); const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - mcpEndLineExemptBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" @@ -2220,7 +2217,7 @@ inline std::optional forTaskText( const std::string& root, const st sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p=, same argument the CLI twin passes - rw::SigLensRules::ForLens, // LB-A: shrink past the zero-score tail, never pad; e= rows + rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) ), // LB-A, e=, docs reorder &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) &mcpShownIds, // lane 2: see verbs_for.h shownSigIds &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below @@ -2265,7 +2262,7 @@ inline std::optional forTaskText( const std::string& root, const st } // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). - if( const std::string cutNotes = rw::sigsCutLegendNotes( mcpSigsCut.isCapped, mcpSigsCut.shown, mcpSigsCut.total, mcpSigsCut.docsDropped ); + if( const std::string cutNotes = rw::sigsCutLegendNotes( mcpSigsCut ); !cutNotes.empty() ) { const std::size_t closeAt = headerStr.rfind( " -->" ); diff --git a/src/serialize.h b/src/serialize.h index eafe10fec..5afab795f 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -865,6 +865,12 @@ inline constexpr std::string_view kForSigsShrunkNote = static_assert( kForDocExcerptRankCount == 24 && kForDocFullRankCount == 12, "kForDocsDroppedNote spells the tier thresholds (r>24; the ladder's r5..24): re-word it with these constants" ); +// The docs reorder's reading (reorderDocsAfterCode), present-only on a tag that carries docs_after_code=. Says what it is +// NOT: no row was added or removed. No "--" (it rides inside an XML comment, G4). +inline constexpr std::string_view kForDocsAfterCodeNote = + " [docs_after_code=N: N doc rows of the shown set moved below its code rows; same rows, reordered, none added or removed; " + "a question naming docs keeps score order]"; + // The clauses a cut report owes, concatenated in a fixed order ("" when it owes none). inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped ) { @@ -4270,7 +4276,7 @@ inline std::uint32_t defEndLine( const Symbol& s, const std::vector& entries, std::vect // comment the rank tiers or the ladder removed (the tag's docs_dropped=). isCapped mirrors the tag's capped="1". struct SigsCutReport { - std::size_t shown = 0; - std::size_t total = 0; - std::size_t docsDropped = 0; - bool isCapped = false; + std::size_t shown = 0; + std::size_t total = 0; + std::size_t docsDropped = 0; + bool isCapped = false; + std::size_t docsAfterCode = 0; // reorderDocsAfterCode's moved count: docs_after_code= (0 ⇒ absent) }; +// …the same clauses from a whole report, plus the docs reorder's reading (the --for and MCP `for` splices). +inline std::string sigsCutLegendNotes( const SigsCutReport& cut ) +{ + std::string notes = sigsCutLegendNotes( cut.isCapped, cut.shown, cut.total, cut.docsDropped ); + if( cut.docsAfterCode > 0 ) + { + notes += kForDocsAfterCodeNote; + } + return notes; +} + inline std::size_t sigsDecimalDigits( std::size_t n ) noexcept { std::size_t d = 1; @@ -4644,6 +4740,11 @@ inline std::string sigsOpenTag( const SigsCutReport& cut ) rw::formatTo( nb, sizeof( nb ), " docs_dropped=\"{}\"", cut.docsDropped ); tag += nb; } + if( cut.docsAfterCode > 0 ) // the docs reorder (reorderDocsAfterCode): uncharged, like e= — a disclosure never costs a row + { + rw::formatTo( nb, sizeof( nb ), " docs_after_code=\"{}\"", cut.docsAfterCode ); + tag += nb; + } tag += ">"; return tag; } @@ -4844,6 +4945,10 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // the shown rows where this is true and `doc` is empty at emission std::uint32_t endLine = 0; // e= (defEndLine), 0 = not printed; spliced at emission, OUTSIDE `head` and so // outside every byte the gate and the ladder charge (writeSigHeadWithEnd) + std::uint32_t displayRank = 0; // the r= this row is SHOWN with: globalRank, unless reorderDocsAfterCode moved it + NodeId id = 0; // the row's symbol, and the head's other inputs — re-rendered when displayRank moves + std::string lensRun; // the churn/amp/clone/tested attribute run (qbuf) + bool pureSig = false; }; std::vector sigFiles; std::vector entries; @@ -4976,6 +5081,10 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: e.positive = rank[id] > 0.0f; // A2: this symbol's own score, at collection time e.hadDoc = hadDoc; e.endLine = hasSigLensRule( lensRules, SigLensRules::EndLine ) ? defEndLine( s, lineBreaks, src.size() ) : 0u; + e.displayRank = globalRank; + e.id = id; + e.lensRun = qbuf; + e.pureSig = pureSig; entries.push_back( std::move( e ) ); ++sf.liveCount; } @@ -5066,7 +5175,15 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: { *cappedOut = plan.ladderFires; } - const SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + // CODE ABOVE DOCS: the shown set is final here (the ladder ran on score order); only its order may change + if( hasSigLensRule( lensRules, SigLensRules::DocsAfterCode ) ) + { + cut.docsAfterCode = reorderDocsAfterCode( entries, + [ & ]( const SigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, + [ & ]( SigEntry& e, std::uint32_t newRank ) + { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); } ); + } if( cutOut ) { *cutOut = cut; @@ -9031,6 +9148,9 @@ struct JsonSigEntry bool hadDoc = false; // the source has a doc comment here (before the rank tiers) — the XML sibling's field std::uint32_t endLine = 0; // "e" (defEndLine), 0 = no key; spliced at emission, outside `head` and every charged // byte (the XML sibling's SigEntry::endLine, same exemption) + std::uint32_t displayRank = 0; // the XML sibling's fields for the docs reorder: the "r" this row is shown with, + NodeId id = 0; // its symbol, and the head inputs a moved row is re-rendered from + bool pureSig = false; }; // §B1.3: how many notes this array-emitter matched, and how many survived the ladder — the caller pairs @@ -9103,6 +9223,8 @@ struct JsonSigLens bool endLines = false; // the XML sibling's SigLensRules::EndLine: an "e" // key (defEndLine) on every row whose extent is // known — --for only; off ⇒ byte-identical. + bool docsAfterCode = false; // the XML sibling's SigLensRules::DocsAfterCode + // (reorderDocsAfterCode) — --for only. }; // One row's `{"l":…` opening through its flag fields — everything EXCEPT doc/sig, which the ladder mutates @@ -9309,6 +9431,9 @@ inline std::size_t collectJsonSigEntries( const IngestResult& ing, const std::ve e.positive = rank && (*rank)[id] > 0.0f; // A2: the XML sibling's own field, same definition e.hadDoc = hadDoc; e.endLine = lens.endLines ? defEndLine( s, lineBreaks, src.size() ) : 0u; + e.displayRank = globalRank; + e.id = id; + e.pureSig = pureSig; outEntries.push_back( std::move( e ) ); ++sf.liveCount; } @@ -9454,9 +9579,19 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped } - if( cutOut ) { - *cutOut = sigsCutReportOf( entries, totalRows, plan ); + SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); + if( lens.docsAfterCode ) // the XML twin's reorder, after the ladder: the shown set is final, only its order changes + { + cut.docsAfterCode = reorderDocsAfterCode( entries, + [ & ]( const JsonSigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, + [ & ]( JsonSigEntry& e, std::uint32_t newRank ) + { e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); } ); + } + if( cutOut ) + { + *cutOut = cut; + } } if( droppedPositiveOut ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index b8981b798..2838a7b72 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -422,6 +422,8 @@ struct ForLensJsonInputs // MainDispatch) — REQUIRED like fileTail above, for the same reason: a defaulted bool is how a degrade // silently drops off this dialect while the XML twin keeps carrying it. bool notesDegraded; + // code above docs (serialize.h reorderDocsAfterCode): the XML twin's verdict, REQUIRED for the same reason + bool docsAfterCode; }; // The lens bundle's opening keys. Every note is absent-unless-present — the same silence-means-nothing- @@ -819,8 +821,8 @@ inline std::string compactOneForNote( std::string_view note ) return std::string( note ); } -// A note channel can carry SEVERAL notes back to back (the doc-mention channel also carries its cap note and the -// docs-after-code note, filter.h docsAfterCodeNote). Each " [...]" is compacted on its own, so a later note is never +// A note channel can carry SEVERAL notes back to back (the doc-mention channel also carries its cap note, +// mention.h absorbCapDisclosure). Each " [...]" is compacted on its own, so a later note is never // swallowed by an earlier note's rewrite (which keeps only the first note's numbers). inline std::string compactForNote( std::string_view note ) { @@ -1477,7 +1479,7 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, - in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true }; + in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true, in.docsAfterCode }; JsonSigNoteCounts noteCounts; const auto packSigs = [ & ]( std::FILE* dst, std::size_t budget, bool* outCapped, std::size_t* outDroppedPositive, std::vector* outShownIds, rw::SigsCutReport* outCut ) @@ -1580,6 +1582,10 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For { sigsCutStanza += ",\"docs_dropped\":" + std::to_string( sigsCut.docsDropped ); } + if( sigsCut.docsAfterCode > 0 ) // the XML tag's docs_after_code= (the reorder's disclosure; uncharged, like the XML attribute) + { + sigsCutStanza += ",\"docs_after_code\":" + std::to_string( sigsCut.docsAfterCode ); + } const bool sigsCappedKey = sigsCut.isCapped; // "capped": the XML tag's capped="1" (the ladder's verdict OR a gate cut) // §C1 + §C2 — the CHARGE, measured from the bytes this function is about to write rather than from the @@ -2285,11 +2291,9 @@ std::optional runForLens( const MainDispatch& d ) // --anchor: ROUTE picks the base lens rank, then ANCHOR expands it; mention/co-change run after. LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning - // code above docs (filter.h applyCodeAboveDocs): --for's own rule, routed path only, on the final rank. Its note rides - // the doc-mention channel and is exempt from the sig charge below (a disclosure never costs a row). - const std::string docsAfterCode = cfg.noRoute ? std::string() : docsAfterCodeNote( applyCodeAboveDocs( ing, cfg.forTask, lr.rank ) ); - lr.docMentionNote += docsAfterCode; std::vector lensRank = std::move( lr.rank ); + // code above docs (filter.h): the routed path on a question that does not ask about docs — the shown set's REORDER + const bool forDocsAfterCode = !cfg.noRoute && !taskAsksAboutDocs( cfg.forTask ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ); const std::string routeNoteRaw = std::move( lr.routeNote ); // verbatim; lands ONLY in route= (attribute-escaped) + the JSON twin — L1: the comment no longer echoes it const std::string mentionNote( std::move( lr.mentionNote ) ); const std::string boostNote( std::move( lr.boostNote ) ); @@ -2782,7 +2786,7 @@ std::optional runForLens( const MainDispatch& d ) &forClone, testedPtr, ampPtr, redactPtr, cfg.packBudgetBytes, cfg.tokenBudget, notesPtr, legoTotal, composeTotal, routesTotal, flRootArg, - &forFileTail, d.notesDegraded } ); + &forFileTail, d.notesDegraded, forDocsAfterCode } ); // §B0: this early return skipped the end-of-function tally below, so a --for --json run redacted // SILENTLY — the one stderr line that tells the user a secret was in their tree never appeared. reportRedactions( stderr, redactCounts ); @@ -2928,12 +2932,12 @@ std::optional runForLens( const MainDispatch& d ) const rw::ForIdRouteLegendParts idRouteParts = rw::forIdRouteLegendParts( headerParts.idRouteLegend, headerParts.scPresent, !routeNoteRaw.empty() ); const std::size_t idRouteLegendEmitted = compactLegendOn ? idRouteParts.route.size() : idRouteParts.bytes(); - // e= (forsigspancheck): its legend clause and the docs-after-code note are disclosures on the same contract — exempt, - // so the ranked rows a ceiling admits are the ones it admitted without them (owner ruling: e= is exempt from the row budget). + // e= (forsigspancheck): its legend clause is a disclosure on the same contract — exempt, so the ranked rows a ceiling + // admits are the ones it admitted without it (owner ruling: e= is exempt from the row budget). const std::size_t endLineLegendEmitted = !headerParts.endLinePresent ? 0u : ( compactLegendOn ? rw::kForCompactEndLineLegend.size() : rw::kForEndLineLegend.size() ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted - + endLineLegendEmitted + docsAfterCode.size(); + + endLineLegendEmitted; if( exemptBytes > headerStr.size() ) { DISCLOSE( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -2954,7 +2958,7 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() - + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ) + docsAfterCode.size(); + + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } @@ -3026,7 +3030,7 @@ std::optional runForLens( const MainDispatch& d ) sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p= - rw::SigLensRules::ForLens, // LB-A: shrink past the zero-score tail, never pad; e= on every row + rw::forLensRules( forDocsAfterCode ), // LB-A floor, e= on every row, and the code-above-docs reorder &forDroppedPositive, // A2: exact count, see droppedPositiveCount &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then @@ -3110,7 +3114,7 @@ std::optional runForLens( const MainDispatch& d ) // cut-fix lane A: the tag's two cut readings (docs_dropped=, shrunk-not-dropped) ride the same splice, present // only when the tag carries the case (serialize.h sigsCutLegendNotes), so the reserve below already covers them. const std::string sigsCeilingNote = ( forDefaultCeiling ? std::string( rw::kForBudgetBytesNote ) : std::string() ) - + rw::sigsCutLegendNotes( forSigsCut.isCapped, forSigsCut.shown, forSigsCut.total, forSigsCut.docsDropped ); + + rw::sigsCutLegendNotes( forSigsCut ); // + the docs reorder's reading (docs_after_code=) // ── the INDEXING-cap disclosure (mention.h CapDisclosure), at the same splice point and for the // same reason: a --for header is charged against the payload ceiling, so a disclosure folded into // the notes above is paid for in ranked rows. Measured on sixteen real invocations, that cost three @@ -3512,7 +3516,7 @@ std::optional runForLens( const MainDispatch& d ) { packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, - rw::SigLensRules::ForLens, nullptr, nullptr, nullptr, // LB-A + e=: the direct-emission degrade path selects identically + rw::forLensRules( forDocsAfterCode ), nullptr, nullptr, nullptr, // LB-A + e= + docs reorder: the degrade path selects identically forTopRowNext ); // L-W: same next= rule on the degrade path } if( legoPreRendered ) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index ccbcccfe0..d26edd012 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -43,11 +43,13 @@ # keeps one. C near miss — Sampler_collectAll keeps its row. rows follow the rows' r= order. # The two "no row" arms need the receiver-evidence hedge (a name-only row marked via="name"); on a binary # without it they SKIP by name ("expects FE-B"), detected on this fixture — never a silent PASS. -# (R) code over docs: TS "How is a per-request context created?" — createContext and the class Context, and every -# other code row, rank above every docs/context.md row; md rows <= 25% of the shown rows unless no code row was -# displaced (the window holds fewer than 40 rows and is not cut); the answer carries the -# " [docs after code: N …]" note. Twin: "Where do the docs describe the context lifecycle?" keeps a docs row in -# its top five. +# (R) code above docs as a REORDER of the shown set (coordinator rule: it reorders, never shrinks the doc rows shown): +# the TS questions answered with the rule and with RIPWIRE_NO_DOCS_AFTER_CODE=1 (plain score order) show the +# same row SET and r= multiset; with the rule every code row precedes every doc row, rows stay in r= order, and +# counts the doc rows that moved (with its reading). Near misses: the same at a +# --token-budget that cuts (a demotion would evict docs there: red on the score-demotion build), and a +# question whose topic ("dropped after the response") only a docs row names keeps that row. Twin: "Where do the +# docs describe the context lifecycle?" keeps a docs row in its top five. # (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). # (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. # (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's @@ -283,34 +285,72 @@ if ran_ok "$CH" "H/C"; then hopcheck "$CH" "H/C near miss" hop Sampler_collectAll fi -echo "(R) the defining code outranks the docs that describe it" -if ran_ok "$TE" "R"; then - python3 - "$TMP/rows.py" "$TE" <<'PY' +echo "(R) code above docs: the SHOWN set reordered, never shrunk" +# rset ANSWER — the shown rows as sorted "n|p|l" lines (the SET), and a line "DOCS k" with the doc-row count +# rcheck LABEL ON OFF [premise_doc_row] — ON (the rule) and OFF (RIPWIRE_NO_DOCS_AFTER_CODE=1, plain score order) show the +# same row SET and the same doc rows; in ON every code row precedes every doc row; the r= values are the same multiset; +# ON carries docs_after_code=N exactly when it moved N doc rows above which OFF had a code row below; the reading rides. +rcheck(){ + python3 - "$TMP/rows.py" "$@" <<'PY' import re, sys exec( open( sys.argv[ 1 ] ).read() ) -doc = load( sys.argv[ 2 ] ) -rows = [ a for _, a in drows( doc ) if a.get( "r", "" ).isdigit() ] -code = [ int( a[ "r" ] ) for a in rows if ( a.get( "n" ), a.get( "p" ) ) in ( ( "createContext", "src/application.ts" ), ( "Context", "src/context.ts" ) ) ] -docs = [ int( a[ "r" ] ) for a in rows if is_doc_row( a ) ] +label, on, off = sys.argv[ 2: 5 ] +need = sys.argv[ 5 ] if len( sys.argv ) > 5 else None +don, doff = load( on ), load( off ) +if not ctx_ok( don ) or not ctx_ok( doff ): + print( " FAIL %s: NOROOT" % label ); sys.exit( 1 ) +ron = [ a for _, a in drows( don ) ] +roff = [ a for _, a in drows( doff ) ] +key = lambda a: ( a.get( "n" ), a.get( "p" ), a.get( "l" ) ) bad = [] -if len( code ) != 2: bad.append( "premise: createContext and class Context are not both shown (%s)" % code ) -elif docs and max( code ) > min( docs ): bad.append( "a docs row r=%d ranks above defining code r=%d" % ( min( docs ), max( code ) ) ) -# docs <= 25% of the shown rows — unless no code row could have been displaced: the window is not full (fewer rows than -# the 40-row lens head) and is not cut, so every positive code row is already shown ABOVE the docs (rule 5: a doc -# row filling a slot no code row competes for is information, never a cut). -sigs = re.search( r"]*>", doc ) -windowOpen = len( rows ) < 40 and sigs is not None and 'capped="1"' not in sigs.group( 0 ) -if code and docs and min( docs ) < max( r for r in ( int( a[ "r" ] ) for a in rows if not is_doc_row( a ) ) ): - bad.append( "a docs row r=%d ranks above a code row" % min( docs ) ) -if rows and 4 * len( docs ) > len( rows ) and not windowOpen: bad.append( "%d of %d shown rows are docs (> 25%%) in a full or cut window" % ( len( docs ), len( rows ) ) ) -if docs and " [docs after code: " not in doc: bad.append( "the docs moved below code but the answer has no [docs after code: N] note" ) +if sorted( map( key, ron ) ) != sorted( map( key, roff ) ): + lost = sorted( set( map( key, roff ) ) - set( map( key, ron ) ) ) + bad.append( "the shown set changed (rows only in plain score order: %s)" % lost[ :4 ] ) +if sorted( a.get( "r" ) for a in ron ) != sorted( a.get( "r" ) for a in roff ): + bad.append( "the r= values are not the same multiset" ) +kinds = [ is_doc_row( a ) for a in ron ] +if True in kinds and False in kinds and kinds.index( True ) < len( kinds ) - 1 - kinds[ ::-1 ].index( False ): + bad.append( "a doc row precedes a code row in the reordered answer" ) +rs = [ int( a[ "r" ] ) for a in ron if a.get( "r", "" ).isdigit() ] +if rs != sorted( rs ): + bad.append( "rows are not in r= order" ) +koff = [ is_doc_row( a ) for a in roff ] +lastCode = max( ( i for i, d in enumerate( koff ) if not d ), default=-1 ) +moved = sum( 1 for i, d in enumerate( koff ) if d and i < lastCode ) +sig = re.search( r"]*>", strip_comments( don ) ) +attr = re.search( r' docs_after_code="([0-9]+)"', sig.group( 0 ) ) if sig else None +if moved and ( not attr or int( attr.group( 1 ) ) != moved ): + bad.append( "docs_after_code= is %s, expected %d" % ( attr.group( 1 ) if attr else "absent", moved ) ) +if not moved and attr: + bad.append( "docs_after_code= present though nothing moved" ) +if moved and "docs_after_code=N:" not in don: + bad.append( "the answer carries docs_after_code= without its reading" ) +if need and not [ a for a in ron if a.get( "n" ) == need and is_doc_row( a ) ]: + bad.append( "premise: the doc row %r is not shown" % need ) if bad: - for b in bad: print( " FAIL R: %s" % b ) + for b in bad: print( " FAIL %s: %s" % ( label, b ) ) sys.exit( 1 ) -print( " PASS R: code r=%s above docs r=%s; docs %d of %d rows" % ( code, docs, len( docs ), len( rows ) ) ) +print( " PASS %s: same %d rows (%d docs), code first, docs_after_code=%s" % ( label, len( ron ), kinds.count( True ), attr.group( 1 ) if attr else "-" ) ) PY - [ $? -eq 0 ] || fail=1 -fi + [ $? -eq 0 ] || { fail=1; return 1; } +} +# offr ROOT TAG QUESTION [ARGS…] — the same question in plain score order (the A/B handle) +offr(){ local r="$1" t="$2" q="$3"; shift 3; local f="$TMP/$r.$t.off.xml"; ( cd "$CORPUS/$r" && RIPWIRE_NO_DOCS_AFTER_CODE=1 "$BIN" . --no-cache "--for=$q" "$@" >"$f" 2>"$f.err" ); printf '%s' "$?" >"$f.rc"; printf '%s' "$f"; } +onr(){ local r="$1" t="$2" q="$3"; shift 3; local f="$TMP/$r.$t.on.xml"; run "$r" "$f" "--for=$q" "$@"; printf '%s' "$f"; } +Q1='How is a per-request context created?' +Q2='How is a per-request context created and dropped after the response?' # only the docs lifecycle section names "dropped after the response" +for spec in "r1|$Q1|" "r1b|$Q1|--token-budget=700" "r2|$Q2|" "r2b|$Q2|--token-budget=700"; do + tag="${spec%%|*}"; rest="${spec#*|}"; q="${rest%%|*}"; extra="${rest#*|}" + if [ -n "$extra" ]; then ON="$( onr ts "$tag" "$q" "$extra" )"; OFF="$( offr ts "$tag" "$q" "$extra" )"; else ON="$( onr ts "$tag" "$q" )"; OFF="$( offr ts "$tag" "$q" )"; fi + if ran_ok "$ON" "R/$tag" && ran_ok "$OFF" "R/$tag (plain order)"; then + case "$tag" in + r2) rcheck "R/$tag only a docs row names the topic: it stays" "$ON" "$OFF" "Context lifecycle" ;; + *) rcheck "R/$tag${extra:+ ($extra)}" "$ON" "$OFF" ;; + esac + fi +done +# the cut premise: the tight budget really cuts (else the arm above proves nothing about eviction) +if grep -q ']*capped="1"' "$TMP/ts.r1b.on.xml" 2>/dev/null; then ok "R/r1b premise: --token-budget=700 cuts the block"; else no "R/r1b premise: --token-budget=700 does not cut — pick a tighter budget"; fi TD="$( ask ts docs 'Where do the docs describe the context lifecycle?' )" if ran_ok "$TD" "R twin"; then python3 - "$TMP/rows.py" "$TD" <<'PY' @@ -404,6 +444,10 @@ printf 'x< if ( hopcheck "$K" "K4 (expected FAIL)" nohop contextHost ) >/dev/null 2>&1; then no "K4: a name-only hop row not caught"; else ok "K4: a name-only hop row is caught"; fi printf '' >"$K" if ( oracle "$K" "K5 (expected FAIL)" "$CORPUS/ts" ) >/dev/null 2>&1; then no "K5: NOROOT passed"; else ok "K5: a document with no --for root is never a pass"; fi +K6ON="$TMP/k6on.xml"; K6OFF="$TMP/k6off.xml" +printf 'x' >"$K6ON" +printf 'xx' >"$K6OFF" +if ( rcheck "K6 (expected FAIL)" "$K6ON" "$K6OFF" ) >/dev/null 2>&1; then no "K6: an evicted doc row not caught"; else ok "K6: a doc row the reorder evicted is caught"; fi if [ "$fail" -ne 0 ]; then echo "forsigspancheck: FAIL" From 29bb986faee0174f11f069707bbbf8051b41a833 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 16:33:04 -0400 Subject: [PATCH 106/229] --for: e= is charged under an explicit ceiling; re-pin three --for byte pins Two e= regimes. At the default ceiling e= stays exempt from the row budget (the ranked set is the one it was without e=). Under an explicit ceiling the reader asked for (--token-budget, a body ceiling, MCP budget_tokens) the answer promises est_tokens <= that budget, so e= and its legend clause are charged like every other byte (SigLensRules::ChargeEndLine) and the ladder's usual capped=/shown=/total= covers any row they cost (fornotesbudgetcheck). By-design re-pins, each checked mechanically (removing e=, its legend clause, est_tokens and the docs reorder leaves the old bytes): docdemotecheck's two --for goldens, compactlegendcheck's A-PIN for.xml. Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 5 +- src/serialize.h | 89 ++++++++++++++++++----- src/verbs_for.h | 12 +-- test/compactlegendcheck.sh | 5 ++ test/compactlegendfix/pre_l1_full/for.xml | 2 +- test/docdemotecheck.sh | 7 ++ test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- 8 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 9471f4225..89f724868 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2191,7 +2191,7 @@ inline std::optional forTaskText( const std::string& root, const st const std::size_t mcpIdRouteExemptBytes = mcpIdRouteParts.bytes(); const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above // e=: a disclosure on the same contract (the CLI twin exempts the same clause) - const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size(); + const std::size_t mcpEndLineExemptBytes = budgetTokens > 0 ? 0u : mcpEndLineLegend.size(); // charged under budget_tokens const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - mcpEndLineExemptBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" @@ -2217,7 +2217,8 @@ inline std::optional forTaskText( const std::string& root, const st sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p=, same argument the CLI twin passes - rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) ), // LB-A, e=, docs reorder + rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ), + /*explicitCeiling=*/budgetTokens > 0 ), // LB-A, e=, docs reorder &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) &mcpShownIds, // lane 2: see verbs_for.h shownSigIds &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below diff --git a/src/serialize.h b/src/serialize.h index 5afab795f..8b7ad78d3 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4282,9 +4282,23 @@ inline constexpr std::string_view kForEndLineLegend = "(extent_suspect, docs, config), never 0; l= is the line of the definition's name, so a definition can start above l="; // The " e=\"N\"" run, spliced into a rendered head right after its l= value (the head always opens ` 0.0f; // A2: this symbol's own score, at collection time e.hadDoc = hadDoc; e.endLine = hasSigLensRule( lensRules, SigLensRules::EndLine ) ? defEndLine( s, lineBreaks, src.size() ) : 0u; + e.endCharged = hasSigLensRule( lensRules, SigLensRules::ChargeEndLine ); + if( e.endCharged ) + { + spliceEndLine( e.head, e.endLine ); // an explicit ceiling: e= is part of the charged head + } e.displayRank = globalRank; e.id = id; e.lensRun = qbuf; @@ -5182,7 +5208,13 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: cut.docsAfterCode = reorderDocsAfterCode( entries, [ & ]( const SigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, [ & ]( SigEntry& e, std::uint32_t newRank ) - { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); } ); + { + e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); + if( e.endCharged ) + { + spliceEndLine( e.head, e.endLine ); + } + } ); } if( cutOut ) { @@ -5211,7 +5243,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: continue; } pushShownSigId( shownIdsOut, order, e.globalRank ); // lane 2 - writeSigHeadWithEnd( w, e.head, e.endLine ); // e= after l=, when the lens asked and the extent is known + writeSigHeadWithEnd( w, e.head, e.endCharged ? 0u : e.endLine ); // e= after l= (already in the head when charged) if( !e.doc.empty() ) { w.write( "" ); w.write( escapeXml( e.doc, esc ) ); w.write( "" ); } w.write( escapeXml( e.sig, esc ) ); w.write( e.notes ); // L3: symbol notes on this (inert when null) @@ -9225,8 +9257,25 @@ struct JsonSigLens // known — --for only; off ⇒ byte-identical. bool docsAfterCode = false; // the XML sibling's SigLensRules::DocsAfterCode // (reorderDocsAfterCode) — --for only. + bool chargeEndLines = false; // SigLensRules::ChargeEndLine: "e" inside the charged + // head (an explicit ceiling's regime, forLensRules). }; +// "e" right after "l" in a JSON row head (the XML sibling's spliceEndLine; the head always opens {"l":N). 0 ⇒ untouched. +inline void spliceJsonEndLine( std::string& head, std::uint32_t endLine ) +{ + if( endLine == 0 ) + { + return; + } + ASSUME( head.starts_with( "{\"l\":" ) ); + const std::size_t lEnd = head.find( ',' ); + ASSUME( lEnd != std::string::npos ); + char eKey[ 24 ]; + rw::formatTo( eKey, sizeof( eKey ), ",\"e\":{}", endLine ); + head.insert( lEnd, eKey ); +} + // One row's `{"l":…` opening through its flag fields — everything EXCEPT doc/sig, which the ladder mutates // and phase 2 appends. Mirrors sigRowHead()'s role on the XML side. inline std::string jsonSigRowHead( const IngestResult& ing, NodeId id, std::uint32_t fileId, @@ -9431,6 +9480,10 @@ inline std::size_t collectJsonSigEntries( const IngestResult& ing, const std::ve e.positive = rank && (*rank)[id] > 0.0f; // A2: the XML sibling's own field, same definition e.hadDoc = hadDoc; e.endLine = lens.endLines ? defEndLine( s, lineBreaks, src.size() ) : 0u; + if( lens.chargeEndLines ) + { + spliceJsonEndLine( e.head, e.endLine ); // an explicit ceiling: "e" is part of the charged head + } e.displayRank = globalRank; e.id = id; e.pureSig = pureSig; @@ -9586,7 +9639,13 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s cut.docsAfterCode = reorderDocsAfterCode( entries, [ & ]( const JsonSigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, [ & ]( JsonSigEntry& e, std::uint32_t newRank ) - { e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); } ); + { + e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); + if( lens.chargeEndLines ) + { + spliceJsonEndLine( e.head, e.endLine ); + } + } ); } if( cutOut ) { @@ -9626,15 +9685,9 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } first = false; std::string row = e.head; - if( e.endLine > 0 ) - { - // "e" right after "l" — the XML sibling's e= position (writeSigHeadWithEnd); the head always opens {"l":N - ASSUME( row.starts_with( "{\"l\":" ) ); - const std::size_t lEnd = row.find( ',' ); - ASSUME( lEnd != std::string::npos ); - char eKey[ 24 ]; - rw::formatTo( eKey, sizeof( eKey ), ",\"e\":{}", e.endLine ); - row.insert( lEnd, eKey ); + if( !lens.chargeEndLines ) + { + spliceJsonEndLine( row, e.endLine ); // the default ceiling: "e" added here, outside every charged byte } if( !e.doc.empty() ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index 2838a7b72..736f8f2d2 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1479,7 +1479,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, - in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true, in.docsAfterCode }; + in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true, in.docsAfterCode, + /*chargeEndLines=*/in.tokenBudget > 0 }; // an explicit ceiling charges "e" (forLensRules) JsonSigNoteCounts noteCounts; const auto packSigs = [ & ]( std::FILE* dst, std::size_t budget, bool* outCapped, std::size_t* outDroppedPositive, std::vector* outShownIds, rw::SigsCutReport* outCut ) @@ -2507,6 +2508,7 @@ std::optional runForLens( const MainDispatch& d ) // 2% of a 1550-token ceiling. Gate: test/budgetpolicycheck.sh arm (B). const bool forGateBudget = cfg.tokenBudget > 0; const bool forBodyCeiling = cfg.maxTokens > 0 && cfg.detail > 0; // the kShapingVerbs carve-out, read once + const bool forExplicitCeiling = forGateBudget || forBodyCeiling; // e= is CHARGED under one (serialize.h forLensRules) if( forGateBudget ) { forConf.attrs += " budget_tokens=\"" + std::to_string( cfg.tokenBudget ) + "\""; @@ -2934,7 +2936,7 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t idRouteLegendEmitted = compactLegendOn ? idRouteParts.route.size() : idRouteParts.bytes(); // e= (forsigspancheck): its legend clause is a disclosure on the same contract — exempt, so the ranked rows a ceiling // admits are the ones it admitted without it (owner ruling: e= is exempt from the row budget). - const std::size_t endLineLegendEmitted = !headerParts.endLinePresent ? 0u + const std::size_t endLineLegendEmitted = !headerParts.endLinePresent || forExplicitCeiling ? 0u // charged under an explicit ceiling : ( compactLegendOn ? rw::kForCompactEndLineLegend.size() : rw::kForEndLineLegend.size() ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted + endLineLegendEmitted; @@ -2958,7 +2960,7 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() - + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ); + + ( headerParts.endLinePresent && !forExplicitCeiling ? rw::kForEndLineLegend.size() : 0u ); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } @@ -3030,7 +3032,7 @@ std::optional runForLens( const MainDispatch& d ) sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p= - rw::forLensRules( forDocsAfterCode ), // LB-A floor, e= on every row, and the code-above-docs reorder + rw::forLensRules( forDocsAfterCode, forExplicitCeiling ), // LB-A floor, e= rows, docs reorder &forDroppedPositive, // A2: exact count, see droppedPositiveCount &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then @@ -3516,7 +3518,7 @@ std::optional runForLens( const MainDispatch& d ) { packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, - rw::forLensRules( forDocsAfterCode ), nullptr, nullptr, nullptr, // LB-A + e= + docs reorder: the degrade path selects identically + rw::forLensRules( forDocsAfterCode, forExplicitCeiling ), nullptr, nullptr, nullptr, // the degrade path selects identically forTopRowNext ); // L-W: same next= rule on the degrade path } if( legoPreRendered ) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 9a103f962..0df1a6b6c 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -288,6 +288,11 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # outside-import/use,super-past-the-tree;...)" (was "(builtin/stdlib-name-...,external-import,...)"), +106 B — and the # est_tokens= that prices them, 858 -> 901 (header and root). Checked: the new output differs from the pin by exactly # those two clauses and that figure; the six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-04 (lane for-spine-span-068, by design), for.xml only: the four rows gain e="N" (the +# definition's last line) right after l=, the full legend gains the present-only e= clause ("; e= on a d row: the 1-based +# line where that definition ends, …", 221 B), and est_tokens= moves 1580 -> 1683 with the bytes (3937 -> 4194 B). +# Checked: removing the four e= attributes and that clause and masking est_tokens leaves the pin byte-identical (at= +# masked); no docs_after_code (the fixture has no markdown row). The six other pins are unchanged. PIN_DIR="$ROOT/test/compactlegendfix/pre_l1_full" # the one normalisation, in python on BOTH sides so no sed dialect decides it (BSD sed appends a final newline, GNU # sed does not): at="…" masked, trailing newlines dropped. diff --git a/test/compactlegendfix/pre_l1_full/for.xml b/test/compactlegendfix/pre_l1_full/for.xml index 60a453fec..94a780efd 100644 --- a/test/compactlegendfix/pre_l1_full/for.xml +++ b/test/compactlegendfix/pre_l1_full/for.xml @@ -1 +1 @@ -double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file +double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 31bc84c78..00ea79246 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,13 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-04 (lane for-spine-span-068, by design): docdemotegolden_for.xml 5821 -> 6268 B and +# docdemotegolden_noroute.xml 9423 -> 9695 B. Every row with a known extent gains e="N" right after l= (5 and 6 +# rows), the full legend gains the e= clause, est_tokens moves with the bytes. The routed golden also shows the +# code-above-docs REORDER: the same 14 rows, flush_cache (r=7 -> r=5) now above the two notes/paging_design.md rows, +# and its reading. --no-route has no reorder (the rule is routed-path only). Checked +# mechanically: removing e=, est_tokens=, the e= clause and the docs_after_code attribute/reading leaves the no-route +# golden byte-identical and the routed one differing only in that row order. Gate: test/forsigspancheck.sh. # RE-PIN 2026-09-13 (merge of lane/sc-legend and lane/for-widen): docdemotegolden_for.xml RE-MEASURED on the # MERGED tree at 5,809 B (est_tokens "2328"), from 5,887 on for-widen's tree and 5,425 on sc-legend's. Neither # lane's own number is the merged one, so this is measured, not summed. Three identified changes, and the golden diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 06e898765..bbfeef830 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/docdemotegolden_noroute.xml b/test/docdemotegolden_noroute.xml index 9a699e69c..ac7fd28a7 100644 --- a/test/docdemotegolden_noroute.xml +++ b/test/docdemotegolden_noroute.xml @@ -1,4 +1,4 @@ -# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ): Date: Sun, 4 Oct 2026 17:28:07 -0400 Subject: [PATCH 107/229] --for: no e= under an explicit ceiling tighter than the default sig budget Supersedes the charged regime of the previous commit. e= stays exempt from the row budget wherever the sig side holds its default share (no explicit ceiling, or one at or above kForPayloadBudgetBytes, where is frozen at the default regime's bytes and the rest of the bundle is funded from the rendered sigs). Under a tighter explicit ceiling or a body ceiling the answer carries no e= at all, no row and no legend clause, and is the answer it was before: exempting e= there broke the est_tokens <= budget promise (fornotesbudgetcheck), charging it cost rows (compactlegendcheck (P4): the compact header leaves no slack). One predicate, endLinesFitCeiling, for the CLI, --json and MCP for. forsigspancheck (B): none at --token-budget=1200, present at 8000. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 ++- src/mcpverbs.h | 6 +-- src/serialize.h | 84 ++++++++++++----------------------------- src/verbs_for.h | 17 +++++---- test/forsigspancheck.sh | 19 ++++++++++ 5 files changed, 60 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c738a5d..74549f169 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,10 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli is ABSENT — never 0, never a guess — when the extent is not known: a markdown or config row, a module-scope row, a row flagged `extent_suspect=`, or a span the file's current bytes cannot hold. `l=` keeps its meaning, so a definition can start above `l=` (a return type on the line before, a decorator). The attribute is exempt from the - signature-row byte budget, so a ceiling admits the same rows it admitted before; it costs about 8 bytes per row. + signature-row byte budget, so the default ceiling admits the same rows it admitted before; it costs about 8 bytes + per row. An answer whose explicit ceiling is TIGHTER than the default signature budget (`--token-budget` below it, a + body ceiling, MCP `budget_tokens` likewise) carries no `e=`: its est_tokens promise and its rows stay exactly what they + were. A ceiling at or above the default serves the default's ``, `e=` included. `--pack-signatures`, `--pack-task`, `--from-trace` and the map keep their bytes. - **Code above docs.** On a question that does not ask about docs, the code rows of the SHOWN signature set now come first and its markdown rows after them. It is a reorder of the same rows: the score order and the byte budget still diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 89f724868..f751aac68 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2119,7 +2119,7 @@ inline std::optional forTaskText( const std::string& root, const st bool mcpForEndLinePresent = false; for( std::size_t i = 0; i < ing.symbols.size() && !mcpForEndLinePresent; ++i ) { - mcpForEndLinePresent = lensRank[i] > 0 && rw::mayCarryEndLine( ing.symbols[i] ); + mcpForEndLinePresent = rw::endLinesFitCeiling( budgetTokens, false ) && lensRank[i] > 0 && rw::mayCarryEndLine( ing.symbols[i] ); } const std::string_view mcpEndLineLegend = mcpForEndLinePresent ? rw::kForEndLineLegend : std::string_view(); // ONE decision, read twice below: appended into the header here, subtracted from the sigs charge there. @@ -2191,7 +2191,7 @@ inline std::optional forTaskText( const std::string& root, const st const std::size_t mcpIdRouteExemptBytes = mcpIdRouteParts.bytes(); const std::size_t mcpAtLegendExemptBytes = !flRootArg.empty() && !mcpForAtAttrStr.empty() ? rw::kForAtStampProse.size() : 0; // r2-LO, above // e=: a disclosure on the same contract (the CLI twin exempts the same clause) - const std::size_t mcpEndLineExemptBytes = budgetTokens > 0 ? 0u : mcpEndLineLegend.size(); // charged under budget_tokens + const std::size_t mcpEndLineExemptBytes = mcpEndLineLegend.size(); const std::size_t fixedBytes = headerStr.size() - rw::kForFileTailLegend.size() - mcpConfidenceExemptBytes - mcpIdRouteExemptBytes - mcpAtLegendExemptBytes - mcpEndLineExemptBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" const std::size_t sigsBudget = forBudgetBytes > fixedBytes ? forBudgetBytes - fixedBytes : 1; // ≥1: 0 = "no budget" @@ -2218,7 +2218,7 @@ inline std::optional forTaskText( const std::string& root, const st notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p=, same argument the CLI twin passes rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ), - /*explicitCeiling=*/budgetTokens > 0 ), // LB-A, e=, docs reorder + rw::endLinesFitCeiling( budgetTokens, /*bodyCeiling=*/false ) ), // LB-A, e=, docs reorder &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) &mcpShownIds, // lane 2: see verbs_for.h shownSigIds &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below diff --git a/src/serialize.h b/src/serialize.h index 8b7ad78d3..15946a011 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4282,23 +4282,9 @@ inline constexpr std::string_view kForEndLineLegend = "(extent_suspect, docs, config), never 0; l= is the line of the definition's name, so a definition can start above l="; // The " e=\"N\"" run, spliced into a rendered head right after its l= value (the head always opens ` bytes, so the +// exemption costs nothing but its bytes wherever the sig side holds its DEFAULT share: no explicit ceiling, or one at or +// above kForPayloadBudgetBytes (the sig side is then frozen at the default regime's bytes — forbudgetmonotoncheck #1). +// A TIGHTER explicit ceiling (--token-budget below that share, MCP budget_tokens likewise) or a body ceiling promises +// est_tokens <= the budget: exempt, e= broke that promise; charged, it cost rows (the compact dialect's header leaves no +// slack — compactlegendcheck (P4)). There the answer carries no e= at all — no row and no legend clause — and is the +// answer it was before e= existed: a row is worth more than the end line of the rows around it. +inline constexpr bool endLinesFitCeiling( std::size_t budgetTokens, bool bodyCeiling ) noexcept { - return SigLensRules( std::uint8_t( SigLensRules::ForLens ) | ( docsAfterCode ? std::uint8_t( SigLensRules::DocsAfterCode ) : 0u ) - | ( explicitCeiling ? std::uint8_t( SigLensRules::ChargeEndLine ) : 0u ) ); + return !bodyCeiling && ( budgetTokens == 0 || budgetBytesForTokens( budgetTokens ) >= kForPayloadBudgetBytes ); } // ── CODE ABOVE DOCS, AS A REORDER OF THE SHOWN SET (filter.h CODE ABOVE DOCS; gate test/forsigspancheck.sh (R)) ───────── @@ -4965,7 +4958,6 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // the shown rows where this is true and `doc` is empty at emission std::uint32_t endLine = 0; // e= (defEndLine), 0 = not printed; spliced at emission, OUTSIDE `head` and so // outside every byte the gate and the ladder charge (writeSigHeadWithEnd) - bool endCharged = false; // e= already inside `head` (ChargeEndLine), so emission must not splice it again std::uint32_t displayRank = 0; // the r= this row is SHOWN with: globalRank, unless reorderDocsAfterCode moved it NodeId id = 0; // the row's symbol, and the head's other inputs — re-rendered when displayRank moves std::string lensRun; // the churn/amp/clone/tested attribute run (qbuf) @@ -5102,11 +5094,6 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: e.positive = rank[id] > 0.0f; // A2: this symbol's own score, at collection time e.hadDoc = hadDoc; e.endLine = hasSigLensRule( lensRules, SigLensRules::EndLine ) ? defEndLine( s, lineBreaks, src.size() ) : 0u; - e.endCharged = hasSigLensRule( lensRules, SigLensRules::ChargeEndLine ); - if( e.endCharged ) - { - spliceEndLine( e.head, e.endLine ); // an explicit ceiling: e= is part of the charged head - } e.displayRank = globalRank; e.id = id; e.lensRun = qbuf; @@ -5208,13 +5195,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: cut.docsAfterCode = reorderDocsAfterCode( entries, [ & ]( const SigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, [ & ]( SigEntry& e, std::uint32_t newRank ) - { - e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); - if( e.endCharged ) - { - spliceEndLine( e.head, e.endLine ); - } - } ); + { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); } ); } if( cutOut ) { @@ -5243,7 +5224,7 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: continue; } pushShownSigId( shownIdsOut, order, e.globalRank ); // lane 2 - writeSigHeadWithEnd( w, e.head, e.endCharged ? 0u : e.endLine ); // e= after l= (already in the head when charged) + writeSigHeadWithEnd( w, e.head, e.endLine ); // e= after l=, when the lens asked and the extent is known if( !e.doc.empty() ) { w.write( "" ); w.write( escapeXml( e.doc, esc ) ); w.write( "" ); } w.write( escapeXml( e.sig, esc ) ); w.write( e.notes ); // L3: symbol notes on this (inert when null) @@ -9257,11 +9238,9 @@ struct JsonSigLens // known — --for only; off ⇒ byte-identical. bool docsAfterCode = false; // the XML sibling's SigLensRules::DocsAfterCode // (reorderDocsAfterCode) — --for only. - bool chargeEndLines = false; // SigLensRules::ChargeEndLine: "e" inside the charged - // head (an explicit ceiling's regime, forLensRules). }; -// "e" right after "l" in a JSON row head (the XML sibling's spliceEndLine; the head always opens {"l":N). 0 ⇒ untouched. +// "e" right after "l" in a JSON row head (the XML sibling's writeSigHeadWithEnd; the head always opens {"l":N). 0 ⇒ untouched. inline void spliceJsonEndLine( std::string& head, std::uint32_t endLine ) { if( endLine == 0 ) @@ -9480,10 +9459,6 @@ inline std::size_t collectJsonSigEntries( const IngestResult& ing, const std::ve e.positive = rank && (*rank)[id] > 0.0f; // A2: the XML sibling's own field, same definition e.hadDoc = hadDoc; e.endLine = lens.endLines ? defEndLine( s, lineBreaks, src.size() ) : 0u; - if( lens.chargeEndLines ) - { - spliceJsonEndLine( e.head, e.endLine ); // an explicit ceiling: "e" is part of the charged head - } e.displayRank = globalRank; e.id = id; e.pureSig = pureSig; @@ -9639,13 +9614,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s cut.docsAfterCode = reorderDocsAfterCode( entries, [ & ]( const JsonSigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, [ & ]( JsonSigEntry& e, std::uint32_t newRank ) - { - e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); - if( lens.chargeEndLines ) - { - spliceJsonEndLine( e.head, e.endLine ); - } - } ); + { e.head = jsonSigRowHead( ing, e.id, ing.symbols[ e.id ].fileId, lens, e.pureSig, rootArg, newRank ); } ); } if( cutOut ) { @@ -9685,10 +9654,7 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } first = false; std::string row = e.head; - if( !lens.chargeEndLines ) - { - spliceJsonEndLine( row, e.endLine ); // the default ceiling: "e" added here, outside every charged byte - } + spliceJsonEndLine( row, e.endLine ); // "e" after "l", outside every charged byte (0 ⇒ untouched) if( !e.doc.empty() ) { appendJsonStrField( row, ",\"doc\":", e.doc ); diff --git a/src/verbs_for.h b/src/verbs_for.h index 736f8f2d2..86ca71baf 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1479,8 +1479,9 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, - in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, /*endLines=*/true, in.docsAfterCode, - /*chargeEndLines=*/in.tokenBudget > 0 }; // an explicit ceiling charges "e" (forLensRules) + in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex, + /*endLines=*/rw::endLinesFitCeiling( in.tokenBudget, /*bodyCeiling=*/false ), // serialize.h + in.docsAfterCode }; JsonSigNoteCounts noteCounts; const auto packSigs = [ & ]( std::FILE* dst, std::size_t budget, bool* outCapped, std::size_t* outDroppedPositive, std::vector* outShownIds, rw::SigsCutReport* outCut ) @@ -2508,7 +2509,7 @@ std::optional runForLens( const MainDispatch& d ) // 2% of a 1550-token ceiling. Gate: test/budgetpolicycheck.sh arm (B). const bool forGateBudget = cfg.tokenBudget > 0; const bool forBodyCeiling = cfg.maxTokens > 0 && cfg.detail > 0; // the kShapingVerbs carve-out, read once - const bool forExplicitCeiling = forGateBudget || forBodyCeiling; // e= is CHARGED under one (serialize.h forLensRules) + const bool forEndLines = rw::endLinesFitCeiling( forGateBudget ? std::size_t( cfg.tokenBudget ) : 0u, forBodyCeiling ); // e= rides? if( forGateBudget ) { forConf.attrs += " budget_tokens=\"" + std::to_string( cfg.tokenBudget ) + "\""; @@ -2659,7 +2660,7 @@ std::optional runForLens( const MainDispatch& d ) continue; } forModScopePresent = forModScopePresent || ing.symbols[sid].kind == rw::SymKind::ModuleScope; - forEndLinePresent = forEndLinePresent || rw::mayCarryEndLine( ing.symbols[sid] ); + forEndLinePresent = forEndLinePresent || ( forEndLines && rw::mayCarryEndLine( ing.symbols[sid] ) ); } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, @@ -2936,7 +2937,7 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t idRouteLegendEmitted = compactLegendOn ? idRouteParts.route.size() : idRouteParts.bytes(); // e= (forsigspancheck): its legend clause is a disclosure on the same contract — exempt, so the ranked rows a ceiling // admits are the ones it admitted without it (owner ruling: e= is exempt from the row budget). - const std::size_t endLineLegendEmitted = !headerParts.endLinePresent || forExplicitCeiling ? 0u // charged under an explicit ceiling + const std::size_t endLineLegendEmitted = !headerParts.endLinePresent ? 0u : ( compactLegendOn ? rw::kForCompactEndLineLegend.size() : rw::kForEndLineLegend.size() ); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted + endLineLegendEmitted; @@ -2960,7 +2961,7 @@ std::optional runForLens( const MainDispatch& d ) const std::size_t fullExempt = adaptiveNote.size() + enrichmentLegendBytesEmitted( plan, false ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes() - + ( headerParts.endLinePresent && !forExplicitCeiling ? rw::kForEndLineLegend.size() : 0u ); + + ( headerParts.endLinePresent ? rw::kForEndLineLegend.size() : 0u ); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); } @@ -3032,7 +3033,7 @@ std::optional runForLens( const MainDispatch& d ) sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p= - rw::forLensRules( forDocsAfterCode, forExplicitCeiling ), // LB-A floor, e= rows, docs reorder + rw::forLensRules( forDocsAfterCode, forEndLines ), // LB-A floor, e= (endLinesFitCeiling), docs reorder &forDroppedPositive, // A2: exact count, see droppedPositiveCount &shownSigIds, // lane 2: the rows actually emitted — the tail excludes THESE files &forSigsCapped, // did the ladder fire? — the budget_bytes= clause rides only then @@ -3518,7 +3519,7 @@ std::optional runForLens( const MainDispatch& d ) { packSignatures( stdout, ing, lensRank, forTopN, cfg.packBudgetBytes, true, fanInPtr, impurePtr, redactPtr, &forChurn, &forClone, testedPtr, ampPtr, /*rankAdaptivePayload=*/true, sigsBudget, notesPtr, flRootArg, - rw::forLensRules( forDocsAfterCode, forExplicitCeiling ), nullptr, nullptr, nullptr, // the degrade path selects identically + rw::forLensRules( forDocsAfterCode, forEndLines ), nullptr, nullptr, nullptr, // the degrade path selects identically forTopRowNext ); // L-W: same next= rule on the degrade path } if( legoPreRendered ) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index d26edd012..4483fa1bb 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -37,6 +37,9 @@ # non-blank line is indented no deeper than the def, or EOF). e= sits right after l=. # (U) unknown extent: Sampler_window and SampleWindow (extent_suspect="head") and every docs/*.md row carry NO # e=; every other row carries one (a present e= is never dropped silently either). +# (B) an explicit --token-budget TIGHTER than the default signature budget (1200 tokens): no e= row and no e= clause, +# XML and JSON (the budget's rows and est_tokens promise stay what they were; charging e= there cost rows, exempting +# it broke the promise). Twin: --token-budget=8000 (above the default share) carries e= on its rows. # (O) outside --for: --pack-signatures rows and the default map's rows carry no e=. # (H) hop slots: TS — contextHost (only callee: a name-only `bag.lookup()`) has no row; createContext keeps # one. Python — Downloader.pull (only callee: a name-only `source.fetch_entry()`) has no row; fetch_entry @@ -214,6 +217,22 @@ if ran_ok "$JE" "E/JS"; then oracle "$JE" "E+U/JS" "$CORPUS/js" fi +echo "(B) an explicit token budget: no e= at all (its rows and its est_tokens promise stay what they were)" +for spec in "xml|" "json|--json"; do + tag="${spec%%|*}"; extra="${spec#*|}"; f="$TMP/ts.budget.$tag" + if [ -n "$extra" ]; then run ts "$f" "--for=How is a per-request context created?" --token-budget=1200 "$extra"; else run ts "$f" "--for=How is a per-request context created?" --token-budget=1200; fi + if ran_ok "$f" "B/$tag"; then + if ! grep -q -e ' Date: Sun, 4 Oct 2026 17:39:19 -0400 Subject: [PATCH 108/229] --for: a hop slot needs a proven callee edge (written against FE-B's hedge bit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A compact --for answer hop-expands the top ranked rows in rank order, and the first row with ANY resolved callee took the slot: an off-topic getter whose one callee was a call bound by name alone (`bag.lookup()` on an untyped local) spent it on a false-looking edge. A candidate whose every out-edge is name-only now gets no row and counts with noedge= ("no RESOLVED callee found" — a name-only binding is a hedge, not a resolution). K stays 6. ASSUMES the receiver-evidence lane (FE-B): the name-only bit is its Graph::outNameOnly. graph.h graphNameOnlyBits reads it when the Graph has it and returns nullptr otherwise, so this commit is byte-identical without FE-B (forsigspancheck's two name-only hop arms SKIP by name there) and the rule is live once FE-B is merged first (all hop arms pass on a trial merge). Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 20 ++++++++++++++++++++ src/serialize.h | 14 ++++++++++++-- src/verbs_for.h | 3 ++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/graph.h b/src/graph.h index dd77e4d3c..59af66ba7 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2899,6 +2899,26 @@ struct DispositionTally // changes NOTHING else — no candidate is admitted, dropped or reordered by it, so the emitted map is // byte-identical armed or not (test/pincensuscheck.sh arm (E) is the executable form of that sentence). +// THE HOP-SLOT RULE'S EDGE PROOF (verbs_for.h buildForCompactHops; gate test/forsigspancheck.sh (H)). The receiver-evidence +// lane (FE-B) adds a per-out-edge HEDGE bit, Graph::outNameOnly (parallel to outTargets: 1 = every call site that bound the +// edge did so by NAME ALONE). This hook reads it when the Graph has it and returns nullptr when it does not, so the rule +// is written against FE-B's bit and is byte-identical on a build without it. ASSUMES FE-B: the train orders FE-B first; +// FE-B's packHops then takes the bit itself (a reference after outTargets), so the merge keeps FE-B's parameter, drops +// this hook and the trailing pointer, and keeps the noProvenEdge test (reference resolution: the lane report). +// nullptr also when the bit is EMPTY (no name-only edge in the corpus). +template< class G > +inline const std::vector* graphNameOnlyBits( const G& g ) noexcept +{ + if constexpr( requires { g.outNameOnly; } ) + { + return g.outNameOnly.empty() ? nullptr : &g.outNameOnly; + } + else + { + return nullptr; + } +} + // ── FE-A: FALSE-EDGE RULES (test/falseedgecheck.sh) ──────────────────────────────────────────────────────────────────────── // The name ladder binds a call to the in-repo definitions of its SPELLING. Three call shapes have no in-repo target by // the language's own name-lookup rules and still got one — each a confident false row in a graded answer: diff --git a/src/serialize.h b/src/serialize.h index 15946a011..42f214fac 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -6813,7 +6813,10 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector RedactCounts* redact = nullptr, std::size_t* outShown = nullptr, // rows actually emitted; nullptr ⇒ not recorded const std::vector* rank = nullptr, // query relevance, for ordering a CUT callee listing - std::string_view rootArg = {} ) // R-E: same single-root-only root= every verb takes + std::string_view rootArg = {}, // R-E: same single-root-only root= every verb takes + const std::vector* outNameOnly = nullptr ) // hop-slot rule: FE-B's per-edge hedge bit, + // parallel to outTargets (graph.h graphNameOnlyBits); nullptr ⇒ + // every edge counts as proven (byte-identical) { if( budgetBytes == 0 ) // 0 ⇒ UNLIMITED, packBodies' own convention { @@ -6876,7 +6879,14 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // WHY a candidate has no row, and folding "has no edges" into "the budget stopped" would make // a fact about the graph look like a fact about the budget. const std::uint32_t outDeg = ( id + 1 < outOff.size() ) ? outOff[ id + 1 ] - outOff[ id ] : 0u; - if( outDeg == 0 ) + // THE HOP-SLOT RULE (gate test/forsigspancheck.sh (H)): a slot needs at least one PROVEN callee edge. A candidate + // whose every out-edge was bound by name alone (an off-topic getter whose one callee row was `bag.lookup()` on an + // untyped local, measured on a graded answer) spent the slot on a hedged edge; it now counts with noedge=, whose + // reading is "no RESOLVED callee found" — a name-only binding is a hedge, not a resolution. + const bool noProvenEdge = outDeg > 0 && outNameOnly != nullptr + && std::all_of( outNameOnly->begin() + outOff[ id ], outNameOnly->begin() + outOff[ id + 1 ], + []( std::uint8_t bit ) { return bit != 0; } ); + if( outDeg == 0 || noProvenEdge ) { ++noEdgeCount; continue; diff --git a/src/verbs_for.h b/src/verbs_for.h index 86ca71baf..c3199d588 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2146,7 +2146,8 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest : std::size_t( 1 ) ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopIds, hopBudget, g.outOff, g.outTargets, redactPtr, /*outShown=*/nullptr, &lensRank, fcRootArg ); }, + { rw::packHops( f, ing, hopIds, hopBudget, g.outOff, g.outTargets, redactPtr, /*outShown=*/nullptr, &lensRank, fcRootArg, + rw::graphNameOnlyBits( g ) ); }, // hop-slot rule: FE-B's hedge bit (nullptr without FE-B) // MARKUP rate, not the body rate — and this is an honesty choice, not a copy-paste slip. The body // rate (3.80 B/tok) prices SOURCE TEXT; the compact section contains none, only tags, identifiers // and line numbers, which tokenize like the rest of the bundle. Charging structured markup at the From 290bfc757bab510df2066c546c206bde5ca95e11 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 18:23:18 -0400 Subject: [PATCH 109/229] serialize.h: reorderDocsAfterCode's counting split out (shownDocsAboveCode), complexity 20 -> under the bar Same rows moved, same counts (forsigspancheck R arms unchanged). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 42f214fac..7bbbfd624 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -35,6 +35,7 @@ #include #include #include +#include // back_inserter (reorderDocsAfterCode) #include #include @@ -4346,30 +4347,36 @@ inline constexpr bool endLinesFitCeiling( std::size_t budgetTokens, bool bodyCei // globalRank order; EntryT has globalRank (kept: it maps the row back to its symbol), displayRank (the r= it is shown // with) and dropped. `isDoc(e)` says which rows are docs; `rehead(e, newRank)` re-renders a row whose r= changed. // Returns how many doc rows moved below a code row (0 ⇒ nothing changed; the caller discloses a non-zero count). -template< class EntryT, class IsDoc, class Rehead > -inline std::size_t reorderDocsAfterCode( std::vector& entries, IsDoc&& isDoc, Rehead&& rehead ) +// The shown (not dropped) rows' indices in `entries` order, and how many doc rows among them sit above the LAST code row +// (the rows reorderDocsAfterCode moves; 0 ⇒ nothing to move). +template< class EntryT, class IsDoc > +inline std::pair, std::size_t> shownDocsAboveCode( const std::vector& entries, IsDoc&& isDoc ) { std::vector live; + std::size_t docsSoFar = 0, docsAboveLastCode = 0; for( std::size_t i = 0; i < entries.size(); ++i ) { - if( !entries[i].dropped ) + if( entries[i].dropped ) { - live.push_back( i ); + continue; } - } - std::size_t lastCode = live.size(); - for( std::size_t k = 0; k < live.size(); ++k ) - { - if( !isDoc( entries[ live[k] ] ) ) + live.push_back( i ); + if( isDoc( entries[i] ) ) { - lastCode = k; + ++docsSoFar; + } + else + { + docsAboveLastCode = docsSoFar; // every doc row seen so far sits above this code row } } - std::size_t moved = 0; - for( std::size_t k = 0; lastCode < live.size() && k < lastCode; ++k ) - { - moved += isDoc( entries[ live[k] ] ) ? 1u : 0u; - } + return { std::move( live ), docsAboveLastCode }; +} + +template< class EntryT, class IsDoc, class Rehead > +inline std::size_t reorderDocsAfterCode( std::vector& entries, IsDoc&& isDoc, Rehead&& rehead ) +{ + const auto [ live, moved ] = shownDocsAboveCode( entries, isDoc ); if( moved == 0 ) { return 0; // no doc row above a code row (or no code row shown at all): byte-identical @@ -4379,18 +4386,9 @@ inline std::size_t reorderDocsAfterCode( std::vector& entries, IsDoc&& i for( const std::size_t i : live ) { ranks.push_back( entries[i].globalRank ); - if( !isDoc( entries[i] ) ) - { - newOrder.push_back( i ); - } - } - for( const std::size_t i : live ) - { - if( isDoc( entries[i] ) ) - { - newOrder.push_back( i ); - } } + std::copy_if( live.begin(), live.end(), std::back_inserter( newOrder ), [ & ]( std::size_t i ) { return !isDoc( entries[i] ); } ); + std::copy_if( live.begin(), live.end(), std::back_inserter( newOrder ), [ & ]( std::size_t i ) { return isDoc( entries[i] ); } ); ASSUME( newOrder.size() == ranks.size() ); for( std::size_t k = 0; k < newOrder.size(); ++k ) { From 3b02acc78075cad6868a50ec3cbfee964d768139 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:09:05 -0400 Subject: [PATCH 110/229] graph.h: graphNameOnlyBits moved to the end of the file The hop-slot hook sat 20 lines above rankGraphTeleport and moved the src/graph.h line that the published showcase capture seeds (showcasecapturecheck (H)). At the end of the file it moves no pinned line; the template, its callers and every output byte are unchanged. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/graph.h b/src/graph.h index 59af66ba7..6356ea6b9 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2899,26 +2899,6 @@ struct DispositionTally // changes NOTHING else — no candidate is admitted, dropped or reordered by it, so the emitted map is // byte-identical armed or not (test/pincensuscheck.sh arm (E) is the executable form of that sentence). -// THE HOP-SLOT RULE'S EDGE PROOF (verbs_for.h buildForCompactHops; gate test/forsigspancheck.sh (H)). The receiver-evidence -// lane (FE-B) adds a per-out-edge HEDGE bit, Graph::outNameOnly (parallel to outTargets: 1 = every call site that bound the -// edge did so by NAME ALONE). This hook reads it when the Graph has it and returns nullptr when it does not, so the rule -// is written against FE-B's bit and is byte-identical on a build without it. ASSUMES FE-B: the train orders FE-B first; -// FE-B's packHops then takes the bit itself (a reference after outTargets), so the merge keeps FE-B's parameter, drops -// this hook and the trailing pointer, and keeps the noProvenEdge test (reference resolution: the lane report). -// nullptr also when the bit is EMPTY (no name-only edge in the corpus). -template< class G > -inline const std::vector* graphNameOnlyBits( const G& g ) noexcept -{ - if constexpr( requires { g.outNameOnly; } ) - { - return g.outNameOnly.empty() ? nullptr : &g.outNameOnly; - } - else - { - return nullptr; - } -} - // ── FE-A: FALSE-EDGE RULES (test/falseedgecheck.sh) ──────────────────────────────────────────────────────────────────────── // The name ladder binds a call to the in-repo definitions of its SPELLING. Three call shapes have no in-repo target by // the language's own name-lookup rules and still got one — each a confident false row in a graded answer: @@ -9435,4 +9415,25 @@ inline std::size_t declinedCallsMadeBy( const Graph& g, std::span return callCount; } +// THE HOP-SLOT RULE'S EDGE PROOF (verbs_for.h buildForCompactHops; gate test/forsigspancheck.sh (H)). The receiver-evidence +// lane (FE-B) adds a per-out-edge HEDGE bit, Graph::outNameOnly (parallel to outTargets: 1 = every call site that bound the +// edge did so by NAME ALONE). This hook reads it when the Graph has it and returns nullptr when it does not, so the rule +// is written against FE-B's bit and is byte-identical on a build without it. ASSUMES FE-B: the train orders FE-B first; +// FE-B's packHops then takes the bit itself (a reference after outTargets), so the merge keeps FE-B's parameter, drops +// this hook and the trailing pointer, and keeps the noProvenEdge test (reference resolution: the lane report). +// nullptr also when the bit is EMPTY (no name-only edge in the corpus). Kept at the END of this file so the hook moves no +// line a published capture pins (showcasecapturecheck (H) seeds src/graph.h:LINE inside rankGraphTeleport). +template< class G > +inline const std::vector* graphNameOnlyBits( const G& g ) noexcept +{ + if constexpr( requires { g.outNameOnly; } ) + { + return g.outNameOnly.empty() ? nullptr : &g.outNameOnly; + } + else + { + return nullptr; + } +} + } // namespace rw From 0d743ac15c76c425c37538beedbdccdf44f42534 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 13:09:06 -0400 Subject: [PATCH 111/229] test: gates that read --for rows accept e=; by-design re-pins for the e= bytes - candheadcheck, defoverdeclcheck: the row parsers allow an optional e="N" after l= (same rows counted with or without it; both still pass on the base binary). - anchorcheck, routecheck: golden_for.xml re-pinned (+250 B each: 3 e= attributes and the full legend's e= clause). With e=, that clause and est_tokens normalized out, old and new documents are byte-identical; the base binary reproduces the old goldens exactly. - relevancefloorcheck (4): the 3000 B ceiling is kept on the answer with e= and its clause normalized out (2871 B, as before); the whole answer gets a 3300 B ceiling (4b); (4c) proves the normalization is live on an e= answer. - fixedbufsweep: the two new fixed buffers (eAttr, eKey: one integer each) and the third sigsOpenTag nb site (docs_after_code) are classified; EXPECTED re-derived. - selfcheckcheck: starts_with joins ALLOW beside ends_with (read-only prefix comparison). Co-Authored-By: Claude Opus 5.5 --- test/anchorcheck.sh | 5 +++++ test/anchorfix/golden_for.xml | 2 +- test/candheadcheck.sh | 5 +++-- test/defoverdeclcheck.sh | 3 ++- test/fixedbufsweep.sh | 10 ++++++++-- test/relevancefloorcheck.sh | 19 +++++++++++++++++-- test/routecheck.sh | 5 +++++ test/routefix/golden_for.xml | 2 +- test/selfcheckcheck.sh | 1 + 9 files changed, 43 insertions(+), 9 deletions(-) diff --git a/test/anchorcheck.sh b/test/anchorcheck.sh index 932ab6cb5..6153123c2 100755 --- a/test/anchorcheck.sh +++ b/test/anchorcheck.sh @@ -108,6 +108,11 @@ QUERY="frobnicate widget cache" # ladder trimmed used to appear in neither section), and the clause defining the tail says so. Verified before # re-pinning: with every comment and est_tokens= normalized out, old and new documents are byte-identical — # this fixture's head covers every file, so its tail is unchanged and every ranking byte is unmoved. +# RE-PIN 2026-10-07 (lane for-spine-span-068, by design): 3557 -> 3807 B (+250 B), est_tokens="1349" -> "1449". CAUSE: --for +# rows with a known extent carry e="N" (the definition's last line) right after l= (3 rows here), and the full +# legend gains the clause defining e=. Verified before re-pinning (tmp re-pin script, base binary 0c115310 == old golden): +# with e= attributes, that clause and est_tokens normalized out, old and new documents are byte-identical — no +# ranking, body or route byte moved. # L1 (2026-09-19): the CLI default legend is compact; this arm compares against a golden recorded from the full default, so it asks for it. "$BIN" anchorfix --no-cache --for="$QUERY" --no-route --legend=full >"$TMP/plain_full.xml" 2>/dev/null diff -q "$TMP/plain_full.xml" "$ROOT/test/anchorfix/golden_for.xml" >/dev/null \ diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index 896989756..12c347c71 100644 --- a/test/anchorfix/golden_for.xml +++ b/test/anchorfix/golden_for.xml @@ -1,4 +1,4 @@ -frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit() — reorder, not filter ────────────────────────────────────── # 9 = the seven pollutant overloads plus the gold's own class + constructor rows; the file-grouping wrapper prints its path ONCE per file, so counting rows (not p="…" occurrences) -# is what actually counts ranked ROWS. -rowcount="$( tr '>' '\n' <"$TMP/lens" | grep -c ', got $rowcount" diff --git a/test/defoverdeclcheck.sh b/test/defoverdeclcheck.sh index 8e0c27863..fe5350611 100755 --- a/test/defoverdeclcheck.sh +++ b/test/defoverdeclcheck.sh @@ -129,7 +129,8 @@ done # inert arm in a gate written for a ranking change is the failure mode this suite has been bitten by # twice. The first row is also the one that survives every budget: it is what the defect actually # costs on a corpus where 85 declarations push the definition off the end. -first="$( tr '>' '\n' <"$TMP/lens" | sed -n 's/.*"` buffer went with the merge. + ( "src/serialize.h", "nb" ): 4, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge. # cut-fix lane A (2026-09-23): +2, sigsOpenTag's nb[96] — ' shown="{}" total="{}" capped="1"' # (31 B literal + two size_t at 20 digits = 71 B worst case, against 95 usable + NUL) and # ' docs_dropped="{}"' (16 B + 20 digits = 36 B). Counts only, no %s, nothing escaped. + # lane/for-spine-span-068 (2026-10-04): +1, the same nb[96]'s ' docs_after_code="{}"' + # (19 B + 20 digits = 39 B). A count, no %s, nothing escaped. ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, ( "src/serialize.h", "rc" ): 1, @@ -594,7 +600,7 @@ if not bad: # 238 -> 239 calls/sites, 104 -> 105 rows). noteCacheReject's new `char detail[192]` names both version # numbers on a refused cache blob; one formatTo of a literal and two std::uint32_t, rowed 'not-markup' # (a stderr notice). Re-derived on the train 20 merged tree; no other train lane moves the population. -EXPECTED = { "mentions": 351, "calls": 241, "sites": 241, "rows": 105, "widthforms": 0 } +EXPECTED = { "mentions": 354, "calls": 244, "sites": 244, "rows": 107, "widthforms": 0 } # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the diff --git a/test/relevancefloorcheck.sh b/test/relevancefloorcheck.sh index 6942b8004..efab47e7a 100755 --- a/test/relevancefloorcheck.sh +++ b/test/relevancefloorcheck.sh @@ -172,9 +172,24 @@ echo "=== (4) byte ceiling on the fixture ===" # Pinned, not relative: the pre-fix binary answers this exact fixture in 5,307 B (recorded in the header). # 3000 leaves room for the shared legends (which are most of what is left) while staying far below any # padded answer. If a legend legitimately grows past this, re-pin in the same commit and say why. +# RE-PIN 2026-10-07 (lane for-spine-span-068, by design): the one floored row now carries e="N" (its end line) +# and the full legend gains the ~225 B clause defining e= — 2871 -> 3107 B. The 3000 B assertion is KEPT on the +# answer with exactly those two additions normalised out (4), and the whole answer gets its own ceiling of +# 3300 B (4b) — still far below the 5307 B padded answer this arm exists to keep out. a_bytes="$( printf '%s' "$A_OUT" | wc -c | tr -d ' ' )" -[ "$a_bytes" -lt 3000 ] && ok "(4) the floored bundle is $a_bytes B (< 3000; pre-fix was 5307 B on this fixture)" \ - || no "(4) the floored bundle is $a_bytes B, over the 3000 B ceiling" +a_norm_bytes="$( printf '%s' "$A_OUT" | sed -E 's/ e="[0-9]+"//g; s/; e= on a d row: the 1-based line where that definition ends, body-inclusive; absent when the extent is not known \(extent_suspect, docs, config\), never 0; l= is the line of the definition.s name, so a definition can start above l=//' | wc -c | tr -d ' ' )" +case "$a_bytes$a_norm_bytes" in *[!0-9]*|'') no "(4) byte counts are not numeric ('$a_bytes' / '$a_norm_bytes')"; a_bytes=999999; a_norm_bytes=999999 ;; esac +[ "$a_norm_bytes" -lt 3000 ] && ok "(4) the floored bundle without e= and its clause is $a_norm_bytes B (< 3000; pre-fix was 5307 B on this fixture)" \ + || no "(4) the floored bundle without e= and its clause is $a_norm_bytes B, over the 3000 B ceiling" +[ "$a_bytes" -lt 3300 ] && ok "(4b) the whole floored bundle is $a_bytes B (< 3300 with e= and its legend clause)" \ + || no "(4b) the whole floored bundle is $a_bytes B, over the 3300 B ceiling" +if printf '%s' "$A_OUT" | grep -q ' e="'; then + [ "$a_norm_bytes" -lt "$a_bytes" ] \ + && ok "(4c) the normalisation in (4) removed e= bytes ($a_bytes -> $a_norm_bytes B), so the 3000 B twin is live" \ + || no "(4c) the answer carries e= but normalising it removed nothing — the (4) twin would be inert" +else + ok "(4c) this answer carries no e= (a binary without it): (4) measures the answer as it is" +fi # ═══════════════════════════════════════════════════════════════════════════ echo "=== (5) nothing matched ⇒ nothing claimed ===" diff --git a/test/routecheck.sh b/test/routecheck.sh index 9adfb8357..8d39f5040 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -111,6 +111,11 @@ CONCEPT="how does resolution work" # ladder trimmed used to appear in neither section), and the clause defining the tail says so. Verified before # re-pinning: with every comment and est_tokens= normalized out, old and new documents are byte-identical — # this fixture's head covers every file, so its tail is unchanged and every ranking byte is unmoved. +# RE-PIN 2026-10-07 (lane for-spine-span-068, by design): 3776 -> 4026 B (+250 B), est_tokens="1433" -> "1533". CAUSE: --for +# rows with a known extent carry e="N" (the definition's last line) right after l= (3 rows here), and the full +# legend gains the clause defining e=. Verified before re-pinning (tmp re-pin script, base binary 0c115310 == old golden): +# with e= attributes, that clause and est_tokens normalized out, old and new documents are byte-identical — no +# ranking, body or route byte moved. # L1 (2026-09-19): the CLI default legend is compact; golden_for.xml was recorded from the full default and the --query arm # reads the full legend's "routed:" comment, so those two runs ask for the full legend. "$BIN" routefix --no-cache --for="$CONCEPT" --no-route --legend=full >"$TMP/concept_noroute.xml" 2>/dev/null diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index 48407f2b1..63a0539e8 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Wed, 7 Oct 2026 16:06:18 -0400 Subject: [PATCH 112/229] fix(for): a charged continuation at the floor ships and is labelled; compact pays from full's room Two open cases of the charged regime (an explicit --token-budget whose sig side is the whole ceiling): - compactlegendcheck P4 (--token-budget=850: compact 892 vs full 844 est_tokens): the compact dialect's sig ledger charges less header than it emits, so it kept one row more than full and landed past both. The plan now also reserves that ledger gap (compactLedgerGapBytes) on a capped compact block, so it is cut to the rows the full one keeps; an uncapped block is untouched. - estchargecheck #11 A7 (--detail=20 --with-graph at 2000: the block at its rank 1..4 floor has nothing to shed for its next=). The recovery handle always ships (PROCESS rule 5): the floor's unpaid bytes are reported (SigsCutReport::continuationUnpaidBytes), and when the finished document lands past its byte allowance or its token budget the root carries over_ceiling="1" with a clause saying why. The continuation's row displacement is one shared helper (paySigsContinuationFromRows) for both dialects. packSignatures / packSignaturesJson signatures are unchanged from origin/main (the request rides SigsCutReport). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 62 +++++++++++++++++++++++++++++++++++++------------ src/verbs_for.h | 40 ++++++++++++++++++++++++++----- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 8cd40748c..30081a772 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -873,6 +873,12 @@ inline constexpr std::string_view kForSigsNextBudgetNote = " [sigs next_budget_tokens=: call this tool again with that budget_tokens to get all total= rows uncut]"; inline constexpr std::string_view kForSigsNextOffsetNote = " [sigs next_offset=: the candidate index the cut starts at; a resume must match the root at=]"; +// knob-honesty-068 (orchestrator ruling 2026-10-07, PROCESS rule 5: the recovery handle ALWAYS ships): a charged at its +// rank 1..4 floor cannot give up the bytes of its next=, so the handle rides unpaid; when the document then lands past its +// ceiling, over_ceiling="1" says so and this clause says why. Not "est_tokens exceeds budget_tokens": on a --detail bundle the +// bodies price at the body rate, so the byte allowance (budget_tokens x 2.36 x 1.15) can be the ceiling crossed. +inline constexpr std::string_view kForSigsFloorOverCeilingNote = + " [over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]"; // The clauses a cut report owes, concatenated in a fixed order ("" when it owes none) — defined below SigsCutReport // (sigsCutReportLegend), because the continuation's clauses read the report itself. @@ -4487,6 +4493,8 @@ struct SigsCutReport bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) bool terseContinuation = false; // a CHARGED pasteable one under a hard ceiling: next= alone (no next_offset=, no // clause — the bundle's own next= reading defines it); see SigsCutContinuation + std::size_t continuationUnpaidBytes = 0; // a CHARGED one's bytes the rank 1..4 floor could not give up: it rides anyway + // (the recovery handle always ships) and the caller labels any overshoot }; // ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── @@ -4535,6 +4543,9 @@ struct SigsCutContinuation bool json = false; // the --json dialect: the re-run is --json too bool pasteHandle = true; // false on MCP: the client re-calls the tool, so no CLI argv — the machine form, // next_budget_tokens=, is the argument a re-call of the same tool needs + std::size_t ledgerGapBytes = 0; // a CHARGED one's plan also reserves this: the sig room the caller's dialect was given beyond + // what its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes), + // so a capped compact block is cut to the rows the full one keeps }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -4707,6 +4718,28 @@ inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, s const std::size_t clauses = withClauses ? kForSigsNextBudgetNote.size() + kForSigsNextOffsetNote.size() : 0u; return valueAttrBytes + offsetAttrBytes + clauses; } +// The plan-side share of a CHARGED continuation: the dialect's ledger gap (SigsCutContinuation::ledgerGapBytes). It rides the +// marker's reservation, so it is spent only when the ladder fires — an uncapped block is untouched. +inline std::size_t sigsCutPlanReserve( const SigsCutContinuation* req ) noexcept +{ + return ( req != nullptr && req->charged ) ? req->ledgerGapBytes : 0u; +} +// A CHARGED continuation is paid FROM THE ROWS, byte for byte: the same ladder runs on past the cut until the block has given +// up `cost` bytes, so block + continuation never exceeds the block the cut alone left — in either dialect, whatever the plan's +// slack (reserving it in the plan let that slack absorb it and the bundle grow). Returns the bytes the rank 1..4 floor could +// NOT give up (0 = paid in full); the continuation rides regardless (orchestrator ruling 2026-10-07) and the caller labels it. +template +inline std::size_t paySigsContinuationFromRows( std::vector& entries, std::vector& files, std::size_t& total, + std::size_t cost, CostFn entryCost ) +{ + if( cost == 0 ) + { + return 0; + } + const std::size_t target = total > cost ? total - cost : 0u; + trimSigLadder( entries, files, total, target, entryCost ); + return total > target ? total - target : 0u; // not bounded by `cost`: a ladder action may append an ellipsis (trimSigLadder) +} // The open tag: `` untrimmed; ` shown= total= capped="1"` when rows were cut or shrunk; ` docs_dropped=` // when a shown row lost its doc comment. The bytes it adds are the ones planSigsTrim reserved. @@ -5119,7 +5152,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: : sizeof( " next_budget_tokens=\"\"" ) - 1 + sigsDecimalDigits( cutNextTokens ), sizeof( " next_offset=\"\"" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/true ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1, sizeof( " docs_dropped=\"\"" ) - 1 ); + sizeof( " shown=\"\" total=\"\" capped=\"1\"" ) - 1 + sigsCutPlanReserve( cutNext ), + sizeof( " docs_dropped=\"\"" ) - 1 ); if( plan.ladderFires ) { // one ladder ACTION on one entry, tail-first; every action re-checks the budget so the ladder @@ -5127,13 +5161,9 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: // ladder itself is trimSigLadder() above, shared verbatim with the JSON sibling (§A4a). trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, entryCost ); } - // knob-honesty-068: a CHARGED continuation is paid FROM THE ROWS, byte for byte: the same ladder runs on past the cut - // until the block has given up its bytes, so block + continuation never exceeds the block the cut alone left — in - // either dialect, whatever the plan's slack (reserving it in the plan let that slack absorb it and the bundle grow). - if( plan.capped && cutNextCost > 0 ) - { - trimSigLadder( entries, sigFiles, total, total > cutNextCost ? total - cutNextCost : 0u, entryCost ); - } + // knob-honesty-068: a CHARGED continuation is paid from the rows (paySigsContinuationFromRows); what the floor + // cannot pay rides anyway and is reported for the caller's over_ceiling label + const std::size_t cutNextUnpaid = paySigsContinuationFromRows( entries, sigFiles, total, plan.capped ? cutNextCost : 0u, entryCost ); // A2: the exact count, computed AFTER the ladder has made its final drop decisions (droppedPositiveCount // above — the shared arithmetic with the JSON sibling). Pure bookkeeping, no output bytes either way. @@ -5171,7 +5201,8 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: *cappedOut = plan.ladderFires; } SigsCutReport cut = sigsCutReportOf( entries, totalRows, plan ); - cut.continuationRequest = cutNext; + cut.continuationRequest = cutNext; + cut.continuationUnpaidBytes = cutNextUnpaid; sigsCutAttachContinuation( cut, cutNext, cutNextInv, cutNextTokens ); // nothing unless capped and asked for if( cutOut ) { @@ -9553,15 +9584,15 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s sizeof( ",\"sigs_next_offset\":" ) - 1 + sigsDecimalDigits( std::numeric_limits::max() ), /*withClauses=*/false ); const SigsTrimPlan plan = planSigsTrim( entries, totalRows, gateCut, total, payloadBudgetBytes, - sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1, sizeof( ",\"docs_dropped\":" ) - 1 ); + sizeof( ",\"sigs_shown\":,\"sigs_total\":" ) - 1 + sigsCutPlanReserve( cutNext ), + sizeof( ",\"docs_dropped\":" ) - 1 ); if( plan.ladderFires ) { trimSigLadder( entries, sigFiles, total, plan.effectiveBudget, jsonSigEntryCost ); } - if( plan.capped && cutNextCost > 0 ) // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule) - { - trimSigLadder( entries, sigFiles, total, total > cutNextCost ? total - cutNextCost : 0u, jsonSigEntryCost ); - } + // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule); the floor's unpaid rest rides anyway + // and this dialect's own predicate (forLensJsonOverCeiling: bytes past the allowance) labels any overshoot + const std::size_t cutNextUnpaid = paySigsContinuationFromRows( entries, sigFiles, total, plan.capped ? cutNextCost : 0u, jsonSigEntryCost ); if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped @@ -9569,7 +9600,8 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s if( cutOut ) { *cutOut = sigsCutReportOf( entries, totalRows, plan ); - cutOut->continuationRequest = cutNext; + cutOut->continuationRequest = cutNext; + cutOut->continuationUnpaidBytes = cutNextUnpaid; sigsCutAttachContinuation( *cutOut, cutNext, cutNextInv, cutNextTokens ); // knob-honesty-068: nothing unless capped and asked for } if( droppedPositiveOut ) diff --git a/src/verbs_for.h b/src/verbs_for.h index 632940192..398112bab 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1287,6 +1287,11 @@ struct ForLensRootFinish std::size_t tokenBudget = 0; int maxTokens = 0; bool bodyCeiling = false; + // knob-honesty-068 (orchestrator ruling 2026-10-07): a charged continuation rode unpaid from the rank 1..4 floor + // (SigsCutReport::continuationUnpaidBytes) AND the document lands past its ceiling — byte allowance or budget_tokens. + // Decided by the caller on the finished document (a value, never a search of the text); labels it over_ceiling="1" + // with kForSigsFloorOverCeilingNote, which is true where kOverCeilingLegend's est_tokens reading may not be. + bool sigsFloorOver = false; }; // PR #215 review: the finished header AND the number it prints, handed back together. finishForLensHeader @@ -1370,9 +1375,13 @@ inline ForLensPricedHeader finishForLensHeaderPriced( std::string header, const // clause unconditionally would charge every budgeted bundle for an attribute it does not carry. Monotone — // adding bytes only RAISES est_tokens — so one extra stage is exact and the flag never oscillates. The // definition rides the legend of the document that carries the attribute. - if( forLensOverCeiling( f.lastRungFired, f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ) ) + const bool namedCeilingOver = forLensOverCeiling( f.lastRungFired, f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); + if( namedCeilingOver || f.sigsFloorOver ) { - spliceBefore( header, " -->", /*fromEnd=*/true, f.overCeilingLegend ); + // each clause rides only where its reading is true: the est_tokens/last-rung one when that predicate holds, the + // floor one when the unpaid recovery handle is what crossed (knob-honesty-068) — both when both hold + spliceBefore( header, " -->", /*fromEnd=*/true, namedCeilingOver ? f.overCeilingLegend : std::string_view() ); + spliceBefore( header, " -->", /*fromEnd=*/true, f.sigsFloorOver ? rw::kForSigsFloorOverCeilingNote : std::string_view() ); overAttr = " over_ceiling=\"1\""; priced = priceFixpoint( header.size() + f.nonHeaderMarkupBytes, overAttr.size() ); } @@ -2936,6 +2945,7 @@ std::optional runForLens( const MainDispatch& d ) // tokens: 995 vs 816). So the compact charge is capped at the FULL dialect's charge for the same header, computed by the // same rule: rows(default) ⊇ rows(full) by construction, and a default that pays more header than full never gets more // room for rows than full had. + std::size_t compactLedgerGapBytes = 0; // knob-honesty-068: how much MORE sig room this compact header was given than full's if( compactLegendOn ) { ForLensHeaderParts fullParts = headerParts; @@ -2945,7 +2955,8 @@ std::optional runForLens( const MainDispatch& d ) + confidenceEarlyAttrsBytes + confidenceEarlyNoteBytes + forAtAttrStr.size() + rw::kForFileTailLegend.size() + idRouteParts.bytes(); const std::size_t fullCharged = fullExempt > fullHeaderBytes ? fullHeaderBytes : fullHeaderBytes - fullExempt; - chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + chargedHeaderBytes = std::min( chargedHeaderBytes, fullCharged ); + compactLedgerGapBytes = fullCharged - chargedHeaderBytes; // >= 0 by the min above } const std::size_t fixedBytes = chargedHeaderBytes + legoStr.size() + composeStr.size() + routeStr.size() + 6; // + "" // the auto bundle's SECTION SPLIT — the sig side's claim is capped so an explicit ceiling wider @@ -3009,11 +3020,17 @@ std::optional runForLens( const MainDispatch& d ) // two regimes are argued): CHARGED exactly when an explicit ceiling's sig side is the whole ceiling (nothing // downstream can pay, and the ceiling is hard); exempt at the default and when the sig side is frozen at the // default's share (forSigSideCeiling), so those ranked sets — and forbudgetmonotoncheck's identity — are untouched. - // A charged one is paid from the rows byte for byte (serialize.h packSignatures), so a capped bundle never grows - // past the bytes the cut alone left it — in the compact dialect too, whose sig ledger leans on the header rungs. + // A charged one is paid from the rows byte for byte (serialize.h paySigsContinuationFromRows), so a capped bundle never + // grows past the bytes the cut alone left it. THE COMPACT DIALECT ALSO PAYS FROM FULL'S ROOM: its sig ledger charges + // less header than it emits and leans on the header rungs to land inside the ceiling, so a charged one also reserves + // the room the compact charge was given beyond full's (compactLedgerGapBytes) — in the plan, so only a block the ladder + // already trims pays it: a capped compact answer serves the rows the full one does and lands no further over its + // ceiling (compactlegendcheck P4). An uncapped answer is untouched. AT THE FLOOR (rank 1..4, nothing left to shed) the + // handle still ships and an overshoot is labelled (rootFinish.sigsFloorOver below; orchestrator ruling 2026-10-07). const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; const std::string forRankFlags = forRankShapingFlags( cfg ); - const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false }; + const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, + /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation @@ -3497,6 +3514,17 @@ std::optional runForLens( const MainDispatch& d ) /*hasRouteAttr=*/!routeNoteRaw.empty(), kNotes ); headerStr = std::move( chosen.header ); rootFinish.lastRungFired = ( chosen.rung == rw::CeilingRung::OverCeiling ); + // knob-honesty-068 (orchestrator ruling 2026-10-07): the recovery handle ALWAYS ships. When the floor could + // not pay for its next= and the document it rides in lands past its ceiling — the byte allowance, or the token + // budget the root names — the root says so: over_ceiling="1" + kForSigsFloorOverCeilingNote. Read off the + // FINISHED document (the header this ladder chose + every byte stdout receives); the label only adds bytes, + // so a document over before it is over after it. + if( forSigsCut.continuationUnpaidBytes > 0 ) + { + const ForLensPricedHeader asChosen = finishForLensHeaderPriced( headerStr, rootFinish ); + rootFinish.sigsFloorOver = asChosen.header.size() + emittedNonHeaderBytes > ladderCeiling + || asChosen.estTokens > cfg.tokenBudget; + } } headerStr = finishForLensHeader( std::move( headerStr ), rootFinish ); From 9143c7a74a394770d5721e61b251819fa011170d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:12:40 -0400 Subject: [PATCH 113/229] --for: RIPWIRE_FOR_ENDLINES=always|auto|never, an experimental switch for measuring e= under tight ceilings The rule that drops e= under an explicit ceiling tighter than the default signature budget stays the default (auto). The switch moves only that one decision, endLinesFitCeiling, which CLI --for (XML and JSON) and MCP for all ask: always adds e= under every ceiling with the same shown rows (e= is never charged to rows, so est_tokens may exceed a tight budget, which is why auto is the default); never drops e= everywhere. An unknown value falls back to auto and says so once on stderr; it is never read as never. Read once per process. No flag and no legend change; CHANGELOG note only. Gate: forsigspancheck (N), 14 arms: always/never/auto in both dialects and MCP for, auto byte-identical to unset, three unknown spellings, and a --pack-signatures near miss. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++- src/serialize.h | 79 +++++++++++++++++++++++++++++- test/forsigspancheck.sh | 106 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74549f169..5307bbe23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,11 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli signature-row byte budget, so the default ceiling admits the same rows it admitted before; it costs about 8 bytes per row. An answer whose explicit ceiling is TIGHTER than the default signature budget (`--token-budget` below it, a body ceiling, MCP `budget_tokens` likewise) carries no `e=`: its est_tokens promise and its rows stay exactly what they - were. A ceiling at or above the default serves the default's ``, `e=` included. + were. A ceiling at or above the default serves the default's ``, `e=` included. For measurement only, the + experimental environment switch `RIPWIRE_FOR_ENDLINES=always|auto|never` moves that one decision (CLI `--for` in both + dialects and MCP `for`): `auto`, the default, is the rule above; `always` adds `e=` under every ceiling with the same + rows, so est_tokens may then exceed a tight budget; `never` drops it everywhere. Any other value falls back to `auto` + and says so on stderr. It is not a flag and may be removed once measured. `--pack-signatures`, `--pack-task`, `--from-trace` and the map keep their bytes. - **Code above docs.** On a question that does not ask about docs, the code rows of the SHOWN signature set now come first and its markdown rows after them. It is a reorder of the same rows: the score order and the byte budget still diff --git a/src/serialize.h b/src/serialize.h index 7bbbfd624..72aead92c 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4285,7 +4285,7 @@ inline constexpr std::string_view kForEndLineLegend = // The " e=\"N\"" run, spliced into a rendered head right after its l= value (the head always opens `= kForPayloadBudgetBytes ); } +static_assert( endLinesFitCeilingFor( ForEndLinesMode::Auto, 0, false ) && !endLinesFitCeilingFor( ForEndLinesMode::Auto, 0, true ) ); +static_assert( endLinesFitCeilingFor( ForEndLinesMode::Always, 1, true ) && !endLinesFitCeilingFor( ForEndLinesMode::Never, 0, false ) ); +inline bool endLinesFitCeiling( std::size_t budgetTokens, bool bodyCeiling ) +{ + return endLinesFitCeilingFor( forEndLinesMode(), budgetTokens, bodyCeiling ); +} // ── CODE ABOVE DOCS, AS A REORDER OF THE SHOWN SET (filter.h CODE ABOVE DOCS; gate test/forsigspancheck.sh (R)) ───────── // Runs AFTER the score order and the byte ladder have chosen which rows are shown, so it can never evict a row: the shown diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 4483fa1bb..9265aafbd 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -57,6 +57,13 @@ # (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. # (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's # line, not the definition's first line). +# (N) the experimental RIPWIRE_FOR_ENDLINES switch (serialize.h forEndLinesMode; default auto = arm (B)'s rule): +# always — e= on the --token-budget=1200 rows, XML and JSON, and on the MCP `for` rows under budget_tokens=1200, +# with the SAME row set (n p l r) as auto there (e= never costs a row); est_tokens vs the budget is REPORTED +# (may exceed it — that is why auto is the default), not gated. never — no e= and no e= clause at the default +# ceiling (XML, JSON, MCP), same row set as auto. auto — byte-identical to unset (default and 1200). An unknown +# value ("sometimes", "Always", "") — byte-identical to unset (so e= stays at the default: never read as never) +# plus one stderr line naming the switch. Near miss: always does not put e= on --pack-signatures rows. # (K) the predicates can fail: a row with e= < l=, a row with e= on an md row, a missing e= on a code row, and a # name-only hop row are each caught; a document with no --for root is never a pass. Every run must exit 0. # @@ -451,6 +458,105 @@ PY [ $? -eq 0 ] || fail=1 fi +echo "(N) RIPWIRE_FOR_ENDLINES=always|auto|never (experimental; unset = auto)" +NQ="--for=How is a per-request context created?" +# runenv VALUE OUT ARGS… — VALUE "-" leaves the switch unset +runenv(){ local v="$1" f="$2"; shift 2 + if [ "$v" = "-" ]; then ( cd "$CORPUS/ts" && env -u RIPWIRE_FOR_ENDLINES "$BIN" . --no-cache "$@" >"$f" 2>"$f.err" ) + else ( cd "$CORPUS/ts" && RIPWIRE_FOR_ENDLINES="$v" "$BIN" . --no-cache "$@" >"$f" 2>"$f.err" ); fi + printf '%s' "$?" >"$f.rc"; } +# rowset FILE — the rows (XML) or "sigs" entries (JSON) as "n p l r" lines, e= ignored; "NOROWS" when none +cat >"$TMP/rowset.py" <<'PY' +import json, re, sys +exec( open( sys.argv[ 1 ] ).read() ) +text = load( sys.argv[ 2 ] ) +if text.lstrip().startswith( "{" ): + rows = [ ( r.get( "n" ), r.get( "p" ), str( r.get( "l" ) ), str( r.get( "r" ) ) ) for r in json.loads( text ).get( "sigs", [] ) ] + est = json.loads( text ).get( "est_tokens" ) +else: + rows = [ ( a.get( "n" ), a.get( "p" ), a.get( "l" ), a.get( "r" ) ) for _, a in drows( text ) ] + m = re.search( r']*\best_tokens="([0-9]+)"', text ); est = m.group( 1 ) if m else None +print( "EST %s" % est ) +print( "\n".join( " ".join( map( str, r ) ) for r in rows ) if rows else "NOROWS" ) +PY +rowset(){ python3 "$TMP/rowset.py" "$TMP/rows.py" "$1" | sed 1d; } +estof(){ python3 "$TMP/rowset.py" "$TMP/rows.py" "$1" | sed -n '1s/^EST //p'; } +has_e(){ grep -Eq ' e="[0-9]+"|"e":[0-9]+' "$1"; } +has_e_clause(){ grep -Eq 'e= on a d row|d e= its last line' "$1"; } +for spec in "xml|" "json|--json"; do + tag="${spec%%|*}"; extra="${spec#*|}" + set -- "$NQ" --token-budget=1200; [ -n "$extra" ] && set -- "$@" "$extra" + NA="$TMP/n.always.$tag"; NU="$TMP/n.unset1200.$tag"; NX="$TMP/n.auto1200.$tag" + runenv always "$NA" "$@"; runenv - "$NU" "$@"; runenv auto "$NX" "$@" + if ran_ok "$NA" "N/always/$tag" && ran_ok "$NU" "N/unset1200/$tag" && ran_ok "$NX" "N/auto1200/$tag"; then + if [ "$( rowset "$NU" )" = NOROWS ]; then no "N/always/$tag: the 1200-token answer has no rows (premise)" + elif ! has_e "$NA"; then no "N/always/$tag: RIPWIRE_FOR_ENDLINES=always carries no e= under --token-budget=1200" + elif has_e "$NU"; then no "N/always/$tag: premise — the unset 1200-token answer already carries e= (arm B's rule moved)" + elif [ "$( rowset "$NA" )" != "$( rowset "$NU" )" ]; then no "N/always/$tag: forcing e= changed the shown row set (e= cost or moved a row)" + else ok "N/always/$tag: e= present under --token-budget=1200, same $( rowset "$NU" | wc -l | tr -d ' ' ) rows as auto (est_tokens $( estof "$NU" ) -> $( estof "$NA" ) vs budget 1200, reported not gated)"; fi + if cmp -s "$NX" "$NU"; then ok "N/auto/$tag: RIPWIRE_FOR_ENDLINES=auto is byte-identical to unset at --token-budget=1200" + else no "N/auto/$tag: auto differs from unset at --token-budget=1200"; fi + fi + set -- "$NQ"; [ -n "$extra" ] && set -- "$@" "$extra" + NN="$TMP/n.never.$tag"; ND="$TMP/n.unset.$tag"; NXD="$TMP/n.auto.$tag" + runenv never "$NN" "$@"; runenv - "$ND" "$@"; runenv auto "$NXD" "$@" + if ran_ok "$NN" "N/never/$tag" && ran_ok "$ND" "N/unset/$tag" && ran_ok "$NXD" "N/auto/$tag"; then + if ! has_e "$ND"; then no "N/never/$tag: premise — the unset default answer carries no e=" + elif has_e "$NN" || has_e_clause "$NN"; then no "N/never/$tag: RIPWIRE_FOR_ENDLINES=never still carries e= or its clause at the default ceiling" + elif [ "$( rowset "$NN" )" != "$( rowset "$ND" )" ]; then no "N/never/$tag: dropping e= changed the shown row set" + else ok "N/never/$tag: no e= and no e= clause at the default ceiling, same rows as auto"; fi + if cmp -s "$NXD" "$ND"; then ok "N/auto/$tag: RIPWIRE_FOR_ENDLINES=auto is byte-identical to unset at the default" + else no "N/auto/$tag: auto differs from unset at the default"; fi + [ -s "$ND.err" ] && no "N/unset/$tag: the unset run wrote to stderr: $( head -c 200 "$ND.err" )" + fi +done +nu=0 +for bad in sometimes Always ""; do + NB="$TMP/n.unknown.$nu"; nu=$(( nu + 1 )) + runenv "$bad" "$NB" "$NQ" + if ran_ok "$NB" "N/unknown '$bad'"; then + if ! cmp -s "$NB" "$TMP/n.unset.xml"; then no "N/unknown '$bad': stdout differs from unset (an unknown value must fall back to auto)" + elif ! has_e "$NB"; then no "N/unknown '$bad': no e= at the default (read as never)" + elif [ "$( grep -c 'RIPWIRE_FOR_ENDLINES' "$NB.err" )" != 1 ]; then no "N/unknown '$bad': expected exactly one stderr line naming RIPWIRE_FOR_ENDLINES, got: $( head -c 300 "$NB.err" )" + else ok "N/unknown '$bad': falls back to auto (stdout == unset, e= kept) and says so once on stderr"; fi + fi +done +for v in always never; do + NM="$TMP/n.mcp.$v.json"; mkdir -p "$TMP/mcphome" + if [ "$v" = always ]; then bargs=',"budget_tokens":1200'; else bargs=''; fi + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":"'"$CORPUS/ts"'","task":"How is a per-request context created?"'"$bargs"'}}}' \ + | ( cd "$CORPUS/ts" && TMPDIR="$TMP/mcphome" RIPWIRE_FOR_ENDLINES="$v" "$BIN" --mcp >"$NM" 2>/dev/null ); printf '%s' "$?" >"$NM.rc" + if ran_ok "$NM" "N/mcp/$v"; then + python3 - "$TMP/rows.py" "$NM" "$v" <<'PY' +import json, re, sys +exec( open( sys.argv[ 1 ] ).read() ) +v = sys.argv[ 3 ] +lines = [ l for l in open( sys.argv[ 2 ], encoding="utf-8", errors="replace" ) if l.strip() ] +try: + text = json.loads( lines[ -1 ] )[ "result" ][ "content" ][ 0 ][ "text" ] +except Exception as err: + print( " FAIL N/mcp/%s: no readable MCP for answer (%s)" % ( v, err ) ); sys.exit( 1 ) +rows = drows( text ) +withE = [ a for _, a in rows if "e" in a ] +if not rows: + print( " FAIL N/mcp/%s: no rows (premise)" % v ); sys.exit( 1 ) +if v == "always" and not withE: + print( " FAIL N/mcp/always: budget_tokens=1200 rows carry no e=" ); sys.exit( 1 ) +if v == "never" and ( withE or "e= on a d row" in text or "d e= its last line" in text ): + print( " FAIL N/mcp/never: e= or an e= legend clause at the default ceiling" ); sys.exit( 1 ) +print( " PASS N/mcp/%s: MCP for %s (%d of %d rows with e=)" % ( v, "carries e= under budget_tokens=1200" if v == "always" else "carries no e= and no clause", len( withE ), len( rows ) ) ) +PY + [ $? -eq 0 ] || fail=1 + fi +done +NP="$TMP/n.packsig.xml"; ( cd "$CORPUS/c" && RIPWIRE_FOR_ENDLINES=always "$BIN" . --no-cache --pack-signatures >"$NP" 2>"$NP.err" ); printf '%s' "$?" >"$NP.rc" +if ran_ok "$NP" "N/near-miss"; then + if ! grep -q ' rows (premise)" + elif has_e "$NP"; then no "N/near-miss: RIPWIRE_FOR_ENDLINES=always put e= on --pack-signatures rows (the switch is --for only)" + else ok "N/near-miss: RIPWIRE_FOR_ENDLINES=always leaves --pack-signatures rows without e="; fi +fi + echo "(K) the predicates can fail" K="$TMP/k.xml" printf 'x' >"$K" From 069a8dd5d92732aeecc77fc8be21fcc60f0f5915 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 11:55:16 -0400 Subject: [PATCH 114/229] resolve (assumes #373): Ruby's own method lookup is proof, never via="name" On top of PR #373's RubySelfReach: RubySelfReach::proves / RubyTopSelf::proves say whether self's lookup PROVES a candidate (a class or module the tree defines, in reach, and a reach no delegator or method_missing opens; a top-level def while no outside ancestor answers first). The name ladder's survivors it proves are the call's answer and set languageLookupProved, so FE-B never hedges them; #373's external refusals stand where the two disagree. receiverevidencecheck gains three rb arms (a mixin's method, a concern calling its includer's method, a template method calling a subclass hook) that are red on FE-B without this commit. rubyreachcheck's 18 positive edges: 0 carry via="name" here. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 41 +++++++++++++++++++ test/receiverevidencecheck.sh | 5 +++ .../rb/lib/app/auditable.rb | 8 ++++ test/receiverevidencefix/rb/lib/app/order.rb | 22 ++++++++++ 4 files changed, 76 insertions(+) create mode 100644 test/receiverevidencefix/rb/lib/app/auditable.rb create mode 100644 test/receiverevidencefix/rb/lib/app/order.rb diff --git a/src/graph.h b/src/graph.h index 265a90515..7ff770468 100644 --- a/src/graph.h +++ b/src/graph.h @@ -3060,6 +3060,25 @@ struct RubySelfReach return memo.emplace( self, std::move( reach ) ).first->second; } + // FE-B: does self's lookup PROVE candidate `c` — a class or module the tree defines, in reach, and a reach no + // delegator or method_missing opens (an open reach admits everything, which proves nothing) + bool proves( const IngestResult& ing, const std::string& self, NodeId c ) const + { + const Reach& reach = reachOf( self ); + if( reach.open ) + { + return false; + } + const auto q = qualifiedOwner.find( c ); + const std::string& owner = q != qualifiedOwner.end() ? q->second : ing.symbols[ c ].scope; + if( owner.empty() ) + { + return !reach.external; // a top-level def: a private method of Object, reached while no outside ancestor answers first + } + const bool known = q != qualifiedOwner.end() || classNames.find( owner ) != classNames.end(); + return known && reach.names.find( owner ) != reach.names.end(); + } + // The candidates of `ids` self's lookup can reach, into `out` (cleared first); true when any was left out. A candidate // owned by no Ruby class or module the tree defines is reachable; a top-level def (a private method of Object) only // while every ancestor in reach is in the tree — an out-of-tree one (ActionController::Base) is found first by Ruby. @@ -3195,6 +3214,11 @@ struct RubyTopSelf return !call || kind == Kind::None ? nullptr : kind == Kind::Main ? &kMain : &kView; } static bool owns( const std::string& self ) noexcept { return self == "
" || self == ""; } + // FE-B: a class's self is RubySelfReach's proof; outside any class nothing is proven (a top-level script's call) + bool proves( const std::string& self, NodeId c ) const + { + return !owns( self ) && reach.proves( ing, self, c ); + } // The candidates of `ids` self reaches, into `out` (cleared first); true when any was left out. A class's self is // RubySelfReach's to read; for "
" and "", a candidate owned by no Ruby class or module the tree defines is @@ -6662,6 +6686,23 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // candidate to the same rule, because one candidate is not evidence. bool nameOnly = false; bool nameOnlyAnyFile = false, nameOnlyAnyDir = false; // which rung the classic ladder would have stopped at (rank weight) + // TRIAL (#373): the candidates Ruby's own lookup PROVES are the call's answer, like any other proof + if( hedgeEligible && !narrowed && rubySelf != nullptr && !cand.empty() ) + { + provedScratch.clear(); + for( NodeId c : cand ) + { + if( rubyTop.proves( *rubySelf, c ) ) + { + provedScratch.push_back( c ); + } + } + if( !provedScratch.empty() ) + { + cand.swap( provedScratch ); + languageLookupProved = true; + } + } if( hedgeEligible && !narrowed && !languageLookupProved && !cand.empty() && r.lang != Lang::Elixir ) { provedScratch.clear(); diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index 7c4612011..d327da22a 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -490,6 +490,11 @@ exactproven swift callees Sources/App/Logger.swift:close "fn flush Sources/App/L # whichever lands first visible rb callees lib/app/logger.rb:line "stamp lib/app/helpers.rb" notproven rb callees lib/app/logger.rb:line "render lib/app/exporter.rb" +# Ruby's own lookup (RubySelfReach, PR #373) is PROOF: what it proves comes out resolved, never via="name" — a mixin's +# method, a concern's call to its includer's method, a template method's call to a subclass's hook +exactproven rb callees lib/app/logger.rb:line "method stamp lib/app/helpers.rb" +exactproven rb callees lib/app/auditable.rb:log_audit "method audit_target lib/app/order.rb" +exactproven rb callees lib/app/order.rb:render_report "method header_line lib/app/order.rb" exactproven rb callees lib/app/logger.rb:close "method flush lib/app/logger.rb;method reset lib/app/logger.rb;method top_level_note lib/app/notes.rb" echo "=== (O) graded false callees from an earlier binary (Go, TS, Python repros): red where still present, pinned clean otherwise ===" diff --git a/test/receiverevidencefix/rb/lib/app/auditable.rb b/test/receiverevidencefix/rb/lib/app/auditable.rb new file mode 100644 index 000000000..5b96af683 --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/auditable.rb @@ -0,0 +1,8 @@ +module App + # A concern calls a method its INCLUDER defines: Ruby's lookup proves it (self is an includer instance). + module Auditable + def log_audit + audit_target + end + end +end diff --git a/test/receiverevidencefix/rb/lib/app/order.rb b/test/receiverevidencefix/rb/lib/app/order.rb new file mode 100644 index 000000000..6ed4df299 --- /dev/null +++ b/test/receiverevidencefix/rb/lib/app/order.rb @@ -0,0 +1,22 @@ +module App + class Order + include Auditable + + def audit_target + "order" + end + end + + # A template method: the base calls a hook only its subclass defines. + class Report + def render_report + header_line + end + end + + class PdfReport < Report + def header_line + "pdf" + end + end +end From 07c17771750b7a52fa9e3f72cfd3a4e5c75e15ea Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 4 Oct 2026 12:26:35 -0400 Subject: [PATCH 115/229] model (assumes #373): the Ruby typed receiver rides explicit Reference fields, not recvVar #373 wrote a Ruby receiver the code builds into Reference::recvVar as an in-band "." token (":." for a FactoryBot build). It now rides memberCtor (the class or factory, as written), memberVia (the building method) and memberFactory, set by ingest_binds.h rubyTypeReceiver; recvVar stays empty, and rubyTypedRecvOf / rubyConstantReceiver read the fields. Format: ref record 54 -> 59 bytes inside FE-B's kCacheVersion 29 (cachefuzzcheck's walker follows). Verified byte-identical: the --top-k=100000 --legend=full map and the pin census of all 13 Ruby fixture roots, before and after; the 18 Ruby gates and receiverevidencecheck pass. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 2 +- src/ingest_binds.h | 15 ++++++++++++--- src/ingest_cache.h | 14 +++++++++----- src/ingest_model.h | 2 ++ src/model.h | 31 ++++++++++++++----------------- test/cachefuzzcheck.sh | 2 +- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/graph.h b/src/graph.h index 7ff770468..15d001be0 100644 --- a/src/graph.h +++ b/src/graph.h @@ -3656,7 +3656,7 @@ inline bool rubyIsConstantName( std::string_view name ) noexcept inline std::string_view rubyConstantReceiver( const Reference& r ) noexcept { const bool constant = r.lang == Lang::Ruby && r.role == RefRole::Call && r.recv == RecvKind::NamedVar && r.qualifier.empty() - && rubyIsConstantName( r.recvVar ) && r.recvVar.find( kRubyTypedRecvSep ) == std::string::npos; + && rubyIsConstantName( r.recvVar ) && !rubyTypedRecvOf( r ); const std::string_view written = !constant ? std::string_view {} : r.fieldName.empty() ? std::string_view( r.recvVar ) : std::string_view( r.fieldName ); return written.substr( written.starts_with( "::" ) ? 2 : 0 ); } diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 36606e285..c051a2b4a 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -800,6 +800,15 @@ inline bool rubyNamingChild( TSNode n, const char* t, TSNode c ) noexcept return kindIs( t, "setter" ) || kindIs( t, "alias" ) || kindIs( t, "undef" ); } +// FE-B on #373: a Ruby receiver the code builds, typed in the reference's own fields (model.h rubyTypedRecvOf) +inline void rubyTypeReceiver( RawRef& ref, std::string_view type, std::string_view via, bool factory ) +{ + ref.recvVar.clear(); + ref.memberCtor.assign( type ); + ref.memberVia.assign( via ); + ref.memberFactory = factory; +} + // ─── Ruby's TYPED receiver (test/rubytypedrecvcheck.sh, parser version 132) ─────────────────────────────────────────── // A receiver the code BUILDS is an instance of one class, and Ruby's method lookup on that class decides which `def` a call // on it reaches (graph.h rubyTypedReceiver). What builds one is read from what the expression IS: `Const.new( … )` and the @@ -810,7 +819,7 @@ inline bool rubyNamingChild( TSNode n, const char* t, TSNode c ) noexcept // binding in its scope builds the same one, or the let/subject RSpec runs for that name there — the innermost definition, // a nested group's override included — where a group with no subject of its own has its parent's, and at the top // `described_class.new`. RubyBareCallWalk reads all of it on the walk it already makes, beside the locals it tracks, and -// typeReceivers writes each type into its call reference (model.h rubyTypedRecvToken). Floors (test/rubytypedrecvcheck.sh): +// typeReceivers writes each type into its call reference (rubyTypeReceiver above; model.h rubyTypedRecvOf). Floors (test/rubytypedrecvcheck.sh): // control flow is not read (one binding of another shape anywhere in the scope, a parameter or a block parameter // included, leaves the local untyped); instance variables, return values and a shared context's lets are not typed. // RSpec's own builders are read the same way (test/rubyrspectargetcheck.sh, parser version 133): inside an example @@ -1378,7 +1387,7 @@ inline void RubyBareCallWalk::typeReceivers( std::size_t firstRef, std::size_t e { if( const RubyTypedSite* site = siteOf( refs_[ i ] ) ) { - refs_[ i ].recvVar = rubyTypedRecvToken( site->type.type, site->type.via, site->type.factory ); + rubyTypeReceiver( refs_[ i ], site->type.type, site->type.via, site->type.factory ); } } } @@ -1503,7 +1512,7 @@ inline void typeRubyJbuilderLocal( std::uint32_t fileId, std::vector& re { if( refs[ i ].role == RefRole::Call && refs[ i ].recv == RecvKind::NamedVar && refs[ i ].recvVar == kJbuilderLocal ) { - refs[ i ].recvVar = rubyTypedRecvToken( kJbuilderTemplate, kJbuilderLocal, false ); + rubyTypeReceiver( refs[ i ], kJbuilderTemplate, kJbuilderLocal, false ); } } } diff --git a/src/ingest_cache.h b/src/ingest_cache.h index bfbed4059..7e8b49658 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -87,6 +87,8 @@ struct RawRef std::string memberRoot; // FE-A: the receiver chain's ROOT identifier when memberCall (`JSON` in `JSON.parse()`); see model.h std::string memberPath; // FE-B: the member names between memberRoot and the callee, '.'-joined ("" = `root.m()`); see model.h std::string memberCtor; // FE-B: the class a constructed receiver names (`new X().m()`, Go `X{}.m()`); see model.h + std::string memberVia; // FE-B on #373: the method a Ruby typed receiver was built by (`new`, a finder); see model.h + bool memberFactory = false; // FE-B on #373: memberCtor names a FactoryBot factory, not a class }; // P2-D Rule 2 raw local-variable type binding (pre-attribution). startByte sits inside the enclosing @@ -137,7 +139,9 @@ constexpr std::uint32_t kCacheMagic = 0x4b505443; // "CTPK" // with — a v2 cache simply misses on every lookup that survives the guard, which is exactly the // self-healing full-reparse path already used for any other corrupt/stale cache. constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiverevidencecheck.sh) — RawRef gains `memberPath` (str) and - // `memberCtor` (str) after `memberRoot` (ref record 46 -> 54 bytes lean), + // `memberCtor` (str) after `memberRoot` (ref record 46 -> 54 bytes lean; + // on #373 also `memberVia` (str) + `memberFactory` (u8), 54 -> 59: the Ruby + // typed receiver moved out of recvVar — same version, one FE-B format), // and RawBind gains four APPENDED kinds (RecvType, MemberType, // MethodAlias, NameAlias). A FORMAT change: the version guard rejects a v28 blob. // 28: FE-A (test/falseedgecheck.sh) — RawRef gains `memberCall` (u8) and @@ -411,7 +415,7 @@ constexpr std::uint32_t kParserVer = 150; // bump on any grammar/.sc // 132 = 2026-10-02 (test/rubytypedrecvcheck.sh): a Ruby call // receiver the file BUILDS (`Client.new`, a finder, a FactoryBot // build, a let or local holding one) carries its type in the - // call RawRef's recvVar (model.h rubyTypedRecvToken), and each + // call RawRef's recvVar (FE-B on #373 moved it to memberCtor/memberVia), and each // FactoryBot `factory` mints a RawBind of the APPENDED kind // LocalBindKind::RubyFactory (ingest_binds.h). Same layout: // kCacheVersion unchanged (27). A Ruby cache written at 131 @@ -2233,7 +2237,7 @@ inline void writeDef( ByteW& w, const RawDef& d, bool withLex, std::size_t fileD } } } -inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); w.str( r.memberPath ); w.str( r.memberCtor ); } +inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8( std::uint8_t( r.lang ) ); w.str( r.name ); w.u8( r.isInherit ? 1 : 0 ); w.u8( r.isDocLink ? 1 : 0 ); w.str( r.qualifier ); w.u8( std::uint8_t( r.recv ) ); w.str( r.recvVar ); w.u8( r.isCompose ? 1 : 0 ); w.str( r.fieldName ); w.str( r.composeRel ); w.u8( std::uint8_t( r.role ) ); w.u32( r.line ); w.u32( r.argCount ); w.u8( r.argCountKnown ? 1 : 0 ); w.u8( r.viaArrow ? 1 : 0 ); w.u8( r.qualifierRootsStd ? 1 : 0 ); w.u8( r.memberCall ? 1 : 0 ); w.str( r.memberRoot ); w.str( r.memberPath ); w.str( r.memberCtor ); w.str( r.memberVia ); w.u8( r.memberFactory ? 1 : 0 ); } // loadCache's countFits() bounds a corrupt on-disk record COUNT against remaining bytes / // minRecordBytes BEFORE reserve() — the guard that keeps a hostile blob's 0xFFFFFFFF count from reaching @@ -2259,7 +2263,7 @@ inline void writeRef( ByteW& w, const RawRef& r ) { w.u32( r.startByte ); w.u8 // can't silently stale them. inline constexpr std::size_t kMinDefRecordBytesLean = 88; // 16×u32 + 16×u8 + 2×str(len u32, empty) inline constexpr std::size_t kMinDefRecordBytesRichExtra = 9; // v10 rich withLex extra: dlWeighted u32 + tokenCount u32 + tfWidth u8 -inline constexpr std::size_t kMinRefRecordBytes = 54; // 3×u32 + 10×u8 + 8×str(len u32, empty) +inline constexpr std::size_t kMinRefRecordBytes = 59; // 3×u32 + 11×u8 + 9×str(len u32, empty) inline std::size_t minDefRecordBytes( bool captureValueUses ) noexcept { @@ -2367,7 +2371,7 @@ inline RawDef readDef( ByteR& r, bool withLex, const std::vector& } return d; } -inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); x.memberPath = r.str(); x.memberCtor = r.str(); return x; } +inline RawRef readRef ( ByteR& r ) { RawRef x; x.startByte = r.u32(); x.lang = r.enumU8( kLangCount ); x.name = r.str(); x.isInherit = r.u8() != 0; x.isDocLink = r.u8() != 0; x.qualifier = r.str(); x.recv = r.enumU8( kRecvKindCount ); x.recvVar = r.str(); x.isCompose = r.u8() != 0; x.fieldName = r.str(); x.composeRel = r.str(); x.role = r.enumU8( kRefRoleCount ); x.line = r.u32(); x.argCount = r.u16Of32(); x.argCountKnown = r.u8() != 0; x.viaArrow = r.u8() != 0; x.qualifierRootsStd = r.u8() != 0; x.memberCall = r.u8() != 0; x.memberRoot = r.str(); x.memberPath = r.str(); x.memberCtor = r.str(); x.memberVia = r.str(); x.memberFactory = r.u8() != 0; return x; } inline void writeBind( ByteW& w, const RawBind& b ) { w.u32( b.startByte ); w.u8( std::uint8_t( b.lang ) ); w.u8( std::uint8_t( b.kind ) ); w.u8( b.isFromAssignment ? 1 : 0 ); w.u32( b.spanStart ); w.u32( b.spanEnd ); w.str( b.var ); w.str( b.typeName ); w.str( b.importedName ); } inline RawBind readBind( ByteR& r ) { RawBind b; b.startByte = r.u32(); b.lang = r.enumU8( kLangCount ); b.kind = r.enumU8( kLocalBindKindCount ); b.isFromAssignment = r.u8() != 0; b.spanStart = r.u32(); b.spanEnd = r.u32(); b.var = r.str(); b.typeName = r.str(); b.importedName = r.str(); return b; } inline void writeFfi( ByteW& w, const BindingAlias& a ) { w.u8( std::uint8_t( a.kind ) ); w.u8( a.lowConf ? 1 : 0 ); w.str( a.aliasName ); w.str( a.targetName ); w.str( a.targetScope ); } diff --git a/src/ingest_model.h b/src/ingest_model.h index cf68040b6..2b96bf79a 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -920,6 +920,8 @@ inline void emitReferences( IngestResult& result, std::vector& rawRefs, ref.memberRoot = std::move( r.memberRoot ); // FE-A: its receiver chain's root identifier ref.memberPath = std::move( r.memberPath ); // FE-B: the members between that root and the callee ref.memberCtor = std::move( r.memberCtor ); // FE-B: the class a constructed receiver names + ref.memberVia = std::move( r.memberVia ); // FE-B on #373: the method a Ruby typed receiver was built by + ref.memberFactory = r.memberFactory; // FE-B on #373: memberCtor names a FactoryBot factory ref.fieldName = std::move( r.fieldName ); // S5-E: the member variable name (e.g. "m_pool") ref.composeRel = std::move( r.composeRel ); // S5-E: "creates" or "uses" ref.startByte = r.startByte; // shadow fix round: for the block-span containment test diff --git a/src/model.h b/src/model.h index c7fc3eb62..f765d7ebf 100644 --- a/src/model.h +++ b/src/model.h @@ -744,7 +744,7 @@ struct Reference std::string calleeName; // referenced name (final identifier segment) std::string qualifier; // explicit scope at the call site (`A` in `A::b()`); "" if bare/method — for canonical resolve std::string recvVar; // receiver variable identifier when recv==NamedVar/FieldOfVar (`x` in `x->m()`); "" otherwise — for Rule 2 - // A Ruby receiver the code BUILDS carries its type here instead (rubyTypedRecvOf below). + // A Ruby receiver the code BUILDS leaves it empty: its type is memberCtor/memberVia (rubyTypedRecvOf). std::string fieldName; // member variable name when isCompose (e.g. "m_pool"); ALSO the INTERMEDIATE // field of a depth-2 chained receiver when recv is FieldOfThis/FieldOfVar // (`this->m_pool.run()` → "m_pool") — the two are mutually exclusive @@ -772,17 +772,21 @@ struct Reference // other shape and every language that records no member-call shape. std::string memberPath; std::string memberCtor; + // FE-B on #373: a Ruby receiver the code BUILDS (`User.new`, a finder, a FactoryBot build, a let or local holding one) + // carries its type here, out of band: memberCtor = the class's constant as written (or the factory name when + // memberFactory), memberVia = the method that built it. recvVar is then empty. Read through rubyTypedRecvOf. + std::string memberVia; + bool memberFactory = false; }; // A Ruby call whose RECEIVER the code builds (parser version 132, test/rubytypedrecvcheck.sh): `c = Client.new` then `c.get`, // `let( :user ) { create( :user ) }` then `user.activate!`, `User.find_by( … ).activate!`. ingest_binds.h reads the shape -// by Ruby's own local rule and writes the type into Reference::recvVar as "." — the class's constant as written -// and the method that built it (`User.find_by`, `OpenSSL::Cipher.new`) — or ":." for a FactoryBot build (`:user.create`), whose class -// only the tree's factory definitions know (graph.h rubyFactoryClasses). The receiver KIND is left as it was (NamedVar, -// FieldOfVar), so every rule that does not read the type sees the call exactly as before; and a `.` is in no Ruby -// identifier or constant segment, so no receiver the tool recorded before can read as a type. -inline constexpr char kRubyTypedRecvSep = '.'; - +// by Ruby's own local rule and writes the type into the reference's OWN fields (FE-B on #373: it used to ride recvVar as an +// in-band "." token): Reference::memberCtor = the class's constant as written (`User`, `OpenSSL::Cipher`), or +// the factory name with memberFactory set for a FactoryBot build, whose class only the tree's factory definitions know +// (graph.h rubyFactoryClasses); memberVia = the method that built it (`find_by`, `new`, `create`). recvVar is left empty, +// and the receiver KIND as it was (NamedVar, FieldOfVar), so every rule that does not read the type sees the call exactly +// as before. struct RubyTypedRecv { std::string_view type; // the class's constant as written (`User`, `Admin::User`), or the factory name @@ -790,22 +794,15 @@ struct RubyTypedRecv bool factory = false; // `type` names a FactoryBot factory, not a class }; -inline std::string rubyTypedRecvToken( std::string_view type, std::string_view via, bool factory ) -{ - return std::string( factory ? ":" : "" ).append( type ).append( 1, kRubyTypedRecvSep ).append( via ); -} // the type a Ruby reference's receiver was built as, or nullopt for every other reference inline std::optional rubyTypedRecvOf( const Reference& r ) noexcept { - const std::size_t sep = r.lang == Lang::Ruby ? r.recvVar.find( kRubyTypedRecvSep ) : std::string::npos; - if( sep == std::string::npos ) + if( r.lang != Lang::Ruby || r.memberCtor.empty() || r.memberVia.empty() ) { return std::nullopt; } - const std::string_view token = r.recvVar; - const bool factory = token.starts_with( ':' ); - return RubyTypedRecv { token.substr( factory ? 1 : 0, sep - ( factory ? 1 : 0 ) ), token.substr( sep + 1 ), factory }; + return RubyTypedRecv { r.memberCtor, r.memberVia, r.memberFactory }; } // RSpec's targets (test/rubyrspectargetcheck.sh, parser version 133): inside an example group, the receiver-less builder diff --git a/test/cachefuzzcheck.sh b/test/cachefuzzcheck.sh index 31c0e13e5..8d69d600e 100755 --- a/test/cachefuzzcheck.sh +++ b/test/cachefuzzcheck.sh @@ -931,7 +931,7 @@ for i in range(n): site32("def.ppAlt", 9 * 4); site32("def.params", 13 * 4) skip(14 * 4 + 6 + 2 * 4); site("def.kind"); site("def.lang"); s(); s(); skip(8) # #150: +1 u8 (scopeRootsStd); cache v26: +2 u32 (fnScopeStart/End) before kind for _ in range(u32()): # refs - skip(4); site("ref.lang"); s(); skip(2); s(); site("ref.recv"); s(); skip(1); s(); s(); site("ref.role"); site32("ref.argCount", 4); skip(12); s(); s(); s() # line, argCount, argCountKnown, viaArrow, #150's qualifierRootsStd, FE-A's memberCall u8 + memberRoot str, FE-B's memberPath + memberCtor strs + skip(4); site("ref.lang"); s(); skip(2); s(); site("ref.recv"); s(); skip(1); s(); s(); site("ref.role"); site32("ref.argCount", 4); skip(12); s(); s(); s(); s(); skip(1) # line, argCount, argCountKnown, viaArrow, #150's qualifierRootsStd, FE-A's memberCall u8 + memberRoot str, FE-B's memberPath + memberCtor + memberVia strs, memberFactory u8 for _ in range(u32()): # includes skip(3 + 4 + 1); s() for _ in range(u32()): # binds From c2de8a13e5209338892cfd91cc8ec96d618c81ad Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:15:35 -0400 Subject: [PATCH 116/229] =?UTF-8?q?CHANGELOG:=20FE-B=20on=20train=2026a=20?= =?UTF-8?q?=E2=80=94=20Ruby=20lookup=20proofs=20are=20answers;=20kParserVe?= =?UTF-8?q?r=20150?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b71a87c01..6509ba6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,8 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. import alias of the class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields whose classes are stated (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields); Python `feed = parser.feed`. A JS/TS call on the class object reaches its `static` members only, and a call on an - instance never one. Ruby keeps only the language-neutral proof below (its own method lookup is separate work). + instance never one. A Ruby call's candidates that Ruby's own method lookup proves (the entries below) are its + answer, never `via="name"`. - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an implicit receiver's class and bases, a free function in scope, the module a receiver alias names). With none proven, every same-file and same-directory candidate is listed — a lone global one too, because one candidate is @@ -44,7 +45,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. - Cost, measured over 210 calls on eight public repositories (`--no-cache`): +8.55% bytes in total; 32% of the ``/``/`` rows carry `via="name"`, most of them the map's `` rows (C trees: no change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses - for every evidence rule). Ingest records the receiver chain (`kParserVer` 146, `kCacheVersion` 29), so a cache + for every evidence rule). Ingest records the receiver chain (`kParserVer` 150, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` From 9d0ad7aab5c79f2f96859f279571c471416df5da Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:21:22 -0400 Subject: [PATCH 117/229] test: re-pin estcalib for-named (o200k) for the e= rows, with an e=-free twin tokenbudgetcheck #18 compared for-named's est_tokens (1290 at this head) with an o200k pin of 789 taken 2026-09-12, when est was 1050; it fell outside the band. Re-pinned with bench/tokenaudit/pin.py (tiktoken 0.14.0, o200k_base): for-named 962 o200k / 958 cl100k / est 1290. Only this row moves between the base and this head (every other row tokenizes identically on both), and the whole movement is the e= rows and their legend clause: +237 B, +64 o200k. Twin (no gate erosion): for-named-noe pins the same question with RIPWIRE_FOR_ENDLINES=never, which is byte-identical to the base answer (898 o200k, est 1195). A leading NAME=VALUE manifest token is an environment assignment in both pin.py and #18; every other pin runs with the switch unset. #18d checks that each side shows its own path (e= present / absent), and #18c now requires nine pins. The other rows' pins are left as they were. Co-Authored-By: Claude Opus 5.5 --- bench/tokenaudit/pin.py | 16 ++++++++++++++-- test/estcalib.manifest | 7 +++++-- test/tokenbudgetcheck.sh | 27 +++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/bench/tokenaudit/pin.py b/bench/tokenaudit/pin.py index d57567b27..820950293 100644 --- a/bench/tokenaudit/pin.py +++ b/bench/tokenaudit/pin.py @@ -18,6 +18,7 @@ import argparse import os +import re import shutil import subprocess import sys @@ -34,11 +35,16 @@ ("pack-signatures", ["--pack-signatures"]), ("expand", ["--expand=billableTotal"]), ("for-named", ["--for=billableTotal"]), + # for-named's TWIN (PROCESS rule 4): the same question on the e=-free path. A leading NAME=VALUE token is an + # environment assignment, not argv (tokenbudgetcheck #18 reads it the same way); every other pin runs with + # RIPWIRE_FOR_ENDLINES unset. + ("for-named-noe", ["RIPWIRE_FOR_ENDLINES=never", "--for=billableTotal"]), ("for-budgeted", ["--for=dedupe", "--token-budget=400"]), ("pack-task", ["--pack-task=dedupe"]), ] EST = b'est_tokens="' +ENV_TOKEN = re.compile(r"^[A-Z][A-Z0-9_]*=") def est_of(out: bytes): @@ -66,7 +72,7 @@ def main(): lines = [ "# est_calib pins — REAL tokenizer counts of ripwire's output on test/estcalibfix.", - "# label o200k cl100k est_at_pin_time argv... (space-separated; no argv may contain a space)", + "# label o200k cl100k est_at_pin_time argv... (space-separated; no argv may contain a space; a leading NAME=VALUE is env)", "# Regenerate: python3 bench/tokenaudit/pin.py --bin build/ripwire (needs tiktoken)", "# Read by: test/tokenbudgetcheck.sh #18 — the calibration band. See bench/tokenaudit/README.md.", ] @@ -75,7 +81,13 @@ def main(): shutil.copytree(fixture, os.path.join(tmp, "f")) for label, argv in PINNED: # the pinned counts describe the FULL-legend document (tokenbudgetcheck #18 asks for the same posture) - p = subprocess.run([binpath, "f"] + argv + ["--legend=full"], capture_output=True, cwd=tmp) + n = 0 + while n < len(argv) and ENV_TOKEN.match(argv[n]): + n += 1 + envs, args = argv[:n], argv[n:] + env = {k: v for k, v in os.environ.items() if k != "RIPWIRE_FOR_ENDLINES"} + env.update(a.split("=", 1) for a in envs) + p = subprocess.run([binpath, "f"] + args + ["--legend=full"], capture_output=True, cwd=tmp, env=env) if p.returncode != 0: print("pin.py: '%s' exited %d" % (label, p.returncode), file=sys.stderr) return 1 diff --git a/test/estcalib.manifest b/test/estcalib.manifest index a745e590f..a919919c8 100644 --- a/test/estcalib.manifest +++ b/test/estcalib.manifest @@ -1,5 +1,5 @@ # est_calib pins — REAL tokenizer counts of ripwire's output on test/estcalibfix. -# label o200k cl100k est_at_pin_time argv... (space-separated; no argv may contain a space) +# label o200k cl100k est_at_pin_time argv... (space-separated; no argv may contain a space; a leading NAME=VALUE is env) # Regenerate: python3 bench/tokenaudit/pin.py --bin build/ripwire (needs tiktoken) # Read by: test/tokenbudgetcheck.sh #18 — the calibration band. See bench/tokenaudit/README.md. map 1009 999 1123 @@ -7,6 +7,9 @@ map-topk5 538 535 653 --top-k=5 metrics 2381 2370 2509 --metrics pack-signatures 1801 1784 2116 --pack-signatures expand 541 542 455 --expand=billableTotal -for-named 789 785 1050 --for=billableTotal +# for-named re-pinned 2026-10-07: its --for rows now carry e= (+237 B, +64 o200k over its twin); +# for-named-noe is its e=-free twin (RIPWIRE_FOR_ENDLINES=never, byte-identical to the pre-e= answer). Other rows: 2026-09-12 pins. +for-named 962 958 1290 --for=billableTotal +for-named-noe 898 893 1195 RIPWIRE_FOR_ENDLINES=never --for=billableTotal for-budgeted 419 415 598 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe diff --git a/test/tokenbudgetcheck.sh b/test/tokenbudgetcheck.sh index 371552d43..ef91a893c 100755 --- a/test/tokenbudgetcheck.sh +++ b/test/tokenbudgetcheck.sh @@ -361,7 +361,25 @@ else # L1 (2026-09-19): the o200k/cl100k counts were pinned (bench/tokenaudit/pin.py, needs tiktoken) on the FULL-legend # documents, the default when they were taken; the CLI default is compact now, so the pinned argv asks for the # document the counts describe. Re-pinning the default posture needs tiktoken — recorded in the L1 lane report. - EST_OUT="$( cd "$EST_TMP" && "$BIN" f $rest --legend=full 2>/dev/null )" + # A LEADING NAME=VALUE token is an environment assignment (pin.py's ENV_TOKEN), not argv: for-named-noe, the + # e=-free twin of for-named, sets RIPWIRE_FOR_ENDLINES=never. Every pin runs with that switch otherwise UNSET, so + # an ambient value cannot move a pin. ${a[@]+...} keeps an empty array legal under set -u on bash 3.2. + est_env=(); est_args=() + for est_tok in $rest; do + if [ "${#est_args[@]}" -eq 0 ] && printf '%s' "$est_tok" | grep -Eq '^[A-Z][A-Z0-9_]*='; then est_env+=( "$est_tok" ); else est_args+=( "$est_tok" ); fi + done + EST_OUT="$( cd "$EST_TMP" && env -u RIPWIRE_FOR_ENDLINES ${est_env[@]+"${est_env[@]}"} "$BIN" f ${est_args[@]+"${est_args[@]}"} --legend=full 2>/dev/null )" + # #18d TWIN PREMISE: for-named prices the e= rows the lane added and for-named-noe the e=-free answer it twins; a + # twin whose env token was dropped would silently re-measure for-named, so each must show its own side. + case "$label" in + for-named) + if printf '%s' "$EST_OUT" | grep -Eq ' rows (premise)" + elif printf '%s' "$EST_OUT" | grep -Eq ' e="[0-9]+"'; then no "#18d for-named-noe: the twin carries e= — RIPWIRE_FOR_ENDLINES=never did not reach the binary" + else ok "#18d for-named-noe: the twin is the e=-free answer (RIPWIRE_FOR_ENDLINES=never)"; fi ;; + esac EST_GOT="$( printf '%s' "$EST_OUT" | grep -oE 'est_tokens="[0-9]+"' | head -1 | grep -oE '[0-9]+' )" if [ -z "$EST_GOT" ]; then no "#18 $label: no est_tokens in the output — the pin says this verb prices itself" @@ -393,9 +411,10 @@ else fi fi - # #18c MUTATION CONTROL. Eight pins are committed; a loop that measured fewer (a manifest truncated by - # a merge, a verb that stopped printing est_tokens) asserted less than the PASS lines above suggest. - if [ "$est_pins" -ge 8 ]; then + # #18c MUTATION CONTROL. Nine pins are committed (eight verbs + for-named's e=-free twin); a loop that measured + # fewer (a manifest truncated by a merge, a verb that stopped printing est_tokens) asserted less than the PASS + # lines above suggest. + if [ "$est_pins" -ge 9 ]; then ok "#18c $est_pins pins measured against the tokenizer manifest" else no "#18c only $est_pins pin(s) measured — the band arms above asserted almost nothing" From d21998fc98273c85bfe9302cdd52c2668fade943 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:25:13 -0400 Subject: [PATCH 118/229] test: qschemetrip re-pinned for kParserVer 150 / kCacheVersion 29 on train 26a (logged) Co-Authored-By: Claude Opus 5.5 --- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 373536ec8..cf28b9b28 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -9fe75d7e33ccde42c2d7f3413786598e09784269d7378a520127681836283213 +abf245ceffbd202cc04f8ec62ea0085e72645190b9eef989b834c8d90c678aeb diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 60dd409fc..745e609c2 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,9 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-07, lane FE-B merged onto train 26a (cd87e30e): RE-DERIVED with UPDATE_GOLDEN=1 (hash abf245ceff…678aeb). +# kParserVer 148 -> 150 (FE-B's receiver chain + bind kinds on top of #373; 149 is 148's rich tag), kCacheVersion +# 28 -> 29 (FE-B's ref record), quality.h mirrors with them. kQSnapCacheScheme stays 17. The train renumbers if needed. # 2026-10-04, lane/fe-b-receiver-evidence: RE-DERIVED with UPDATE_GOLDEN=1 (hash 9e06e28ba5…36253dbb). kParserVer 141 -> 146 # (above train 25's 143/144; the ref record gains the receiver chain memberPath/memberCtor and the receiver-evidence # binding kinds), kCacheVersion 28 -> 29; quality.h's mirrors move with them. kQSnapCacheScheme stays 17: a snapshot's meaning is unchanged, and the From 3b709a2408baf7c54d3fd5d9e98bbc867d4a0495 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:36:13 -0400 Subject: [PATCH 119/229] =?UTF-8?q?test:=20the=20=20floor=20twin=20?= =?UTF-8?q?=E2=80=94=20at=20the=20rank=201..4=20floor=20next=3D=20ships,?= =?UTF-8?q?=20labelled;=20the=20old=20bound=20holds=20for=20the=20rest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estchargecheck #11 A7 and its sweep accept a document past the allowance only as the disclosed floor twin: root over_ceiling="1", the floor clause, capped at shown<=4 with next=, est_tokens inside the band, and the document WITHOUT the handle and its label still within the allowance (the old bound, intact). A dedicated arm exercises the twin every run on the git-less corpus (@790), with a near miss where the handle is paid (@850: inside the allowance, no label). compactlegendcheck P4 keeps its four budgets; its twin pins the floor at 500 (both dialects labelled, est_tokens priced per P3, compact no further over than full) and the paid case at 750. Both twins are RED on the base binary (no next= on a capped ). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 11 +++--- test/compactlegendcheck.sh | 38 +++++++++++++++++++++ test/estchargecheck.sh | 70 ++++++++++++++++++++++++++++++++++---- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dcb20fd5..a21b6ab06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,9 +29,11 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal the `budget_tokens` a re-call needs, beside the same `next_offset=`. Present-only legend clauses define each. At the default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, they are exempt from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) - `next=` rides alone and is charged inside the block, and a compact answer pays from the full dialect's row budget, so - `est_tokens` stays within the budget; measured at `--token-budget=2000` on 20 queries, that costs one row on 11 — a row - the `next=` itself serves. No budget, ceiling or token conversion changed. + `next=` rides alone and is paid from the block's rows, and a compact answer is cut to the rows the full one keeps, so + the answer stays within the budget wherever it can fit; measured at `--token-budget=2000` on 20 queries, that costs one + row on 11 — a row the `next=` itself serves. At the rank 1..4 floor, where no row is left to pay, `next=` still ships + (the call that recovers a cut is never the thing cut) and, when the answer then lands past its ceiling, the root says + `over_ceiling="1"` with a clause naming the cause. No budget, ceiling or token conversion changed. - **`--lint` / `--lint-rules` and `--sarif`.** A rule that spends its per-rule match budget (`count_capped="1"`) now makes the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`): only the floored rules, under a 10x budget, through the new `--lint-max-per-rule=N` (the default stays 5000). A re-run still floored names its own. @@ -39,7 +41,8 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal selector lost its only continuation with no marker. It is now always emitted in full, like every other `next=`. Also: the `ripwire-handoff` skill passed `--top-k=20` to `--for`, which does not read it; it now spells -`--signatures-only`. Gates: `forrankordercheck` (9), `lintbudgetcheck` (5), `impactimportcheck` (#9d). +`--signatures-only`. Gates: `forrankordercheck` (9), `lintbudgetcheck` (5), `impactimportcheck` (#9d), `estchargecheck` +(#11 A7 floor twin), `compactlegendcheck` (P4 floor twin). ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 9a103f962..df06985a1 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -843,6 +843,44 @@ for tb in 850 1000 1100 1200; do fi done [ "$p4bad" -eq 0 ] && ok "(P4) at --token-budget 850/1000/1100/1200 the compact --for answer is inside its budget or no further over it than the full one" +# (P4 floor twin — knob-honesty-068; orchestrator ruling 2026-10-07, PROCESS rules 4/5) A capped under a hard ceiling +# names the call that recovers its cut (next=) and pays for it from its rows. At 500 tokens this fixture's block is at its +# rank 1..4 floor in BOTH dialects and cannot pay: the recovery handle still ships, and the overshoot is DISCLOSED — root +# over_ceiling="1" with the floor clause defining it, est_tokens pricing the bytes delivered (P3's rule), and the P4 claim +# itself (compact no further over than full). Near miss at 750: the handle is paid — est_tokens <= budget in both dialects, +# next= rides, no over_ceiling= and no floor clause (a label riding every capped answer would pass the 500 half alone). +p4floor_bad=0 +for tb in 500 750; do + for lg in compact full; do + ( cd "$P4" && "$BIN" . --for="widget routine dispatcher" --token-budget=$tb --legend=$lg 2>/dev/null ) >"$TMP/p4f.$lg" + done + pc="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.compact" | head -1 | tr -dc '0-9' )"; pf="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.full" | head -1 | tr -dc '0-9' )" + for lg in compact full; do + f="$TMP/p4f.$lg"; pb="$( wc -c <"$f" | tr -d ' ' )"; pe="$( grep -o 'est_tokens="[0-9]*"' "$f" | head -1 | tr -dc '0-9' )" + root="$( grep -oE '^]*>' "$f" | head -1 )"; sigs="$( grep -oE ']*>' "$f" | head -1 )" + hasnext=0; case "$sigs" in *' capped="1"'*' next="--for='*) hasnext=1;; esac + over=0; [ "${root#* over_ceiling=\"1\"}" != "$root" ] && over=1 + clause=0; grep -qF '[over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]' "$f" && clause=1 + if [ -z "$pe" ] || [ "$hasnext" -ne 1 ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: est_tokens=${pe:-?}, next= present=$hasnext — a capped block must carry its next=" + continue + fi + if [ "$tb" -eq 500 ]; then + truth=$(( pb * 100 / 250 )); floor=$(( pb * 100 / 255 )); diff=$(( pe - truth )); [ "$diff" -lt 0 ] && diff=$(( -diff )) + shown="$( printf '%s' "$sigs" | grep -oE ' shown="[0-9]+"' | tr -dc '0-9' )" + if [ "$over" -ne 1 ] || [ "$clause" -ne 1 ] || [ "${shown:-99}" -gt 4 ] || [ $(( diff * 100 )) -gt $(( truth * 12 )) ] || [ "$pe" -lt "$floor" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=500 --legend=$lg: over_ceiling=$over clause=$clause shown=${shown:-?} est_tokens=$pe for $pb B — the floor's unpaid next= must ride labelled and honestly priced" + fi + elif [ "$pe" -gt "$tb" ] || [ "$over" -ne 0 ] || [ "$clause" -ne 0 ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) near miss --token-budget=$tb --legend=$lg: est_tokens=$pe over_ceiling=$over clause=$clause — a paid next= must fit with no label" + fi + done + lim=$tb; [ "${pf:-0}" -gt "$lim" ] && lim=$pf + if [ -z "$pc" ] || [ "$pc" -gt "$lim" ]; then + p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=${pc:-?} over the budget AND over the full answer's ${pf:-?}" + fi +done +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= at the rank 1..4 floor with over_ceiling=\"1\", the floor clause and an honest est_tokens; at 750 the paid next= fits with no label; compact never further over than full" # (P5) the BUDGET LEDGER survives compaction (orchestrator rule, METHODOLOGY §9.3/§9.4: never cut silently). --pack-task's # full legend ends with "budget=N bytes (T-token target, ceiling C) | ranking: … | bodies: … | callers: … | notes: … | tests: diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 82d1100da..04e8eacf6 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -585,9 +585,42 @@ ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --no-cache >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? FTB="$( bytes_of "$TMP/f_tb.out" )" -{ [ "$rc_ftb" -eq 0 ] && [ "$FTB" -le "$ALLOW" ]; } 2>/dev/null \ - && ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B within the $ALLOW B allowance (exit 0, SHAPED)" \ - || no "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B vs the $ALLOW B allowance, exit $rc_ftb — the budget does not bound the appended sections" +# THE FLOOR TWIN (knob-honesty-068; orchestrator ruling 2026-10-07, PROCESS rules 4/5). A capped under a hard +# ceiling carries next= — the call that recovers its cut — and pays for it from its rows. At the rank 1..4 floor nothing is +# left to shed, and the recovery handle ALWAYS ships: the document may then land past the allowance, and only if it says so. +# This is not a wider allowance: every condition of the old bound still holds for everything except the disclosed handle. +# sigs_floor_twin FILE ALLOW → exit 0 iff ALL of: the root carries over_ceiling="1"; the legend carries the floor clause +# defining it; is capped at shown<=4 (the floor) and carries next="--for=…"; est_tokens is positive and prices the +# delivered bytes inside the 2.00-4.20 B/tok band (honest: it moved with the handle); and the document WITHOUT the handle +# and its label (the next= attribute, over_ceiling="1", the clause) is within ALLOW — the old bound, intact. +sigs_floor_twin(){ python3 - "$1" "$2" <<'PYF' +import sys, re +d = open( sys.argv[ 1 ], 'rb' ).read().decode( 'utf-8', 'replace' ); allow = int( sys.argv[ 2 ] ) +clause = ' [over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]' +root = re.search( r']*>', d ); sigs = re.search( r']*>', d ) +why = [] +if not root or ' over_ceiling="1"' not in root.group( 0 ): why.append( 'no root over_ceiling="1"' ) +if clause not in d: why.append( 'no floor clause' ) +nxt = re.search( r' next="--for=[^"]*"', sigs.group( 0 ) ) if sigs else None +shown = re.search( r' shown="(\d+)"', sigs.group( 0 ) ) if sigs else None +if not sigs or ' capped="1"' not in sigs.group( 0 ) or not shown or int( shown.group( 1 ) ) > 4: why.append( ' not capped at its floor' ) +if not nxt: why.append( ' carries no next=' ) +est = re.search( r' est_tokens="(\d+)"', root.group( 0 ) ) if root else None +b = len( d.encode( 'utf-8' ) ) +if not est or int( est.group( 1 ) ) <= 0 or not ( 200 <= b * 100 // int( est.group( 1 ) ) <= 420 ): why.append( 'est_tokens absent or out of band' ) +rest = b - ( len( nxt.group( 0 ) ) if nxt else 0 ) - len( ' over_ceiling="1"' ) - len( clause ) +if rest > allow: why.append( f'{rest} B without the handle > {allow} B' ) +print( '; '.join( why ) if why else f'{b} B, {rest} B without the handle <= {allow} B, est_tokens={est.group( 1 )}' ) +sys.exit( 1 if why else 0 ) +PYF +} +if { [ "$rc_ftb" -eq 0 ] && [ "$FTB" -le "$ALLOW" ]; } 2>/dev/null; then + ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B within the $ALLOW B allowance (exit 0, SHAPED)" +elif [ "$rc_ftb" -eq 0 ] && a7why="$( sigs_floor_twin "$TMP/f_tb.out" "$ALLOW" )"; then + ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: the floor twin — $a7why, labelled over_ceiling=\"1\" with next= (exit 0, SHAPED)" +else + no "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B vs the $ALLOW B allowance, exit $rc_ftb — the budget does not bound the appended sections${a7why:+ (floor twin: $a7why)}" +fi # and without an explicit --token-budget the bodies keep their own budget: the bundle must NOT have shrunk { [ "$( bytes_of "$TMP/f_for_detail.out" )" -gt "$ALLOW" ]; } 2>/dev/null \ && ok "#11 A7 no --token-budget: --detail keeps its --pack-budget-bytes budget (unbudgeted bundle unshrunk)" \ @@ -624,7 +657,7 @@ for i in range( 4 ): with open( os.path.join( out, f"mod{i}.cpp" ), "w" ) as fh: fh.write( "\n".join( lines ) ) PYG -a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0 +a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0; a7s_floor=0 # RE-ANCHORED 2026-09-13 (PR #215): the default sweep starts at 760, not 1200. --for's rung zero now triggers on the # EXACT ceiling (verbs_for.h), so a document 1..15% over its budget drops its three explanatory clauses before the # allowance is consulted; on this corpus the late-label band (over_ceiling="1" INSIDE the allowance — the residual @@ -640,7 +673,11 @@ for spec in "default:760:1500:" "detail_graph:2880:3080:--detail=20 --with-graph s_b="$( bytes_of "$A7S/o.xml" )" s_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" s_root="$( grep -aoE '^]*>' "$A7S/o.xml" | head -1 )" - if [ "$s_rc" -ne 0 ] || { [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml"; }; then + # past the allowance: the ladder's last rung says so, or (knob-honesty-068) the floor twin above holds in full + if [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml" \ + && sigs_floor_twin "$A7S/o.xml" "$s_a" >/dev/null; then + a7s_floor=$(( a7s_floor + 1 )) + elif [ "$s_rc" -ne 0 ] || { [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml"; }; then a7s_badn=$(( a7s_badn + 1 )) [ "$a7s_badn" -le 6 ] && a7s_bad="$a7s_bad $s_label@$N=${s_b}/${s_a}B(exit $s_rc)" elif [ "$s_b" -le "$s_a" ] && [ "${s_root#* over_ceiling=\"1\"}" != "$s_root" ]; then @@ -649,13 +686,34 @@ for spec in "default:760:1500:" "detail_graph:2880:3080:--detail=20 --with-graph done done [ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung" \ + && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung, or the disclosed floor twin ($a7s_floor)" \ || no "#11 A7 sweep: $a7s_badn of $a7s_runs budgets deliver past the allowance with no ladder rung fired (first:$a7s_bad) — a byte spliced in after the ladder priced the document" # control: the sweep must cross the band the defect lives in — a root that says over_ceiling="1" while the document # still fits the allowance (est_tokens > N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. [ "$a7s_inside_labelled" -gt 0 ] \ && ok "#11 A7 sweep control: $a7s_inside_labelled budget(s) carry a root over_ceiling=\"1\" INSIDE the allowance — the sweep crosses the late-label band" \ || no "#11 A7 sweep control: no budget carried over_ceiling=\"1\" inside the allowance — the sweep no longer reaches the est_tokens > N band, re-anchor its ranges" +# A7 FLOOR TWIN, EXERCISED EVERY RUN (knob-honesty-068; ruling 2026-10-07). The A7 arm and the sweep accept the twin only as +# an alternative, so a fixture that stopped reaching the floor would leave it untested. On this git-less corpus at +# --token-budget=790 the default bundle's sits at its rank 1..4 floor and cannot pay its next=: the twin must hold. +# Its near miss, 850: the handle is paid (the block sheds its bytes above the floor) — next= rides inside the allowance, and neither +# over_ceiling="1" nor the floor clause appears (a label that rode on every capped answer would pass the first half alone). +( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=790 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? +fl_a="$( awk 'BEGIN{printf "%d", 790*2.36*1.15}' )" +if [ "$fl_rc" -eq 0 ] && fl_why="$( sigs_floor_twin "$A7S/floor.xml" "$fl_a" )"; then + ok "#11 A7 floor twin @790: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" +else + no "#11 A7 floor twin @790 (exit $fl_rc): ${fl_why:-?} — the floor did not ship its next= with a labelled, honestly priced overshoot" +fi +( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=850 --no-cache ) >"$A7S/paid.xml" 2>/dev/null; pd_rc=$? +pd_b="$( bytes_of "$A7S/paid.xml" )"; pd_a="$( awk 'BEGIN{printf "%d", 850*2.36*1.15}' )" +pd_root="$( grep -aoE '^]*>' "$A7S/paid.xml" | head -1 )" +if [ "$pd_rc" -eq 0 ] && [ "$pd_b" -le "$pd_a" ] && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/paid.xml" \ + && [ "${pd_root#* over_ceiling=\"1\"}" = "$pd_root" ] && ! grep -aqF 'r=1..4 floor cannot pay its next=' "$A7S/paid.xml"; then + ok "#11 A7 floor twin near miss @850: the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no floor clause" +else + no "#11 A7 floor twin near miss @850 (exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" +fi # A9/A10 — the header's own spliced attributes are inside the number. IDENTITY, not a band: for a bundle with # no --detail bodies, est_tokens is markup-only, so it must equal round(delivered bytes / 2.50) EXACTLY From e6a0b2e7695c43fefde23c4119555a290963714c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:42:52 -0400 Subject: [PATCH 120/229] resolve: a Go module alias proves its package by import path, nested go.mod included On train 26a falseedgecheck (gomodcomment) went red: quoted/go.mod declares example.com/qm, and quoted/app imports example.com/qm/lib. FE-B proved a Go module-alias member call only by the candidate directory ending the import path (quoted/lib vs .../qm/lib: no), so lib.Mark() was name-only, and a name-only Go member call never reaches a free function -> external=. collectGoModules now records each Go file package import path (the nearest go.mod module path plus the directory below it, FalseEdgeRules::goPackagePath); moduleNames proves a candidate whose package path equals the import, and keeps the directory-tail rule for a tree with no module line above the file (goreplace). Red on d21998fc, green here; base cd87e30e green. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 15 +++++++++++++++ src/receiverevidence.h | 10 +++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/graph.h b/src/graph.h index 15d001be0..9b298d885 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4762,6 +4762,8 @@ struct FalseEdgeRules std::vector goDotImport; // fileId → the file has `import . "…"` std::vector goModules; // every in-tree go.mod's module path std::vector goUnderModule; // fileId → a go.mod sits at or above the file, inside the root + std::vector goPackagePath; // fileId → its package's import path: the nearest go.mod's module + // path + the directory below it ("" when no module line is found) HashMap jsVocabulary; // jsModuleVocabulary, built only when a JS alias exists HashMap rustOutsideUse; // "#name": a Rust `use` of a path outside the crate names it std::vector> functionsByFile; // fileId → its functions/methods sorted by sigStartByte (enclosingFunction) @@ -5209,7 +5211,9 @@ inline std::vector goModuleTreePaths( std::string_view text ) inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) { rules.goUnderModule.assign( ing.files.size(), 0 ); + rules.goPackagePath.assign( ing.files.size(), std::string{} ); HashMap> modOfDir; // disk directory → its go.mod's tree paths (empty: no go.mod) + HashMap modLineOfDir; // disk directory → its go.mod's own `module` path HashMap seenModule; for( const Symbol& s : ing.symbols ) { @@ -5219,6 +5223,7 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) } std::string_view rel = rootRelPath( ing, s.fileId ); std::string disk = diskPath( ing, s.fileId ); + const std::string_view fileDir = includerDir( rel ); // walk up as many directories as the root-relative path has, never above the root while( true ) { @@ -5235,11 +5240,20 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) if( const std::optional text = docparse::detail::readWholeFile( disk + "/go.mod" ) ) { it->second = goModuleTreePaths( *text ); + modLineOfDir.try_emplace( disk, goModulePathOf( *text ) ); } } if( !it->second.empty() ) { rules.goUnderModule[ s.fileId ] = 1; + // FE-B: the package's import path — the module path, then the file's directory below the go.mod's + if( const auto ml = modLineOfDir.find( disk ); ml != modLineOfDir.end() && !ml->second.empty() ) + { + const std::string_view modDir = relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ); + std::string_view below = fileDir.substr( std::min( modDir.size(), fileDir.size() ) ); + below.remove_prefix( below.starts_with( '/' ) ? 1 : 0 ); + rules.goPackagePath[ s.fileId ] = below.empty() ? ml->second : ml->second + "/" + std::string( below ); + } for( const std::string& m : it->second ) { if( seenModule.try_emplace( m, '\0' ).second ) @@ -5825,6 +5839,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { PROFILE_SCOPE_DESCRIBE( "buildGraph/2i: receiver-evidence tables" ); recvEv.localNames = &fieldNarrow.localNameSet; + recvEv.goPackagePath = &falseEdges.goPackagePath; recvEv.build(); } HashMap nameOnlyEdges; // (from<<32|to) of an edge some call bound by NAME ALONE (no receiver evidence) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 5399c4016..0b1c3c319 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -109,6 +109,7 @@ struct ReceiverEvidence mutable rw::SmallVec found; bool active = false; const HashMap* localNames = nullptr; // graph.h FieldNarrowTables::localNameSet + const std::vector* goPackagePath = nullptr; // graph.h FalseEdgeRules::goPackagePath ReceiverEvidence( const IngestResult& i, const HashMap>& up, const HashMap& classes ) : ing( i ), chaUp( up ), classNames( classes ) {} @@ -690,7 +691,14 @@ struct ReceiverEvidence const std::string_view target = rootRelPath( ing, ing.symbols[ c ].fileId ); if( r.lang == Lang::Go ) { - // an import path names a package DIRECTORY; the candidate's directory must end the path, segment-aligned + // an import path names a package: the candidate's own package import path (its go.mod's module path and the + // directory below it — a nested module's `quoted/lib` is `example.com/qm/lib`) proves it exactly + const std::uint32_t cf = ing.symbols[ c ].fileId; + if( goPackagePath != nullptr && cf < goPackagePath->size() && !( *goPackagePath )[ cf ].empty() && source == ( *goPackagePath )[ cf ] ) + { + return true; + } + // else the candidate's directory must end the path, segment-aligned (a replace-only tree, no go.mod above) const std::string_view dir = includerDir( target ); return !dir.empty() && source.size() >= dir.size() && source.ends_with( dir ) && ( source.size() == dir.size() || source[ source.size() - dir.size() - 1 ] == '/' ); From f981b1d01ba54cedec5bc2a47d27cc142aceb66d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 16:52:23 -0400 Subject: [PATCH 121/229] fix(asan-probe): say hang only on a timeout; re-probe when the ASan runtime changes (review) Review of lane asan-toolchain-probe-068 asked for two changes: - The start-up probe now says 'hang' only when the cap fired (CMake: a timeout result; shell: rc 142/124). Any other non-zero exit reports 'exited rc=N instead of 0', and the shell text no longer says 'on this macOS' for a failure that can happen on Linux. - The probe cache key also carries the path and modification time of the ASan runtime library the compiler links, so an in-place Command Line Tools/Xcode update behind the same compiler path re-runs the probe. Co-Authored-By: Claude Sonnet 5.5 --- CMakeLists.txt | 18 ++++++++++++++++-- scripts/asanprobe.sh | 7 +++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index be8c1c439..47d1aa7c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1024,8 +1024,17 @@ if(RIPWIRE_ASAN) if(APPLE AND CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND NOT CMAKE_CROSSCOMPILING AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(RIPWIRE_ASAN_PROBE_TIMEOUT 10 CACHE STRING "seconds the configure-time ASan start-up probe may take (macOS)") + # CMake never re-detects CMAKE_CXX_COMPILER_VERSION (or the host OS version) in an existing build tree, and a Command + # Line Tools/Xcode update replaces the compiler behind the same /usr/bin/c++. So the key also carries the ASan + # runtime the compiler links today (its path and modification time): an in-place toolchain update re-runs the probe. + execute_process(COMMAND ${CMAKE_CXX_COMPILER} ${CMAKE_CXX_COMPILER_ARG1} -print-file-name=libclang_rt.asan_osx_dynamic.dylib + OUTPUT_VARIABLE _ripwire_asan_rt OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + set(_ripwire_asan_rt_stamp "") + if(EXISTS "${_ripwire_asan_rt}") + file(TIMESTAMP "${_ripwire_asan_rt}" _ripwire_asan_rt_stamp "%s" UTC) + endif() set(_ripwire_asan_probe_key - "${CMAKE_CXX_COMPILER}|${CMAKE_CXX_COMPILER_VERSION}|${CMAKE_HOST_SYSTEM_VERSION}|${CMAKE_OSX_SYSROOT}|${CMAKE_EXE_LINKER_FLAGS}") + "${CMAKE_CXX_COMPILER}|${CMAKE_CXX_COMPILER_VERSION}|${CMAKE_HOST_SYSTEM_VERSION}|${CMAKE_OSX_SYSROOT}|${CMAKE_EXE_LINKER_FLAGS}|${_ripwire_asan_rt}@${_ripwire_asan_rt_stamp}") if(NOT "${_ripwire_asan_probe_key}" STREQUAL "${_RIPWIRE_ASAN_PROBE_PASSED}") set(_ripwire_asan_probe_dir "${CMAKE_BINARY_DIR}/CMakeFiles/ripwire_asan_probe") file(MAKE_DIRECTORY "${_ripwire_asan_probe_dir}") @@ -1058,7 +1067,12 @@ if(RIPWIRE_ASAN) TIMEOUT ${RIPWIRE_ASAN_PROBE_TIMEOUT} RESULT_VARIABLE _ripwire_asan_probe_run_rc OUTPUT_QUIET ERROR_QUIET) - if(NOT _ripwire_asan_probe_run_rc EQUAL 0) + if(NOT _ripwire_asan_probe_run_rc EQUAL 0 AND NOT _ripwire_asan_probe_run_rc MATCHES "timeout") + message(FATAL_ERROR "RIPWIRE_ASAN: an empty program built with ${CMAKE_CXX_COMPILER} -fsanitize=address exited " + "(${_ripwire_asan_probe_run_rc}) instead of returning 0: this toolchain's AddressSanitizer runtime fails at " + "start-up. Update the Command Line Tools / Xcode, or configure with Homebrew LLVM (CONTRIBUTING.md, Sanitizer " + "build).") + elseif(NOT _ripwire_asan_probe_run_rc EQUAL 0) message(FATAL_ERROR "RIPWIRE_ASAN: an empty program built with ${CMAKE_CXX_COMPILER} -fsanitize=address did not " "run to completion within ${RIPWIRE_ASAN_PROBE_TIMEOUT} s (${_ripwire_asan_probe_run_rc}). This toolchain's " "AddressSanitizer runtime deadlocks in its own start-up, before main, on this macOS (seen with Apple clang " diff --git a/scripts/asanprobe.sh b/scripts/asanprobe.sh index d71efb042..eb1b8af5a 100644 --- a/scripts/asanprobe.sh +++ b/scripts/asanprobe.sh @@ -60,8 +60,11 @@ ripwire_asan_probe() # $1 = compiler, $2 = writable scratch dir; extra args = return 1 fi ASAN_OPTIONS=detect_leaks=0 gate_bounded "$cap" "$dir/asanprobe" >/dev/null 2>&1; rc=$? - if [ "$rc" -ne 0 ]; then - RIPWIRE_ASAN_PROBE_WHY="an empty -fsanitize=address program built by $cxx did not run to completion within ${cap} s (rc=$rc; the sanitizer runtime hangs before main on this macOS, fixed by a newer Command Line Tools/Xcode or by Homebrew llvm@22)" + if [ "$rc" -eq 142 ] || [ "$rc" -eq 124 ]; then # 128+SIGALRM: gate_bounded's cap fired + RIPWIRE_ASAN_PROBE_WHY="an empty -fsanitize=address program built by $cxx did not run to completion within ${cap} s (rc=$rc: it hung, as the sanitizer runtime of Apple clang 17 / Command Line Tools 26.3 does before main on macOS 26; fixed by a newer Command Line Tools/Xcode or by Homebrew llvm@22)" + return 1 + elif [ "$rc" -ne 0 ]; then + RIPWIRE_ASAN_PROBE_WHY="an empty -fsanitize=address program built by $cxx exited rc=$rc instead of 0 (the sanitizer runtime failed at start-up on this host)" return 1 fi return 0 From 868f99eaae64f5f9b8ee9e74f9d9c0870523cbee Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 17:38:55 -0400 Subject: [PATCH 122/229] route: skip / harness events, default arm to auto, report NO CONTROL ARM and readout date (#381) Prompt hooks and the --help-task classifier treat a leading ) as a harness event: skip-system, no classification. Unconfigured arm resolves to auto in the Claude router, tool-call router and meter. routing_ab_report.py prints per-arm counts, NO CONTROL ARM, and the registered readout date. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 13 +++++ INSTALL.md | 6 ++ bench/routing_ab_report.py | 91 ++++++++++++++++++++++++++++++- docs/EVALS.md | 22 ++++++++ hooks/ripwire-claude-route.sh | 65 +++++++++++++++------- hooks/ripwire-claude-toolroute.sh | 13 ++++- hooks/ripwire-codex-route.sh | 53 ++++++++++++------ hooks/ripwire-nudge.sh | 23 ++++---- src/taskroute.h | 57 +++++++++++++------ test/codexpromptroutecheck.sh | 31 +++++++++++ test/hookcheck.sh | 29 ++++++++-- test/routehookcheck.sh | 86 +++++++++++++++++++++++++++++ test/routingreportcheck.sh | 51 +++++++++++++++++ test/taskroutecheck.sh | 19 +++++++ test/toolcallroutecheck.sh | 44 ++++++++++++++- 15 files changed, 527 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 833ea7c58..369f4f940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,19 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — router and meter default to a random control arm; channel events and agent hand-backs are not prompts + +With no `~/.ripwire/meter.conf` and no `RIPWIRE_METER_ARM`, the Claude Code prompt router, the tool-call router and +the meter used to put every session on `treatment`, so the registered treatment-versus-control comparison had no control +group on a default install (issue #381). The unconfigured default is now `arm=auto`, the stable per-session split +the meter already had; `arm=treatment` and `arm=control` are still honoured. The Claude Code and Codex prompt-router hooks +and the `--help-task` classifier now treat a prompt that starts with ``, as a harness event, like `` and +``: no classification, a `skip-system` log row. ``, `` and a prompt that merely +mentions a wrapper mid-sentence still route. `bench/routing_ab_report.py` prints per-arm counts, says `NO CONTROL ARM` +when an arm has no prompts, and prints the registration's readout date, flagged once it has passed. Gates: +`routehookcheck`, `codexpromptroutecheck`, `taskroutecheck`, `toolcallroutecheck`, `hookcheck`, `routingreportcheck`. + ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/INSTALL.md b/INSTALL.md index 2f1d7a2a7..a45fe900b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -185,6 +185,12 @@ rm -rf "$h/substitution.jsonl" "$h/routing.jsonl" "$h/routing-pending" "$h/meter rmdir "$h" 2>/dev/null || true ``` +`meter.conf` is optional and the installer never writes it. With no `meter.conf` (and no `RIPWIRE_METER_ARM` +in the environment) the hooks assign each session to the `treatment` or `control` arm of the router and +meter A/B at random, from a hash of the session id, so the A/B always has a control group. Control sessions +get the same logging and no injected advice. To force every session onto one side, write `arm=treatment` or +`arm=control` into `$h/meter.conf` (or set `RIPWIRE_METER_ARM`); `arm=auto` spells out the default. + **4. The MCP server** (only if you added it). - Claude Code: `claude mcp remove ripwire`. diff --git a/bench/routing_ab_report.py b/bench/routing_ab_report.py index 92082cbc1..a22259f75 100755 --- a/bench/routing_ab_report.py +++ b/bench/routing_ab_report.py @@ -36,8 +36,9 @@ # and NO VERDICT below 40 recommended prompts per arm — that floor is enforced unconditionally below; # there is no flag that overrides it, and none should be added. # -# WHAT THIS NEVER OPENS. Exactly the two paths named by --routing and --meter — nothing else on disk, -# ever. routing.jsonl carries no prompt text by construction (a cksum and a byte length only), and this +# WHAT THIS NEVER OPENS. Exactly the two paths named by --routing and --meter, plus the one registration +# file docs/EVALS.md (--evals) read for the registered readout date and nothing else — no other path on +# disk, ever. routing.jsonl carries no prompt text by construction (a cksum and a byte length only), and this # script never reads a field that could hold any (`detail`, transcripts, repo content); every number # printed below is a count, a rate, or a file path the caller supplied. # @@ -65,7 +66,9 @@ # Exit 0 on a normal readout OR a refusal (refusing below the floor is a correct answer, not a # failure); non-zero only when an input file's content cannot be read as this instrument's data at all. import argparse +import datetime import os +import re import subprocess import sys import json @@ -76,6 +79,63 @@ AGENT = "claude" # routing.jsonl is shared with the Codex router; see header +def default_evals(): + """docs/EVALS.md next to this script (bench/ and docs/ are siblings in the checkout).""" + return os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "docs", "EVALS.md") + + +# The prompt-router registration (docs/EVALS.md, "Claude Code prompt router — PRE-REGISTERED 2026-09-02") +# carries its readout date as bold lines `**Readout date:** YYYY-MM-DD` and, once, `**Readout date, extended +# once:** YYYY-MM-DD`. Both are read from that section only. +REGISTRATION_HEADING = "### Claude Code prompt router — PRE-REGISTERED" +READOUT_RE = re.compile(r"^\*\*Readout date:\*\*\s*(\d{4}-\d{2}-\d{2})", re.M) +READOUT_EXT_RE = re.compile(r"^\*\*Readout date, extended once:\*\*\s*(\d{4}-\d{2}-\d{2})", re.M) + + +def read_readout_dates(path): + """(readout, extended, problem). Dates are `datetime.date` or None; `problem` is a one-line reason + when the readout date could not be read (file missing, section missing, no date line) so the report + says "unknown" and why instead of staying silent.""" + try: + with open(path, "r", encoding="utf-8", errors="replace") as fh: + text = fh.read() + except OSError: + return None, None, "registration file not readable: %s" % os.path.normpath(path) + start = text.find(REGISTRATION_HEADING) + if start < 0: + return None, None, "no prompt-router registration section in %s" % os.path.normpath(path) + nxt = text.find("\n### ", start + 1) + section = text[start:nxt if nxt > 0 else len(text)] + m, e = READOUT_RE.search(section), READOUT_EXT_RE.search(section) + try: + readout = datetime.date.fromisoformat(m.group(1)) if m else None + extended = datetime.date.fromisoformat(e.group(1)) if e else None + except ValueError: + return None, None, "readout date in the registration is not a valid calendar date" + if readout is None: + return None, None, "the registration section carries no `**Readout date:**` line" + return readout, extended, None + + +def readout_lines(readout, extended, problem, today): + """The report's readout-date lines: the registered date, how far it is from `today`, and a loud flag + once it has passed (a passed date is exactly when an unread, underpowered window needs a decision).""" + if problem: + return ["readout date: UNKNOWN -- %s" % problem] + lines = [] + for label, d in (("prompt-router registered readout date", readout), ("prompt-router extended readout date (one-time)", extended)): + if d is None: + continue + delta = (today - d).days + if delta > 0: + lines.append("%s: %s -- PASSED %d day(s) ago (as of %s)" % (label, d.isoformat(), delta, today.isoformat())) + elif delta == 0: + lines.append("%s: %s -- TODAY" % (label, d.isoformat())) + else: + lines.append("%s: %s -- in %d day(s) (as of %s)" % (label, d.isoformat(), -delta, today.isoformat())) + return lines + + def default_home(): return os.environ.get("RIPWIRE_HOME", os.path.join(os.path.expanduser("~"), ".ripwire")) @@ -262,6 +322,17 @@ def report_one_router(router, routing_rows, meter_rows): print("%-10s %8d %12d %10d %8s %9d" % (arm, st["prompts"], st["recommended"], st["adopted"], rate_s, st["sessions"])) + # Per-arm counts on one line, and a LOUD flag for an empty arm: a window with no control prompts + # (the shipped default put every session on treatment until issue #381) cannot yield the registered + # treatment-minus-control difference no matter how long it runs. + print("arm counts: treatment=%d control=%d prompts" + % (arm_stats["treatment"]["prompts"], arm_stats["control"]["prompts"])) + for arm, other in (("control", "treatment"), ("treatment", "control")): + if arm_stats[arm]["prompts"] == 0: + print("!!! NO %s ARM -- %s has 0 prompts in this window (the other arm has %d); " + "the registered treatment-minus-control difference CANNOT be computed from it !!!" + % (arm.upper(), arm, arm_stats[other]["prompts"])) + covered, total = join_coverage(prompts, meter_rows) if total: print("join coverage: %d/%d routing row(s) (%.1f%%) have a session with >=1 meter row" @@ -294,6 +365,12 @@ def main(): help="path to substitution.jsonl (default: $RIPWIRE_HOME or ~/.ripwire, substitution.jsonl)") ap.add_argument("--since", default=None, help="only rows with at >= this ISO8601 timestamp") ap.add_argument("--until", default=None, help="only rows with at < this ISO8601 timestamp") + ap.add_argument("--evals", default=default_evals(), + help="path to docs/EVALS.md, read only for the prompt-router registration's readout " + "date (default: the docs/EVALS.md next to this script)") + ap.add_argument("--today", default=None, + help="YYYY-MM-DD to compare the readout date against (default: the current UTC date; " + "a fixed value makes the report reproducible)") ap.add_argument("--router", default=None, help="report only this router (e.g. prompt, toolcall) instead of every router " "present in the log") @@ -311,6 +388,12 @@ def main(): % args.meter, file=sys.stderr) return 1 + try: + today = datetime.date.fromisoformat(args.today) if args.today else datetime.datetime.now(datetime.timezone.utc).date() + except ValueError: + print("routing_ab_report: --today must be YYYY-MM-DD, got %r" % args.today, file=sys.stderr) + return 2 + routing_rows = filter_window(routing_rows, args.since, args.until) print("routing_ab_report -- routing=%s meter=%s" % (args.routing, args.meter)) @@ -319,6 +402,10 @@ def main(): len(meter_rows), meter_bad, "found" if meter_existed else "not found")) if args.since or args.until: print("window: [%s, %s)" % (args.since or "-inf", args.until or "+inf")) + # The registered readout date belongs to the PROMPT router's registration; the toolcall router has + # its own registration and is not dated here. + for line in readout_lines(*read_readout_dates(args.evals), today): + print(line) if args.router: routers = [args.router] diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b910a35c..54fd9de3d 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -1157,6 +1157,13 @@ measures in the wild; a coverage collapse invalidates the readout the same way a drift does. (c) Route accuracy in the field is **not** measurable from this log by construction — the rows are hash-only — and no number resembling it will be reported from it. +**Readout date:** 2026-09-30 +**Readout date, extended once:** 2026-10-14 + +The two dates above are the registration's "four weeks (or six, under the extension)" counted from the +registration date, 2026-09-02, the only date this section carries. They are written out so that +`bench/routing_ab_report.py` can print them and flag a passed one; they do not change the rule. + **Decision rule.** One readout, at four weeks (or six, under the extension). KEEP → the router stays as shipped. REWORD → one revision of the injected framing text, then a fresh registration with a new band; the mechanism is not re-litigated. **REMOVE → the hook is unregistered from the installer and deleted**, @@ -1193,6 +1200,21 @@ population is biased toward the intents it already covers. **The readout is a LATER session.** This lane ships the instrument and the band. Nothing in this registration is a result. +**Amendment, 2026-10-07 (issue #381) — the shipped default previously gave no control arm.** The control arm +above is real only for a session that resolves to `control`. The hooks resolved an unconfigured arm (no +`RIPWIRE_METER_ARM`, no `arm=` line in `~/.ripwire/meter.conf`) to `treatment`, and nothing in the installer +wrote `arm=auto`, so a machine that never created `meter.conf` logged only treatment sessions: one such +machine reported 18968 treatment prompts and 0 control prompts over four weeks, and the treatment-minus-control +difference registered above could not be computed from them. The default is now `auto` (the stable +session-hash split) in `hooks/ripwire-claude-route.sh`, `hooks/ripwire-claude-toolroute.sh` and +`hooks/ripwire-nudge.sh`; an explicit `arm=treatment` or `arm=control` is still honoured. Rows written before +the change by an unconfigured hook carry `arm="treatment"` without a control population to compare against; a +window that spans the change should be split at it, as this section already requires for any change to the +mechanism. The same issue taught the prompt hooks to skip MCP channel events (``) and sub-agent +hand-backs (``) like `` and ``: they are harness events, +not prompts. `bench/routing_ab_report.py` now prints per-arm counts, says `NO CONTROL ARM` when an arm has no +prompts, and prints the readout dates above with a flag once one has passed. + **The readout instrument.** `python3 bench/routing_ab_report.py [--routing PATH] [--meter PATH] [--since AT] [--until AT]` (defaults: `$RIPWIRE_HOME/routing.jsonl` and `$RIPWIRE_HOME/substitution.jsonl`) computes exactly the metric above: per arm, recommended-prompt diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index cc315c8a1..beb58ca6a 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -18,7 +18,7 @@ # The round cannot separate the three, and the registration says so. # # THE ARM IS THE METER'S ARM, NOT A SECOND COIN FLIP. It resolves `arm` exactly as meter_init() in -# hooks/ripwire-nudge.sh does — env over `meter.conf` over the `treatment` default, with the literal +# hooks/ripwire-nudge.sh does — env over `meter.conf` over the `auto` default (issue #381; it was `treatment`), with the literal # `auto` selecting the same stable session-id hash — so a session lands on the SAME side in both # instruments and the two logs join on session_hash. A control session runs the classifier, writes the # identical row and the identical pending file, and injects NOTHING; its adoption-within-two number is @@ -83,7 +83,10 @@ resolve_arm() case "$_ra_k" in arm) _ra_conf="$_ra_v" ;; esac done < "$_ra_home/meter.conf" fi - case "${RIPWIRE_METER_ARM:-$_ra_conf}" in + # UNSET (no env, no `arm=` in meter.conf) is `auto`, not `treatment` (issue #381): the shipped default + # used to put every session on treatment, so the pre-registered treatment-minus-control difference could + # never be computed. An explicit `treatment` stays the opt-out; any OTHER value still reads as treatment. + case "${RIPWIRE_METER_ARM:-${_ra_conf:-auto}}" in control) route_arm="control" ;; auto) _ra_h="$( printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1 )" @@ -369,9 +372,11 @@ case "$promptBytes" in ''|*[!0-9]*) exit 0;; esac [ "$promptBytes" -le 8192 ] || exit 0 # HARNESS/SYSTEM EVENT GUARD, checked before the classifier is ever invoked. Claude Code delivers a -# background-task completion (`…`) and an injected reminder -# (`…`) through this SAME UserPromptSubmit channel — they are not user -# input, but `--help-task` has no concept of "this names no task" and answers the report prose anyway +# background-task completion (`…`), an injected reminder +# (`…`), an MCP channel event (`…`) and a +# sub-agent hand-back (`…`) through this SAME UserPromptSubmit +# channel (the last two: issue #381 — channel events alone were about half of one machine's injected +# recommendations) — they are not user input, but `--help-task` has no concept of "this names no task" and answers the report prose anyway # (docs/EVALS.md, the routing-noise round: a background-task summary quoting words like "Summary:" and # "Fix," minted `--connect='Split,A,Report'` out of text that never named a task). Narrow and POSITIONAL # on purpose: only the prompt's own leading bytes, after whitespace, are tested, so a genuine user prompt @@ -388,24 +393,42 @@ case "$promptBytes" in ''|*[!0-9]*) exit 0;; esac # existing session's routing-pending file, none of which a harness/system event should ever cause. This # bash parameter-expansion form strips the FULL leading run of [:space:] bytes (space/tab/newline/CR) from # the whole string in one pass, not line by line. +# ---- BEGIN MIRRORED BLOCK rw_is_harness_event (issue #381) ------------------------------------------------ +# KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh and hooks/ripwire-codex-route.sh; test/routehookcheck.sh +# extracts both copies and diffs them (the rw_is_ripwire_call pattern). The list of harness-event wrappers is +# data in ONE place per file: the C++ classifier keeps the same list in src/taskroute.h (kHarnessEventTags). +# rw_is_harness_event REST — REST is the prompt with its leading whitespace already stripped. True when it +# begins with a wrapper Claude Code uses to deliver something that is not user input: a background-task +# completion or an injected reminder (matched with their closing `>`), or an MCP channel event / sub-agent +# hand-back (``). ``, ``, `` and a prompt that merely +# MENTIONS a wrapper mid-sentence are not matches. +rw_is_harness_event() +{ + case "$1" in + ''*|''*) return 0 ;; + ''*|''*) return 0 ;; + esac + return 1 +} +# ---- END MIRRORED BLOCK rw_is_harness_event --------------------------------------------------------------- lead="${prompt%%[![:space:]]*}" rest="${prompt#"$lead"}" -case "$rest" in - ''*|''*) - if meter_home; then - promptHash="$( hash_text "$prompt" )" - [ -n "$session" ] || session="prompt:$promptHash" - sessionHash="$( hash_text "$session" )" - now="$( date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true )" - jq -cn --arg at "$now" --arg hash "$promptHash" --arg sessionHash "$sessionHash" \ - --argjson bytes "$promptBytes" \ - '{v:2,at:$at,agent:"claude",router:"prompt",event:"UserPromptSubmit",status:"skip-system", - intent:"",recommended:"",arm:"",session_hash:$sessionHash,prompt_hash:$hash, - prompt_bytes:$bytes}' >>"$routingLog" 2>/dev/null || true - fi - exit 0 - ;; -esac +if rw_is_harness_event "$rest" +then + if meter_home; then + promptHash="$( hash_text "$prompt" )" + [ -n "$session" ] || session="prompt:$promptHash" + sessionHash="$( hash_text "$session" )" + now="$( date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true )" + jq -cn --arg at "$now" --arg hash "$promptHash" --arg sessionHash "$sessionHash" \ + --argjson bytes "$promptBytes" \ + '{v:2,at:$at,agent:"claude",router:"prompt",event:"UserPromptSubmit",status:"skip-system", + intent:"",recommended:"",arm:"",session_hash:$sessionHash,prompt_hash:$hash, + prompt_bytes:$bytes}' >>"$routingLog" 2>/dev/null || true + fi + exit 0 +fi route="$( ripwire "$cwd" --help-task="$prompt" 2>/dev/null )" || exit 0 # The root's ATTRIBUTE, not a byte prefix: since the compact legend became the CLI default (L1) the root reads diff --git a/hooks/ripwire-claude-toolroute.sh b/hooks/ripwire-claude-toolroute.sh index 09221176c..a549eba08 100755 --- a/hooks/ripwire-claude-toolroute.sh +++ b/hooks/ripwire-claude-toolroute.sh @@ -18,7 +18,7 @@ # # THE ARM IS THE METER'S ARM, NOT A SECOND COIN FLIP. `resolve_arm` below reimplements the exact rule # meter_init() (hooks/ripwire-nudge.sh) and hooks/ripwire-claude-route.sh's own `resolve_arm` apply — -# env over `meter.conf` over the `treatment` default, `auto` selecting the same stable session-id hash +# env over `meter.conf` over the `auto` default (issue #381; it was `treatment`), `auto` selecting the same stable session-id hash # — so a session lands on the same side across all three instruments. A control session runs the same # classification and writes the identical row; it injects nothing. Reimplemented rather than sourced: # sourcing either sibling script would run its own top-level PreToolUse/UserPromptSubmit body. @@ -129,7 +129,11 @@ esac # build/gate invocation) never reaches here at all for Bash: it is not a recursive-grep shape, so # the classification below never sets `shape`, and the fast "no shape, no row" exit further down # handles it identically to `cat`/`sed`. This block only needs to catch the marker literally -# appearing inside one of the fields we DO act on. ---- +# appearing inside one of the fields we DO act on. +# DELIBERATELY NOT `` here would suppress an honest grep for RSS/XML +# text (`grep -rn '' feeds/`). The wrappers only matter as the leading bytes of a prompt. ---- case "$command$pattern$gpath$file_path" in *'[SYSTEM NOTIFICATION'*|*''*) notif=1 ;; *) notif=0 ;; @@ -155,7 +159,10 @@ resolve_arm() case "$_ra_k" in arm) _ra_conf="$_ra_v" ;; esac done < "$_ra_home/meter.conf" fi - case "${RIPWIRE_METER_ARM:-$_ra_conf}" in + # UNSET (no env, no `arm=` in meter.conf) is `auto`, not `treatment` (issue #381): the shipped default + # used to put every session on treatment, so the pre-registered treatment-minus-control difference could + # never be computed. An explicit `treatment` stays the opt-out; any OTHER value still reads as treatment. + case "${RIPWIRE_METER_ARM:-${_ra_conf:-auto}}" in control) route_arm="control" ;; auto) _ra_h="$( printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1 )" diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 89ca3ffb4..bad6adb7f 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -285,8 +285,9 @@ case "$promptBytes" in ''|*[!0-9]*) exit 0;; esac [ "$promptBytes" -le 8192 ] || exit 0 # HARNESS/SYSTEM EVENT GUARD, checked before the classifier is ever invoked. A background-task completion -# (`…`) or an injected reminder (`…`) -# can arrive on this same channel, not from the user, and `--help-task` has no concept of "this names no +# (`…`), an injected reminder (`…`), +# an MCP channel event (``) or a sub-agent hand-back (``) (issue #381) can +# arrive on this same channel, not from the user, and `--help-task` has no concept of "this names no # task" — it answers the report prose anyway (docs/EVALS.md, the routing-noise round). Narrow and # POSITIONAL on purpose: only the prompt's own leading bytes, after whitespace, are tested, so a genuine # prompt that merely mentions one of these markers mid-sentence is untouched. The classifier is never @@ -296,23 +297,41 @@ case "$promptBytes" in ''|*[!0-9]*) exit 0;; esac # blank line before the marker kept that leading newline in `rest` and missed the case match below, # falling through to a real classifier call and status="abstain" logging instead of "skip-system". This # form strips the full leading run of [:space:] bytes from the whole string in one pass. +# ---- BEGIN MIRRORED BLOCK rw_is_harness_event (issue #381) ------------------------------------------------ +# KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh and hooks/ripwire-codex-route.sh; test/routehookcheck.sh +# extracts both copies and diffs them (the rw_is_ripwire_call pattern). The list of harness-event wrappers is +# data in ONE place per file: the C++ classifier keeps the same list in src/taskroute.h (kHarnessEventTags). +# rw_is_harness_event REST — REST is the prompt with its leading whitespace already stripped. True when it +# begins with a wrapper Claude Code uses to deliver something that is not user input: a background-task +# completion or an injected reminder (matched with their closing `>`), or an MCP channel event / sub-agent +# hand-back (``). ``, ``, `` and a prompt that merely +# MENTIONS a wrapper mid-sentence are not matches. +rw_is_harness_event() +{ + case "$1" in + ''*|''*) return 0 ;; + ''*|''*) return 0 ;; + esac + return 1 +} +# ---- END MIRRORED BLOCK rw_is_harness_event --------------------------------------------------------------- lead="${prompt%%[![:space:]]*}" rest="${prompt#"$lead"}" -case "$rest" in - ''*|''*) - if meter_home; then - promptHash="$( hash_text "$prompt" )" - [ -n "$session" ] || session="prompt:$promptHash" - sessionHash="$( hash_text "$session" )" - now="$( date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true )" - jq -cn --arg at "$now" --arg hash "$promptHash" --arg sessionHash "$sessionHash" \ - --argjson bytes "$promptBytes" \ - '{v:2,at:$at,event:"UserPromptSubmit",status:"skip-system",intent:"",recommended:"", - session_hash:$sessionHash,prompt_hash:$hash,prompt_bytes:$bytes}' >>"$routingLog" 2>/dev/null || true - fi - exit 0 - ;; -esac +if rw_is_harness_event "$rest" +then + if meter_home; then + promptHash="$( hash_text "$prompt" )" + [ -n "$session" ] || session="prompt:$promptHash" + sessionHash="$( hash_text "$session" )" + now="$( date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true )" + jq -cn --arg at "$now" --arg hash "$promptHash" --arg sessionHash "$sessionHash" \ + --argjson bytes "$promptBytes" \ + '{v:2,at:$at,event:"UserPromptSubmit",status:"skip-system",intent:"",recommended:"", + session_hash:$sessionHash,prompt_hash:$hash,prompt_bytes:$bytes}' >>"$routingLog" 2>/dev/null || true + fi + exit 0 +fi route="$( ripwire "$cwd" --help-task="$prompt" 2>/dev/null )" || exit 0 # The root's ATTRIBUTE, not a byte prefix: since the compact legend became the CLI default (L1) the root reads diff --git a/hooks/ripwire-nudge.sh b/hooks/ripwire-nudge.sh index 975a390fe..d446760d0 100755 --- a/hooks/ripwire-nudge.sh +++ b/hooks/ripwire-nudge.sh @@ -94,9 +94,12 @@ set -u # with no marker file and no shared state to go stale, including across the internal SessionStart # resets a long session can produce (see the post_nudge/post_sweep reset behavior above). It is reached # only when the arm config names the new literal `auto` (RIPWIRE_METER_ARM=auto, or `arm=auto` in -# meter.conf) — UNSET still means treatment, exactly as before this fix, so a machine nobody has -# touched keeps behaving exactly as it always has; going live with a real A/B population is a -# deployment-time config write (`arm=auto`), not a change to what the script defaults to. +# meter.conf). THAT WAS THE DEFAULT-OFF DESIGN, AND IT IS SUPERSEDED (issue #381, 2026-10-07): UNSET now +# means `auto`. Field data from one machine that never wrote meter.conf showed the prompt router logging +# 18968 treatment prompts and 0 control, so the registered treatment-minus-control difference could not be +# computed. An explicit `arm=treatment` / RIPWIRE_METER_ARM=treatment is the opt-out, and any other value +# still reads as treatment. The same default applies in hooks/ripwire-claude-route.sh and +# hooks/ripwire-claude-toolroute.sh (their resolve_arm), so the three instruments still agree per session. # # THIS FIX DOES NOT CHANGE TREATMENT SEMANTICS. A session that lands on the treatment arm behaves # bit-for-bit as before: same nudges, same dedup, same sweep. What changes is only that a control arm @@ -435,14 +438,12 @@ meter_init() # # The literal `control`/`treatment` still force an arm (an operator override, or a test harness # pinning one side) — that contract, and its "any unrecognized value reads as treatment" safety - # net, is UNCHANGED, on purpose: this file's own default stays `treatment` so a machine that never - # touches meter.conf keeps behaving exactly as it always has. What is NEW is a third literal value, - # `auto`, which activates the session-id hash split in `meter_auto_arm` — this is the fix's actual - # deliverable, but it is opt-in at the CONFIG layer (deployment writes `arm=auto` to - # ~/.ripwire/meter.conf) rather than a change to what an unconfigured hook does. That split keeps - # "does this commit change treatment semantics" answerable with "no" — nothing about this hook's - # behavior moves unless something now names `auto` where nothing was named before. - case "${RIPWIRE_METER_ARM:-$_conf_arm}" in + # net, is UNCHANGED, on purpose. The third literal value, `auto`, activates the session-id hash + # split in `meter_auto_arm`. Until issue #381 it was opt-in at the CONFIG layer and an unconfigured + # hook resolved `treatment`; an unconfigured hook (no env, no `arm=` line) now resolves `auto`, so + # a machine nobody has touched gets a real control arm. The opt-out is an explicit `arm=treatment`. + # UNSET (no env, no `arm=` in meter.conf) is `auto` as of issue #381 — see the note above. + case "${RIPWIRE_METER_ARM:-${_conf_arm:-auto}}" in control) meter_arm="control" ;; auto) meter_arm="$( meter_auto_arm "$session" )" ;; *) meter_arm="treatment" ;; diff --git a/src/taskroute.h b/src/taskroute.h index 9e7110cb4..7fe46270f 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -5,6 +5,7 @@ // weights at runtime. Integer scores and stable tie-breaking keep the result byte-deterministic. #include +#include #include #include #include @@ -143,21 +144,39 @@ inline int wordScore( std::string_view lower, std::initializer_list…` completions and `…` -// blocks this way)? Both the Claude Code and Codex UserPromptSubmit router hooks (under hooks/, filenames -// ending "-route.sh") skip calling this classifier at all on such input — a bash-level guard mirroring the -// PreToolUse tool-call router's own pre-classification notification check (hooks/, filename ending -// "-toolroute.sh") — but --help-task is also callable directly: by a test, by an MCP client, by a future -// integration that does not go through either hook. So the same discipline lives here too, rather than -// only at the shell layer. A background-task report is mostly prose quoting machine output (a sanitizer -// excerpt, a diff, an XML `` block): before this guard existed, that prose could mint a -// `--from-trace=-`/`--grep=`/`--connect=` recommendation out of text that named no task at all (the -// routing-noise round, docs/EVALS.md). Narrow and literal on purpose — this is a shape check on the -// harness's own wake-up markers, not a guess at what a user prompt looks like: only the FIRST non- -// whitespace bytes are tested, so a user prompt that happens to mention `` in the -// middle of a sentence ("what does mean in the hook?") is unaffected. +// Is `task` a harness/system event rather than something a user typed — a background-task wake-up, an +// injected reminder, an MCP channel event or a sub-agent hand-back, delivered to UserPromptSubmit through the +// same channel as a real prompt (Claude Code routes `…` completions, +// `…` blocks, `…` events and +// `…` hand-backs this way; the last two are issue #381)? Both the +// Claude Code and Codex UserPromptSubmit router hooks (under hooks/, filenames ending "-route.sh") skip +// calling this classifier at all on such input — a bash-level guard (rw_is_harness_event, a mirrored block +// that lists the same four wrappers) mirroring the PreToolUse tool-call router's own pre-classification +// notification check (hooks/, filename ending "-toolroute.sh", which deliberately does NOT carry the two +// newer wrappers: a tool input is the agent's own action, and the guard there is a substring match) — but +// --help-task is also callable directly: by a test, by an MCP client, by a future integration that does not +// go through either hook. So the same discipline lives here too, rather than only at the shell layer. A +// background-task report is mostly prose quoting machine output (a sanitizer excerpt, a diff, an XML +// `` block): before this guard existed, that prose could mint a `--from-trace=-`/`--grep=`/ +// `--connect=` recommendation out of text that named no task at all (the routing-noise round, +// docs/EVALS.md). Narrow and literal on purpose — this is a shape check on the harness's own wrappers, not +// a guess at what a user prompt looks like: only the FIRST non-whitespace bytes are tested, so a user prompt +// that happens to mention `` in the middle of a sentence ("what does +// mean in the hook?") is unaffected, and ``, `` and +// `` are not the wrappers (the attribute-carrying tags must be followed by a space or `>`). +struct HarnessEventTag +{ + std::string_view tag; // the bytes the prompt must start with + bool whole; // true: `tag` already ends in `>` and matches as is; false: the tag may carry + // attributes, so it must be followed by a space or `>` +}; +inline constexpr std::array kHarnessEventTags{ { + { "", true }, + { "", true }, + { "" ) || rest.starts_with( "" ); + return std::any_of( kHarnessEventTags.begin(), kHarnessEventTags.end(), [&rest]( const HarnessEventTag &t ) { + if( !rest.starts_with( t.tag ) ) + { + return false; + } + return t.whole || ( rest.size() > t.tag.size() && ( rest[t.tag.size()] == ' ' || rest[t.tag.size()] == '>' ) ); + } ); } inline bool looksLikeTrace( std::string_view lower ) noexcept diff --git a/test/codexpromptroutecheck.sh b/test/codexpromptroutecheck.sh index d2b8f097c..431228774 100755 --- a/test/codexpromptroutecheck.sh +++ b/test/codexpromptroutecheck.sh @@ -134,6 +134,37 @@ jq -e -s 'any(.[]; .event == "RouteObservation" and .outcome == "adopted" and .p && ok "adopted rows record the recommended verb as observed, not a leading modifier flag" \ || no "adopted row recorded a modifier flag as the observed verb" +# MCP channel events (``) and sub-agent hand-backs (``) reach +# UserPromptSubmit like a prompt but are harness events (issue #381): no classifier call, no context, a +# status=skip-system row. The stub ripwire above recommends for any text without "abstain", so a routed +# prompt is visible as output. Own RIPWIRE_HOME so the counted log above is undisturbed. Near-misses +# (``, ``, ``) and a mid-sentence mention still route. +CH="$TMP/chan"; mkdir -p "$CH" +chan_case() # chan_case LABEL SESSION PROMPT WANT(skip|route) +{ + _cj="$( jq -cn --arg p "$3" --arg c "$TMP/repo" --arg s "$2" '{prompt:$p,cwd:$c,session_id:$s}' )" + _co="$( printf '%s\n' "$_cj" | PATH="$TMP/bin:$PATH" RIPWIRE_HOME="$CH" "$HOOK" )" + _cst="$( jq -rs --arg sh "$( printf '%s' "$2" | cksum | cut -d' ' -f1 )" '[.[] | select(.session_hash == $sh)] | last | .status // "none"' "$CH/routing.jsonl" 2>/dev/null )" + if [ "$4" = skip ]; then + [ -z "$_co" ] && [ "$_cst" = "skip-system" ] && ok "$1: skipped (status=skip-system, nothing injected)" \ + || no "$1: expected skip, got status=[$_cst] out=[${_co:+set}]" + else + [ -n "$_co" ] && [ "$_cst" = "recommend" ] && ok "$1: still routes" \ + || no "$1: expected a route, got status=[$_cst] out=[${_co:+set}]" + fi +} +CHT='understand parseArgs without broad reads' +chan_case "channel event" cx1 "$CHT" skip +chan_case "channel event, leading blank line" cx2 "$( printf ' \n\t%s' "$CHT" )" skip +chan_case "bare tag" cx3 "$CHT" skip +chan_case "agent-message hand-back" cx4 "$CHT" skip +chan_case "agent-message, leading blank line" cx5 "$( printf '\n\n %s' "$CHT" )" skip +chan_case "near-miss " cx6 " $CHT" route +chan_case "near-miss " cx7 " $CHT" route +chan_case "near-miss " cx8 " $CHT" route +chan_case "mid-sentence mention" cx9 "what does mean? $CHT" route +chan_case "mid-sentence mention" cx10 "what does mean? $CHT" route + OFF="$( printf '%s\n' "{\"prompt\":\"$PROMPT\",\"cwd\":\"$TMP/repo\",\"session_id\":\"route-off\"}" | \ PATH="$TMP/bin:$PATH" RIPWIRE_HOME="$TMP/off" RIPWIRE_ROUTE_METER=0 "$HOOK" )" [ -n "$OFF" ] && [ ! -e "$TMP/off" ] \ diff --git a/test/hookcheck.sh b/test/hookcheck.sh index 4338b7ee1..4a9f08c4d 100755 --- a/test/hookcheck.sh +++ b/test/hookcheck.sh @@ -623,7 +623,7 @@ LASTROW="$( meterrows "$DEFAULT_LOG" )" # ── M19-M20: the arm is on every row, and the PreToolUse path is now ARM-INDEPENDENT ─────────────── # §RETIRED (2026-09-02): with both tiers silent in both arms, the PreToolUse path can no longer differ # by arm — so these arms assert exactly that, plus the two things that did NOT change: the arm is still -# resolved, still recorded on the row, and still defaults to treatment. The one behaviour the arm does +# resolved, still recorded on the row, and now defaults to the auto split (issue #381). The one behaviour the arm does # separate is the SessionStart primer, gated by A3/A4. # OR-chain pattern (P4.2, 2026-08-29): a literal `grep -rn needle .` would demote category to "" before # the arm is consulted, and these arms would pass for the wrong reason (nudge=none). @@ -635,10 +635,31 @@ OUTM19="$( run_meter "$L19" "$( bashjson meter19 "grep -rn 'needle|other' ." )" || no "M19 meter: arm=[$( meterrowget "$L19" 1 arm )] nudged=[$( meterrowget "$L19" 1 nudged )] nudge=[$( meterrowget "$L19" 1 nudge )] out=[$OUTM19]" TM20="$TMP/tm20"; mkdir -p "$TM20"; L20="$TMP/m20.jsonl" OUTM20="$( run_meter "$L20" "$( bashjson meter20 "grep -rn 'needle|other' ." )" "$TM20" )" -[ "$( meterrowget "$L20" 1 arm )" = "treatment" ] && [ -z "$OUTM20" ] \ +# Issue #381: with no env override and no meter.conf the arm is now the `auto` split (it used to be +# `treatment` for every session, so no control arm could ever exist). Expected side computed here the way +# the meter hashes a session id. +M20H="$( printf '%s' meter20 | cksum | cut -d' ' -f1 )" +if [ "$(( M20H % 100 ))" -lt 50 ]; then M20WANT=control; else M20WANT=treatment; fi +[ "$( meterrowget "$L20" 1 arm )" = "$M20WANT" ] && [ -z "$OUTM20" ] \ && [ "$( meterrowget "$L20" 1 nudge )" = "retired" ] \ - && ok "M20 meter: DEFAULT arm is treatment, and treatment's PreToolUse path is silent too" \ - || no "M20 meter: default arm=[$( meterrowget "$L20" 1 arm )] nudge=[$( meterrowget "$L20" 1 nudge )] out=[${OUTM20:+set}]" + && ok "M20 meter: DEFAULT arm is the auto split ($M20WANT for this session), and the PreToolUse path is silent" \ + || no "M20 meter: default arm=[$( meterrowget "$L20" 1 arm )] want=[$M20WANT] nudge=[$( meterrowget "$L20" 1 nudge )] out=[${OUTM20:+set}]" +# Twin for the old default: an EXPLICIT treatment is still honoured, on a session the auto split would +# have put in control (so the arm cannot pass by coincidence). +M20CSID="" +for _m in meter20c1 meter20c2 meter20c3 meter20c4 meter20c5 meter20c6; do + _mh="$( printf '%s' "$_m" | cksum | cut -d' ' -f1 )"; [ "$(( _mh % 100 ))" -lt 50 ] && { M20CSID="$_m"; break; } +done +TM20C="$TMP/tm20c"; mkdir -p "$TM20C"; L20C="$TMP/m20c.jsonl" +run_meter "$L20C" "$( bashjson "$M20CSID" "grep -rn 'needle|other' ." )" "$TM20C" RIPWIRE_METER_ARM=treatment >/dev/null 2>&1 +[ -n "$M20CSID" ] && [ "$( meterrowget "$L20C" 1 arm )" = "treatment" ] \ + && ok "M20c meter: an explicit RIPWIRE_METER_ARM=treatment is honoured over the auto default" \ + || no "M20c meter: explicit treatment gave arm=[$( meterrowget "$L20C" 1 arm )] for control-side session [$M20CSID]" +TM20D="$TMP/tm20d"; mkdir -p "$TM20D"; L20D="$TMP/m20d.jsonl" +run_meter "$L20D" "$( bashjson "$M20CSID" "grep -rn 'needle|other' ." )" "$TM20D" >/dev/null 2>&1 +[ -n "$M20CSID" ] && [ "$( meterrowget "$L20D" 1 arm )" = "control" ] \ + && ok "M20d meter: the same control-side session with no override lands in control (the default really is auto)" \ + || no "M20d meter: unconfigured control-side session [$M20CSID] got arm=[$( meterrowget "$L20D" 1 arm )]" [ "$OUTM19" = "$OUTM20" ] \ && ok "M20b meter: control and treatment produce BYTE-IDENTICAL PreToolUse output (both empty)" \ || no "M20b meter: the arms still differ on the PreToolUse path: control=[$OUTM19] treatment=[$OUTM20]" diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 8babc75e6..a27efb1ac 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -234,6 +234,40 @@ route_run "$H6" "$WITH_RIPWIRE" "$( promptjson junksession "$REPO" "$RECPROMPT" && ok "A6 arm: an unrecognized arm value reads as treatment (no invented third population)" \ || no "A6 arm: arm=banana produced arm=[$( rowget "$H6/routing.jsonl" 1 arm )]" +# THE DEFAULT IS `auto` (issue #381). With no env override and no meter.conf the router used to resolve +# `treatment` for EVERY session, so the pre-registered treatment-minus-control difference could never be +# computed (one machine logged 18968 prompts, 0 of them control). The shipped default must now be the +# meter's own random per-session split. Eight session ids, hashed exactly as the meter hashes them. +# RED on a hook whose default is treatment (the control-side sessions come back treatment). +DEFBAD=""; DEFCTL=0; DEFTRT=0 +for sid in s1 s2 s3 s4 s5 s6 s7 s8; do + h="$( printf '%s' "$sid" | cksum | cut -d' ' -f1 )" + if [ "$(( h % 100 ))" -lt 50 ]; then want=control; DEFCTL=$(( DEFCTL + 1 )); else want=treatment; DEFTRT=$(( DEFTRT + 1 )); fi + HD="$TMP/hd_$sid"; mkdir -p "$HD" + route_run "$HD" "$WITH_RIPWIRE" "$( promptjson "$sid" "$REPO" "$RECPROMPT" )" >/dev/null 2>&1 + got="$( rowget "$HD/routing.jsonl" 1 arm )" + [ "$got" = "$want" ] || DEFBAD="$DEFBAD [$sid -> $got, auto says $want]" +done +[ -z "$DEFBAD" ] && [ "$DEFCTL" -gt 0 ] && [ "$DEFTRT" -gt 0 ] \ + && ok "A7 arm default: no env and no meter.conf resolves to auto (both arms populated: $DEFCTL control / $DEFTRT treatment of 8)" \ + || no "A7 arm default: unconfigured router did not follow the auto split:$DEFBAD (control=$DEFCTL treatment=$DEFTRT)" +# An explicit `arm=treatment` in meter.conf is still honoured (the opt-out), for a session that auto +# would have put in control; and a meter.conf that sets other keys but no arm= takes the same default. +CTLSID="" +for sid in s1 s2 s3 s4 s5 s6 s7 s8; do + h="$( printf '%s' "$sid" | cksum | cut -d' ' -f1 )"; [ "$(( h % 100 ))" -lt 50 ] && { CTLSID="$sid"; break; } +done +H7A="$TMP/h7a"; mkdir -p "$H7A"; printf 'arm=treatment\n' >"$H7A/meter.conf" +route_run "$H7A" "$WITH_RIPWIRE" "$( promptjson "$CTLSID" "$REPO" "$RECPROMPT" )" >/dev/null 2>&1 +[ -n "$CTLSID" ] && [ "$( rowget "$H7A/routing.jsonl" 1 arm )" = "treatment" ] \ + && ok "A8 arm default: an explicit meter.conf arm=treatment is honoured over the auto default" \ + || no "A8 arm default: meter.conf arm=treatment gave arm=[$( rowget "$H7A/routing.jsonl" 1 arm )] for control-side session [$CTLSID]" +H7B="$TMP/h7b"; mkdir -p "$H7B"; printf 'sweep=0\n' >"$H7B/meter.conf" +route_run "$H7B" "$WITH_RIPWIRE" "$( promptjson "$CTLSID" "$REPO" "$RECPROMPT" )" >/dev/null 2>&1 +[ -n "$CTLSID" ] && [ "$( rowget "$H7B/routing.jsonl" 1 arm )" = "control" ] \ + && ok "A9 arm default: a meter.conf with no arm= line follows the auto default too" \ + || no "A9 arm default: arm-less meter.conf gave arm=[$( rowget "$H7B/routing.jsonl" 1 arm )] for control-side session [$CTLSID]" + # ═══════════════════════════════════════════════════════════════════════════════════════════════════ # (O) ADOPTION-WITHIN-TWO — the loop the band is measured through, closed from the PreToolUse hook # ═══════════════════════════════════════════════════════════════════════════════════════════════════ @@ -609,6 +643,43 @@ printf '%s' "$ONOISE4" | grep -Fq -- '--expand' \ && ok "N6 notification: a real prompt merely MENTIONING the marker mid-sentence still routes normally" \ || no "N6 notification: the mid-sentence mention wrongly suppressed a real route: [$ONOISE4]" +# ── MCP channel events and sub-agent hand-backs are harness events too (issue #381, 2026-10-07) ───────── +# Claude Code delivers an MCP channel event (`…`) and a sub-agent hand-back +# (`…`) through this same UserPromptSubmit channel. Field data on +# one machine: roughly half of all injected recommendations were minted from ``; +# near-misses (``, ``, ``) and a mid-sentence mention still route. +CHROUT="Help me understand the implementation of alphaNode" +chan_skip() # chan_skip LABEL SESSION PROMPT +{ + _hd="$TMP/hc_$2"; mkdir -p "$_hd" + _co="$( route_run "$_hd" "$WITH_RIPWIRE" "$( promptjson "$2" "$NREPO" "$3" )" RIPWIRE_METER_ARM=treatment )"; _crc=$? + [ "$_crc" -eq 0 ] && [ -z "$_co" ] && [ "$( rowget "$_hd/routing.jsonl" 1 status )" = "skip-system" ] \ + && ok "$1: skipped with status=skip-system, nothing injected" \ + || no "$1: exit=$_crc out=[$_co] status=[$( rowget "$_hd/routing.jsonl" 1 status )] (expected empty + skip-system)" +} +chan_route() # chan_route LABEL SESSION PROMPT +{ + _hd="$TMP/hc_$2"; mkdir -p "$_hd" + _co="$( route_run "$_hd" "$WITH_RIPWIRE" "$( promptjson "$2" "$NREPO" "$3" )" RIPWIRE_METER_ARM=treatment )" + printf '%s' "$_co" | grep -Fq -- '--expand' && [ "$( rowget "$_hd/routing.jsonl" 1 status )" = "recommend" ] \ + && ok "$1: still routes normally" \ + || no "$1: wrongly suppressed or mis-logged: out=[$_co] status=[$( rowget "$_hd/routing.jsonl" 1 status )]" +} +chan_skip "N7 channel: a event skips" chan1 "$CHROUT" +chan_skip "N7b channel: leading whitespace/blank line before " chan2 "$( printf ' \n\t%s' "$CHROUT" )" +chan_skip "N7c channel: the bare opening tag skips" chan3 "$CHROUT" +chan_skip "N8 agent-message: an hand-back skips" agm1 "$CHROUT" +chan_skip "N8b agent-message: leading whitespace before " agm2 "$( printf '\n\n %s' "$CHROUT" )" +chan_skip "N8c agent-message: the bare opening tag skips" agm3 "$CHROUT" +chan_route "N9 channel near-miss: is not the wrapper" chn1 " $CHROUT" +chan_route "N9b channel near-miss: is not the wrapper" chn2 "$( printf '\n%s' "$CHROUT" )" +chan_route "N9c channel mention: mid-sentence still routes" chn3 "what does mean here? $CHROUT" +chan_route "N9d agent-message near-miss: is not the wrapper" chn4 " $CHROUT" +chan_route "N9e agent-message mention: mid-sentence still routes" chn5 "what does mean here? $CHROUT" +chan_route "N9f channel near-miss: a bare word 'channel' in prose routes" chn6 "channel the energy: $CHROUT" + # ═══════════════════════════════════════════════════════════════════════════════════════════════════ # O9 — rw_is_ripwire_call: ONE block, three files, and the shapes an agent actually types (PR #215 item 6) # ═══════════════════════════════════════════════════════════════════════════════════════════════════ @@ -632,6 +703,21 @@ else no "O9 the three copies of rw_is_ripwire_call have DRIFTED — the hooks and the meter will disagree on the same command line" fi +# The harness-event list is a second mirrored block (issue #381): the Claude and Codex prompt hooks must +# agree on which wrappers are not user input, or one agent's router keeps minting recommendations from +# channel events while the other does not. +extract_hblock(){ awk '/^# ---- BEGIN MIRRORED BLOCK rw_is_harness_event/,/^# ---- END MIRRORED BLOCK rw_is_harness_event/' "$1"; } +HB_CLAUDE="$TMP/hb_claude.sh"; HB_CODEX="$TMP/hb_codex.sh" +extract_hblock "$ROOT/hooks/ripwire-claude-route.sh" > "$HB_CLAUDE" +extract_hblock "$ROOT/hooks/ripwire-codex-route.sh" > "$HB_CODEX" +if [ ! -s "$HB_CLAUDE" ]; then + no "O9b the rw_is_harness_event block is absent from hooks/ripwire-claude-route.sh (the diff below would prove nothing)" +elif diff -q "$HB_CLAUDE" "$HB_CODEX" >/dev/null; then + ok "O9b rw_is_harness_event is byte-identical in the Claude and Codex prompt hooks ($( wc -l < "$HB_CLAUDE" | tr -d ' ' ) lines)" +else + no "O9b the two copies of rw_is_harness_event have DRIFTED — Claude and Codex disagree on what a user prompt is" +fi + # The rule itself, sourced from the claude hook's copy so the arm tests what ships. cat "$B_CLAUDE" > "$TMP/rule.sh" printf 'if rw_is_ripwire_call "$1"; then echo 1; else echo 0; fi\n' >> "$TMP/rule.sh" diff --git a/test/routingreportcheck.sh b/test/routingreportcheck.sh index f857b4ab8..a8f782101 100755 --- a/test/routingreportcheck.sh +++ b/test/routingreportcheck.sh @@ -322,6 +322,57 @@ echo "$OUT8T" | grep -q '=== router=toolcall ===' && ok "--router=toolcall: show echo "$OUT8T" | grep -q '=== router=prompt ===' && no "--router=toolcall: leaked the prompt section too" \ || ok "--router=toolcall: does not print the prompt section" +# ── C/D: per-arm counts, a loud NO CONTROL ARM, and the registered readout date (issue #381) ────────── +# A treatment-only log is what an unconfigured install used to produce. The report must say so in so many +# words, however many treatment prompts there are, and must NOT say it when both arms have prompts. +DC="$TMP/dc"; mkdir -p "$DC" +python3 - "$DC" <<'PYEOF' +import json, sys +rows = [{"v": 2, "at": "2026-10-01T10:00:00Z", "agent": "claude", "event": "UserPromptSubmit", "status": "recommend", + "intent": "x", "recommended": "--for", "arm": "treatment", "session_hash": str(1000 + i), + "prompt_hash": str(2000 + i), "prompt_bytes": 10} for i in range(5)] +open(sys.argv[1] + "/routing.jsonl", "w").write("\n".join(json.dumps(r) for r in rows) + "\n") +open(sys.argv[1] + "/substitution.jsonl", "w").write("") +PYEOF +OUTC="$( run "$DC" )" +echo "$OUTC" | grep -q 'NO CONTROL ARM' && ok "C1: a treatment-only log prints NO CONTROL ARM" \ + || no "C1: treatment-only log did not say NO CONTROL ARM -- $OUTC" +echo "$OUTC" | grep -q 'arm counts: treatment=5 control=0' && ok "C2: per-arm counts are printed (treatment=5 control=0)" \ + || no "C2: per-arm count line missing or wrong -- $( echo "$OUTC" | grep 'arm counts' )" +echo "$OUTC" | grep -q 'NO TREATMENT ARM' && no "C3: a log WITH treatment prompts claimed NO TREATMENT ARM" \ + || ok "C3: no false NO TREATMENT ARM when treatment has prompts" +echo "$OUT1" | grep -q 'NO CONTROL ARM' && no "C4: a log with both arms populated still printed NO CONTROL ARM" \ + || ok "C4: both arms populated -> no NO CONTROL ARM line" +echo "$OUT1" | grep -q 'arm counts: treatment=43 control=40' && ok "C5: per-arm counts on the both-arms fixture (43 treatment incl. 3 edge-case prompts / 40 control)" \ + || no "C5: per-arm count line wrong on the 43/40 fixture -- $( echo "$OUT1" | grep 'arm counts' )" + +# The readout date is read from the registration file. Synthetic registrations pin each state; the last +# arm reads the REAL docs/EVALS.md so the registration cannot lose its date without this gate noticing. +EV="$TMP/evals.md" +printf '## x\n\n### Claude Code prompt router — PRE-REGISTERED 2026-09-02 (test)\n\nbody\n\n**Readout date:** 2026-09-30\n**Readout date, extended once:** 2026-10-14\n\n### Next section\n\n**Readout date:** 2020-01-01\n' >"$EV" +ro(){ python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" --evals "$EV" --today "$1" 2>&1; } +ro 2026-10-07 | grep -q 'registered readout date: 2026-09-30 -- PASSED 7 day' && ok "D1: a passed readout date is printed and flagged PASSED" \ + || no "D1: passed date not flagged -- $( ro 2026-10-07 | grep -i readout )" +ro 2026-10-07 | grep -q 'extended readout date (one-time): 2026-10-14 -- in 7 day' && ok "D2: the extension date, not yet passed, is printed without the flag" \ + || no "D2: extension date line wrong -- $( ro 2026-10-07 | grep -i extended )" +ro 2026-09-20 | grep -q 'PASSED' && no "D3: nothing has passed on 2026-09-20 but the report said PASSED" \ + || ok "D3: before both dates nothing is flagged PASSED" +ro 2026-09-30 | grep -q 'registered readout date: 2026-09-30 -- TODAY' && ok "D4: on the day itself the line says TODAY" \ + || no "D4: same-day line wrong -- $( ro 2026-09-30 | grep -i readout )" +ro 2026-10-20 | grep -q 'extended readout date (one-time): 2026-10-14 -- PASSED 6 day' && ok "D5: the extension flags PASSED too, once it passes" \ + || no "D5: passed extension not flagged -- $( ro 2026-10-20 | grep -i extended )" +ro 2026-10-07 | grep -q '2020-01-01' && no "D6: a readout date from a LATER section leaked into the report" \ + || ok "D6: only the prompt-router section's date is read" +printf '## nothing here\n' >"$EV" +ro 2026-10-07 | grep -q 'readout date: UNKNOWN' && ok "D7: a registration without a date prints UNKNOWN instead of staying silent" \ + || no "D7: missing date not reported -- $( ro 2026-10-07 | grep -i readout )" +python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" --evals "$TMP/no-such-evals.md" --today 2026-10-07 2>&1 \ + | grep -q 'readout date: UNKNOWN.*not readable' && ok "D8: an unreadable registration file prints UNKNOWN" \ + || no "D8: unreadable registration file not reported" +python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" --today 2026-10-07 2>&1 \ + | grep -Eq 'registered readout date: 20[0-9]{2}-[0-9]{2}-[0-9]{2} -- ' && ok "D9: the shipped docs/EVALS.md carries a parseable readout date" \ + || no "D9: docs/EVALS.md's prompt-router registration has no parseable readout date" + echo "" if [ "$fail" = 0 ]; then echo "ALL PASS" diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 053142007..7bb6ba1a8 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -236,6 +236,25 @@ case "$SYS3" in *'status="abstain"'*) ok "leading whitespace before the marker s SYS4="$( route 'what does mean in the hook? also, help me understand the implementation of targetSymbol' )" case "$SYS4" in *'status="recommend"'*'intent="understand-symbol"'*'--expand='*'targetSymbol'*) ok "a real prompt merely mentioning the marker mid-sentence still routes normally";; *) no "mid-sentence mention wrongly suppressed a real route: $SYS4";; esac +# MCP channel events (``) and sub-agent hand-backs (``) arrive +# on the same channel (issue #381). The prefix is the opening tag plus a space or `>`; every positive +# below ends with a sentence that routes by itself (SYS4's), so only the guard can make it abstain. +SYSTASK='help me understand the implementation of targetSymbol' +for sysp in "$SYSTASK" "$SYSTASK" \ + "$( printf '\n \t%s' "$SYSTASK" )" \ + "$SYSTASK" "$SYSTASK" \ + "$( printf ' \n%s' "$SYSTASK" )"; do + SYSC="$( route "$sysp" )" + case "$SYSC" in *'status="abstain"'*'resolved_symbols="0"'*) ok "harness wrapper abstains: $( printf '%s' "$sysp" | tr '\n\t' ' ' | cut -c1-40 )";; *) no "harness wrapper routed: $sysp => $SYSC";; esac +done +# Near-misses: a different tag, or a mention that is not the leading bytes, is a user prompt and routes. +for nearp in " $SYSTASK" " $SYSTASK" " $SYSTASK" \ + "what does mean? $SYSTASK" "what does mean? $SYSTASK" \ + "channel the energy: $SYSTASK"; do + SYSN="$( route "$nearp" )" + case "$SYSN" in *'status="recommend"'*'--expand='*'targetSymbol'*) ok "near-miss still routes: $( printf '%s' "$nearp" | cut -c1-40 )";; *) no "near-miss wrongly suppressed: $nearp => $SYSN";; esac +done + # ── the weak tier may not confirm itself, and may not read a config key as code (2026-09-10) ─────────── # Two independent defects, two independent arms each; all four recommend-side arms are RED against a # pre-change binary (each recommended understand-symbol with an --expand). diff --git a/test/toolcallroutecheck.sh b/test/toolcallroutecheck.sh index 721180572..65fa5eda6 100755 --- a/test/toolcallroutecheck.sh +++ b/test/toolcallroutecheck.sh @@ -35,7 +35,10 @@ ln -sf "$BIN" "$BINDIR/ripwire" export PATH="$BINDIR:$PATH" export RIPWIRE_HOME="$TMP/home"; mkdir -p "$RIPWIRE_HOME" export TMPDIR="$TMP/tmp"; mkdir -p "$TMPDIR" -unset RIPWIRE_METER_ARM RIPWIRE_METER RIPWIRE_ROUTE_METER +unset RIPWIRE_METER RIPWIRE_ROUTE_METER +# The unconfigured default arm is `auto` (issue #381), so the corpus pass below pins `treatment` to score the +# classifier on every row; the default itself is asserted separately in the "default arm" section. +export RIPWIRE_METER_ARM=treatment LOG="$RIPWIRE_HOME/routing.jsonl" run_hook() @@ -146,12 +149,49 @@ tail -n1 "$LOG" | jq -e '.status == "abstain" and .reason == "cap"' >/dev/null 2 : >"$LOG" export RIPWIRE_METER_ARM=control COUT="$( run_hook Bash "$( jq -cn '{command:"grep -rn controlPattern src/"}' )" "control-session-1" )" -unset RIPWIRE_METER_ARM +export RIPWIRE_METER_ARM=treatment if [ -z "$COUT" ]; then ok "control arm: injects nothing"; else no "control arm: injected anyway -- $COUT"; fi tail -n1 "$LOG" | jq -e '.status == "recommend" and .arm == "control" and .recommended == "--grep"' >/dev/null 2>&1 \ && ok "control arm: still logs the recommend decision (status/arm/recommended all correct)" \ || no "control arm: logged row wrong -- $( tail -n1 "$LOG" )" +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# Default arm (issue #381): no RIPWIRE_METER_ARM and no meter.conf resolves to `auto`, the meter's own +# session-hash split. It used to be `treatment` for every session, so the registered treatment-minus- +# control difference could never be computed. RED on a hook whose default is treatment. An explicit +# `arm=treatment` in meter.conf is still honoured. +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +DEFBAD=""; DEFCTL=0; DEFTRT=0; DEFCTLSID="" +for sid in d1 d2 d3 d4 d5 d6 d7 d8; do + h="$( printf '%s' "$sid" | cksum | cut -d' ' -f1 )" + if [ "$(( h % 100 ))" -lt 50 ]; then want=control; DEFCTL=$(( DEFCTL + 1 )); [ -n "$DEFCTLSID" ] || DEFCTLSID="$sid"; else want=treatment; DEFTRT=$(( DEFTRT + 1 )); fi + : >"$LOG" + ( unset RIPWIRE_METER_ARM; run_hook Bash "$( jq -cn --arg c "grep -rn defaultArm$sid src/" '{command:$c}' )" "$sid" >/dev/null ) + got="$( tail -n1 "$LOG" | jq -r '.arm // ""' 2>/dev/null )" + [ "$got" = "$want" ] || DEFBAD="$DEFBAD [$sid -> $got, auto says $want]" +done +[ -z "$DEFBAD" ] && [ "$DEFCTL" -gt 0 ] && [ "$DEFTRT" -gt 0 ] \ + && ok "default arm: unconfigured toolroute follows the auto split (both arms populated: $DEFCTL control / $DEFTRT treatment of 8)" \ + || no "default arm: unconfigured toolroute did not follow the auto split:$DEFBAD (control=$DEFCTL treatment=$DEFTRT)" +: >"$LOG"; printf 'arm=treatment\n' >"$RIPWIRE_HOME/meter.conf" +( unset RIPWIRE_METER_ARM; run_hook Bash "$( jq -cn '{command:"grep -rn confTreat src/"}' )" "$DEFCTLSID" >/dev/null ) +[ -n "$DEFCTLSID" ] && [ "$( tail -n1 "$LOG" | jq -r '.arm // ""' 2>/dev/null )" = "treatment" ] \ + && ok "default arm: an explicit meter.conf arm=treatment is honoured over the auto default" \ + || no "default arm: meter.conf arm=treatment gave $( tail -n1 "$LOG" | cut -c1-160 ) for control-side session [$DEFCTLSID]" +rm -f "$RIPWIRE_HOME/meter.conf" + +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# This hook's notification guard matches markers INSIDE a tool input; it deliberately does NOT carry the +# prompt hooks' `` is ordinary RSS/XML text that a grep pattern +# legitimately names. A grep for it must still route. +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +: >"$LOG" +CHOUT="$( run_hook Grep "$( jq -cn '{pattern:"",path:"src"}' )" "chan-tool-1" )" +tail -n1 "$LOG" | jq -e '.status == "recommend"' >/dev/null 2>&1 && [ -n "$CHOUT" ] \ + && ok "a Grep whose pattern names still routes (the guard is for prompt wrappers, not tool inputs)" \ + || no "a Grep for was suppressed: out=[$CHOUT] row=$( tail -n1 "$LOG" | cut -c1-160 )" + # ═══════════════════════════════════════════════════════════════════════════════════════════════════ # Malformed / degenerate input: exit 0, empty (or valid-JSON) stdout, never a crash. # ═══════════════════════════════════════════════════════════════════════════════════════════════════ From 832c0bad7bcb10f260218fb9f074dadf8597c970 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 17:35:26 -0400 Subject: [PATCH 123/229] refactor: one innermost-enclosing-function walk for FE-A and FE-B; goPackageImportPath helper ReceiverEvidence::enclosingFn and FalseEdgeRules::enclosingFunction were the same 129-token walk over a per-file sorted function list (quality-delta duplication); both now call innermostEnclosingFn. collectGoModules hands the import-path join to goPackageImportPath, so its complexity and length stay at the train-26a figures. No output change intended. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 64 +++++++++++++++++++++--------------------- src/receiverevidence.h | 48 ++++++++++++++++--------------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/src/graph.h b/src/graph.h index 9b298d885..c95d5ff47 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4907,29 +4907,7 @@ struct FalseEdgeRules return false; } // the innermost function/method of the same file whose span strictly holds `id`'s span; kNoNode when none - NodeId enclosingFunction( NodeId id ) const - { - const Symbol& inner = ing.symbols[ id ]; - if( inner.fileId >= functionsByFile.size() ) - { - return kNoNode; - } - NodeId best = kNoNode; - for( NodeId c : functionsByFile[ inner.fileId ] ) - { - const Symbol& s = ing.symbols[ c ]; - if( s.sigStartByte > inner.sigStartByte ) - { - break; // sorted by start: nothing later can hold it - } - const bool holds = c != id && s.endByte >= inner.endByte && ( s.sigStartByte < inner.sigStartByte || s.endByte > inner.endByte ); - if( holds ) - { - best = c; // later starts are more inner - } - } - return best; - } + NodeId enclosingFunction( NodeId id ) const { return innermostEnclosingFn( ing, functionsByFile, id ); } // Rule (1) for a receiverless call, per language. Nothing dropped → Keep: the ladder decides exactly as before. What // is left when something was dropped is narrowed by the language's own visibility where the extractor records it; @@ -5206,6 +5184,36 @@ inline std::vector goModuleTreePaths( std::string_view text ) return paths; } +// FE-B: a Go package's import path — its go.mod's module path, then the package directory below the go.mod's directory +// (`example.com/qm` + `quoted/lib` under `quoted` → `example.com/qm/lib`); "" when the go.mod has no module line. +inline std::string goPackageImportPath( std::string_view modulePath, std::string_view modDir, std::string_view pkgDir ) +{ + if( modulePath.empty() ) + { + return {}; + } + std::string_view below = pkgDir.substr( std::min( modDir.size(), pkgDir.size() ) ); + below.remove_prefix( below.starts_with( '/' ) ? 1 : 0 ); + std::string path( modulePath ); + if( !below.empty() ) + { + path.append( 1, '/' ).append( below ); + } + return path; +} + +// FE-B: record `fileId`'s package import path, its go.mod found in disk directory `disk` — root-relative, the walk's +// `walkRel` cut at `walkCut` (npos: the root itself) +inline void noteGoPackagePath( const IngestResult& ing, FalseEdgeRules& rules, const HashMap& modLineOfDir, + const std::string& disk, std::uint32_t fileId, std::string_view walkRel, std::size_t walkCut ) +{ + if( const auto ml = modLineOfDir.find( disk ); ml != modLineOfDir.end() ) + { + const std::string_view modDir = walkCut == std::string_view::npos ? std::string_view{} : walkRel.substr( 0, walkCut ); + rules.goPackagePath[ fileId ] = goPackageImportPath( ml->second, modDir, includerDir( rootRelPath( ing, fileId ) ) ); + } +} + // FE-A's Go module census: for every Go file, the go.mod at or above its directory INSIDE the root (read once per // directory); every module path found joins goModules. A file with none above it is unknown (goUnderModule 0). inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) @@ -5223,7 +5231,6 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) } std::string_view rel = rootRelPath( ing, s.fileId ); std::string disk = diskPath( ing, s.fileId ); - const std::string_view fileDir = includerDir( rel ); // walk up as many directories as the root-relative path has, never above the root while( true ) { @@ -5246,14 +5253,7 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) if( !it->second.empty() ) { rules.goUnderModule[ s.fileId ] = 1; - // FE-B: the package's import path — the module path, then the file's directory below the go.mod's - if( const auto ml = modLineOfDir.find( disk ); ml != modLineOfDir.end() && !ml->second.empty() ) - { - const std::string_view modDir = relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ); - std::string_view below = fileDir.substr( std::min( modDir.size(), fileDir.size() ) ); - below.remove_prefix( below.starts_with( '/' ) ? 1 : 0 ); - rules.goPackagePath[ s.fileId ] = below.empty() ? ml->second : ml->second + "/" + std::string( below ); - } + noteGoPackagePath( ing, rules, modLineOfDir, disk, s.fileId, rel, relCut ); for( const std::string& m : it->second ) { if( seenModule.try_emplace( m, '\0' ).second ) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 0b1c3c319..16e765585 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -86,6 +86,31 @@ inline bool isSuperRoot( std::string_view root ) noexcept return root == "super" || root == "base"; } +// The innermost function/method of `id`'s file whose span strictly holds `id`'s span; kNoNode when none. `byFile` is +// fileId → that file's functions/methods sorted by sigStartByte. Shared by ReceiverEvidence and graph.h FalseEdgeRules. +inline NodeId innermostEnclosingFn( const IngestResult& ing, const std::vector>& byFile, NodeId id ) +{ + const Symbol& inner = ing.symbols[ id ]; + if( inner.fileId >= byFile.size() ) + { + return kNoNode; + } + NodeId best = kNoNode; + for( NodeId c : byFile[ inner.fileId ] ) + { + const Symbol& s = ing.symbols[ c ]; + if( s.sigStartByte > inner.sigStartByte ) + { + break; // sorted by start: nothing later can hold it + } + if( c != id && s.endByte >= inner.endByte && ( s.sigStartByte < inner.sigStartByte || s.endByte > inner.endByte ) ) + { + best = c; // later starts are more inner + } + } + return best; +} + struct ReceiverEvidence { static constexpr std::size_t kWalkCap = 64; // classes visited per base walk; deterministic (sorted chaUp) @@ -320,28 +345,7 @@ struct ReceiverEvidence // ── queries ─────────────────────────────────────────────────────────────────────────────────────────────────────── // the innermost function/method of the same file whose span strictly holds `id`; kNoNode when none - NodeId enclosingFn( NodeId id ) const - { - const Symbol& inner = ing.symbols[ id ]; - if( inner.fileId >= fnsByFile.size() ) - { - return kNoNode; - } - NodeId best = kNoNode; - for( NodeId c : fnsByFile[ inner.fileId ] ) - { - const Symbol& s = ing.symbols[ c ]; - if( s.sigStartByte > inner.sigStartByte ) - { - break; - } - if( c != id && s.endByte >= inner.endByte && ( s.sigStartByte < inner.sigStartByte || s.endByte > inner.endByte ) ) - { - best = c; - } - } - return best; - } + NodeId enclosingFn( NodeId id ) const { return innermostEnclosingFn( ing, fnsByFile, id ); } // the class a typed local names in `from` or a function enclosing it; nullptr when untyped (or tombstoned) const std::string* typedLocal( NodeId from, std::string_view var ) const { From bcb1da3ddad3b39f1a0c1e2f5272ec16ef5e408b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 17:48:30 -0400 Subject: [PATCH 124/229] chore(quality): ack lane FE-B's quality-delta footprint vs train 26a (reasons in the ledger) Co-Authored-By: Claude Opus 5.5 --- .ripwire_quality_acks | 91 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index fff1468da..8ee5bc57e 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -396,13 +396,16 @@ ack complexity 1cdbcf525f9e7871 60 cid=4a92ba4004e888e5 lane/tc-sliceat: the --a ack complexity 1da01868deceb731 21 cid=bce36ef4a75ab3fb by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack complexity 1e9c684ddd70ed1f 27 cid=f3502fc85213b238 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack complexity 205bde7df6b15388 26 cid=a790a90cc086ddef by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) -ack complexity 20b8113908960e36 40 cid=c93e456ce00ceaec capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack complexity 20b8113908960e36 52 cid=8333a4060e6ba0f6 now=52 was=48 p=src/verbs_navigate.h:2095 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity 218041bccb277b92 76 cid=69bc85c464d51fa1 lane B1 cap disclosure: the census IS the +3. The raw-token window moved out of the while condition so the scan reaches the end of the task text and counts every qualifying token, which is what makes mention_tokens_total= an exact number instead of a bare 'something was dropped'. The kept set is unchanged and the scan is bounded by one sentence of task text. ack complexity 231d5965c5d8503b 52 cid=0ecc0b97fecbcc72 P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). ack complexity 240fa4dc89f6442d 23 cid=ca5968a9693b8433 churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. ack complexity 2434bf5574dc575b 16 cid=e3c081715b00762c now=16 was=0 p=src/quality.h:5813 lane/ack-provenance-backfill: the legacy-ack backfill (clone kinds reconstructed from the current tree, marked prov=recon), its refresh/unverified freshness rule, and qackconcurrencycheck arm 9 — plus this lane's own minor footprint on the ack reader/writer it extends +ack complexity 2570a290e1506158 33 cid=86af5bb8f2dd4ec2 now=33 was=0 p=src/receiverevidence.h:687 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 2abd26fdd3d1e6df 66 cid=80501bdf3db6fba6 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. | prior: P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) ack complexity 2b0d39faec60fc08 20 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack complexity 2bea60141008e4d0 50 cid=0f6117d2b7b8058c now=50 was=0 p=src/receiverevidence.h:261 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity 305daad7cbc4548c 24 cid=b80c4dd4359061a0 now=24 was=0 p=src/receiverevidence.h:176 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 31105421c3ce88ae 47 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: L10: --doctor legend + blobs_floor= disambiguation (finding 6) — DoctorCacheStats gains capHit, doctorCacheStats sets it, runDoctor emits blobs_floor= and the new legend comment; short-horizon-churn and the verbosity bump are the direct, deliberate cost of that ack complexity 3298be65bf6058ef 408 cid=dbfe71c5e9704f93 2026-09-06 stranger-audit rows 8-12: the two refusals (unreadable root, --cache=) are two guarded calls in dispatchMain; stripHomePair's churn is the two-segment envelope itself | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack complexity 3561d0281d324276 99 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -414,9 +417,14 @@ ack complexity 3ba0d3f31d319672 24 cid=e5a559aa3504947e at-seed decision round ( ack complexity 3be1c13661e5a63c 20 pack-task budget round (verifier K1+K2, 2026-08-19): +4 ccx / +14 LOC in packTaskBundleText is the section REORDER (bodies allocated last, after the four fine-grained prefix sections) plus the one-shot reflow lap's ranking branch; the three list-section top-ups were factored into reflowListSection rather than inlined, which is why the delta is this small. selectMonotoneBodySubset +1 ccx / +8 LOC is the one early return that admits the top-ranked candidate at every pool. churn=self on both is this change's own edit window. Measured payoff on this repo, --pack-task=rank the call graph: callers 13/20 to 20/20 and fill 52.5% to 59.8% at the default 6000-token budget, 43.5% to 90.6% at 8000; the task-named body now survives every budget increase. Both properties gated red-first in test/packtaskmonotoncheck.sh ack complexity 3c07d993bfdbce53 77 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack complexity 3d87404c1cdf50ec 27 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack complexity 3dfd850df2b251c4 42 cid=bcd12702a7b47ec5 now=42 was=41 p=src/nonlocalstate.h:559 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity 426a1b33e60ac8f4 27 cid=f38b11013db6caeb now=27 was=0 p=src/receiverevidence.h:452 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 44b41056b999b501 33 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack complexity 44b4c7b78ee480d6 19 cid=6da24121cdbb52f4 capture-audit 2026-09-04 wave-1 close, lane L5 H7 (lane-L5.md §2): enumerateRefs counts DECLARED ref names matching the --stray-content filter before the HEAD-is-not-stray exclusion, so an empty SELECTION refuses while a real zero stays an answer — the +4 complexity is that count and its guard ack complexity 45b52ada32f63c11 195 cid=e9e4c8dd62e800e6 at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack +ack complexity 45bb9f7c5bc37916 33 cid=c8ed741b38b4c0a9 now=33 was=31 p=src/serialize.h:6655 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity 47a961ce3b905fea 26 cid=02e812a08cd1c540 now=26 was=0 p=src/ingest_binds.h:127 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity 48768a385100895f 46 cid=5cbec57476270c83 now=46 was=44 p=src/verbs_for.h:3595 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 49ea9954a4e77a06 67 cid=638312475b7a6935 round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack complexity 4b309450f25c2b44 722 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack complexity 4b8467d6559779e4 22 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history @@ -427,19 +435,20 @@ ack complexity 52c2e3e32753dd28 161 cid=32fe671bf177bea8 Review of #214, round 2 ack complexity 5361e2bced6f1988 23 cid=b34695b5d2051f1d capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 follow-up (capture-audit L9): --ensemble gained root=/root-relative p= — writeEnsembleReport's 3 new default-valued params (singleRoot/rootPrefix/rootAttr, back-compat) thread the caller's already-computed single-root spelling through; short-horizon-churn on the touched dispatcher. ack complexity 53a1214d5505fa41 433 cid=e155c3fb2ceeb9f1 lane V1 N4 (bd8d666): the no-baseline half of the M8 --since policy decided beside the shape/range validation, from SinceScope::baselineSha | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity 53b3f823688054b4 83 cid=137a04e79db61a58 by=src/* §N6-C .gitignore-by-default: the crawl gains an ignore mode. The two api-surface/params rows are ONE deliberate contract change — ingest()/collectSources() take a trailing defaulted respectGitignore, the only way a CLI flag can reach the crawl without a global; the three short-horizon-churn rows are this lane's own edits to the flag ledger, the crawl and the --skipped verb, which is what adding a flag with a disclosure IS; collectSources +3 ccx / +11 LOC is what remains after the probe, the mode and the prune fan-out were extracted into probeIgnoreSet/recordDirPrune (it was +15/+43 inline). +ack complexity 569699e57b2a3d87 16 cid=6c5914500a133567 now=16 was=13 p=src/ingest_binds.h:2370 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 572dbeee3b452cbc 83 cid=4e4f42aee136bfe3 by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack complexity 574641dcc1bdf0ec 110 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity 59f050855874875f 28 cid=79ec141006215e25 by=src/* P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. | prior: M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. -ack complexity 5b224c7fe142bd56 322 cid=a785260560dbe199 by=src/* T14: deriveForConfidence gains homonymDecline (bool, no default — every call site updated in this same commit): confidence= must not claim high on a cut declined for a large name-exact homonym pool, and the flag has to reach both callers (CLI --for and MCP for) that already independently call this function with their own adaptiveCut result. No default was given deliberately, so a future call site cannot forget it silently. The one minor complexity row on runForLens (317->322) is the decline branch inline in the existing --adaptive if/else. | prior: round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes +ack complexity 5b224c7fe142bd56 328 cid=75447556b5ecaf1d by=src/* now=328 was=322 p=src/verbs_for.h:2249 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: T14: deriveForConfidence gains homonymDecline (bool, no default — every call site updated in this same commit): confidence= must not claim high on a cut declined for a large name-exact homonym pool, and the flag has to reach both callers (CLI --for and MCP for) that already independently call this function with their own adaptiveCut result. No default was given deliberately, so a future call site cannot forget it silently. The one minor complexity row on runForLens (317->322) is the decline branch inline in the existing --adaptive if/else. ack complexity 5d8f5288f0df10f7 191 cid=f31f8078003db35b capture-audit 2026-09-04 lane L1 (flag-table universe), the seven gating rows of this lane's own five commits, each the documented cost of the guard/table it lands: parseArgs +4 cx is the --exclude= empty-value guard, one branch in the hand-written residue, the exact shape of its --and=/--not= siblings (M6); validateConfig +4 cx/+17 LOC is the bare --quality-ack pairing refusal plus the =REASON implication moved out of the parser arm (H10); validateModifierGuards +4 cx/+21 LOC is two pairing guards (--naming-locals refuse, --no-stable notice) in the helper every prior guard grew the same way (M16); kShapingVerbs 19->62 LOC is the ledger of every verb outside honorsPaging — 36 rows for 36 verbs, a table is its length, and test/shapingflagcheck.sh arm (F) is what stops it rotting (H3); main +11 LOC is the --json check moved ahead of the CLI edit bridge plus one call to refuseInertMainModifiers (H2/M16) — the two guards themselves and jsonUnsupportedVerb's allow-list walk live in their own helpers. Gates for all five written red first; battery green at this head. churn=self rows on jsonUnsupportedVerb/kViewFlags/writeAckRecords are this lane's own rewrites and are invisible in ref-pair mode by construction. | prior: R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). ack complexity 5ec38fbd414fa4d4 56 cid=35883c203ba33e28 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh ack complexity 5f7ae7ac9bbef3c5 33 cid=8662c6730ed46798 now=33 was=30 p=src/darkflags.h:522 0.6.6 D5 review B1: forEachCodeByte gains the per-file TemplateStack (JS/TS template literals span lines); its one caller, harvestEnvReads, is updated in the same commit ack complexity 61e5df9e1e40ff70 19 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack complexity 639de1c3670999f9 69 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity 685ade09a168535e 30 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean -ack complexity 6979fa794f2a5154 389 cid=9132efbcc6dfcbb7 now=389 was=386 p=src/ingest_sidecap.h:1706 fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam | prior: ruby-attr class-DSL lane (review round; kParserVer 119->120): duplication/new-clone elixirArguments|elixirTarget is token-shape coincidence only - elixirTarget is the shared fieldIdentifierText primitive plus a null guard, elixirArguments reads a different field with no identifier gate; no shared logic to lift. captureTagsFacts +1cx/+4verb is the two-line gated call site of the inline-visibility lift. +ack complexity 6979fa794f2a5154 416 cid=027d9c38f3666ebb now=416 was=415 p=src/ingest_sidecap.h:1732 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam ack complexity 6a38d70787d09e64 28 cid=f6a0c09dfc153ece Kotlin port (PR #126): the leading whitespace sample is walked by codepoint (jsonesc::utf8SeqLen) so invalid UTF-8 folds into errNodes/errBytes, deduplicated against the ERROR spans tree-sitter recovery already counted; without it a garbage byte run swallowed by recovery reads as a healthy file in --skipped. -ack complexity 6e58b0a307757079 85 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack complexity 6e58b0a307757079 106 cid=cd68c661a9c12758 now=106 was=100 p=src/serialize.h:8704 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity 7051b3950aaf4c14 35 cid=7919a3d486bba416 lane B1 cap disclosure: doc_mentions_capped= is contracted to be PROVABLE, never guessed, and that proof is the +6/+16. It is the in-loop test that a doc below its anchor's lift target was the one a cap turned away, plus docLiftWasRefused for the other half of the total cap, which ends the OUTER loop over anchors already consulted. A one-line flag would instead say 'there might be more', which is the fabricated fact in the other direction. ack complexity 75bf40d0284fc951 16 cid=9fe19a9429222b7f cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. ack complexity 763ff87870373241 23 cid=0f3e66f03f7be1f1 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement @@ -447,43 +456,52 @@ ack complexity 767211b0b7743f79 23 cid=f8439fcff51f596b capture-audit 2026-09-04 ack complexity 779387a679944627 25 cid=c93357a7d8868988 Kotlin port (PR #126): function_value_parameters counts parameter children only. A Kotlin parameter's vararg modifier and its default-value expression are SIBLINGS of the parameter node in this grammar, so the generic every-named-child rule reads 3 real params as 5 (kotlincheck §10 pins params=3); the Kotlin arm is one guarded branch inside the existing loop. ack complexity 7a04eee0ff6ec2d7 28 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack complexity 7a74ae49acb306cf 78 cid=42744dca5ad71517 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. +ack complexity 7b4ba1b688c902ab 25 cid=d53ccc7632e3889a now=25 was=13 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 7c0e356e60b323ba 19 cid=02317a43a1806043 at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack complexity 7e26e5533d486c7a 39 cid=5220b8e1fc725dbe round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: F5: fromTraceBundleText grows by the named-vs-default ceiling-notes pair, the smallest-named-ceiling computation and the reasoning comments. The rung-sniff it replaces was one line and wrong; the property is the fix. ack complexity 7ed8ad2c213537a4 100 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack complexity 81fbe59b4a35659b 89 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity 851e83b4505f10f6 68 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. -ack complexity 89b3fd14c192c899 779 cid=bf9296f0f9d8d2a3 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: wave merge 2026-08-20 (harvestexec): TWO seams in buildGraph, both from this wave, measured together at the merged head (cx 753->764, LOC 1327->1360). (a) The RefRole::Type namespace gate, from lane/resolver-precision: +8 cx / +23 LOC, ONE inserted block - a stable in-place filter over the assembled candidate set (12 lines of code) plus the 9-line derivation comment above it, plus a 3-line re-route of the isClassLike lambda through the same predicate. Not extracted to a helper on purpose, and the reason is the gate's own subject matter - on THIS loop the predicate is a provable no-op (the loop admits only role=Call, un-narrowed by resolve.h doctrine, and role=Macro, which retagMacroCallReferences already proves uniquely macro), so the block exists to be the ONE seam a future round edits, and hiding it behind a call would move the lines without removing them while making that edit invisible. test/nsfiltercheck.sh arm 2 is the executable form of the no-op claim and fails if the narrowing ever starts biting here. The measurable effect lives in contextratio.h's all-roles resolution, and the role that reaches it is RefRole::Extends, NOT RefRole::Type - collectFacts continues on Type 28 lines before it calls resolveCandidates, so a Type reference can never arrive there, and what keeps a type mention from spraying across same-named functions is that continue, not this predicate. What the predicate keeps from spraying is a BASE CLAUSE: on test/nsfilterfix, 'class Derived : public Handler' binds to both 'class Handler' and the free 'int Handler( int )' without it (ents 1->2, amb 0->1). Corrected 2026-08-20 by adversarial verification (V-5): the two source comments this ack leaned on named the wrong role, and test/nsfiltercheck.sh arm 5 now pins the real effect on the real role - the gate is red under full removal of the narrowing, which it was not before. Acked separately below. (b) The S5-E compose lang guard, from claude/kind-kepler-0c9b90: +3 cx / +10 LOC, one langCompatible branch plus its rationale comment, so HAS-A resolution applies the same language gate as every other admission site; before it a C++ member bound to a Python/TS same-named class and the duplicate defeated the (ownerSym,fieldName) dedup. Recorded red-first by test/composelangcheck.sh (mixed-lang fixture test/composelangfix, C<->C++ bridge control kept green). Cost is bounded and one-time for both: buildGraph does not grow again when a role or a language is added, only when a seam is. +ack complexity 89b3fd14c192c899 1203 cid=9fe16468f6ebb08e by=src/* now=1203 was=983 p=src/graph.h:5400 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack complexity 8a92173ded649e17 46 cid=a2c4a7904b16ec5b by=src/* A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity 8d58de9bb922f582 43 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack complexity 8dadfd869fbf7649 139 cid=2a94511e3d636398 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch -ack complexity 90d3966814ee05fc 198 cid=2d17ccec98a6d4a0 capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck | prior: R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack complexity 90d3966814ee05fc 296 cid=6321caf30ea6eef6 now=296 was=284 p=src/main.cpp:1629 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity 92ac9caf38b8aab0 20 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack complexity 949f0a5b0ae79ca0 29 cid=b9354baf0556cc75 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. ack complexity 96fdcdff2f0ff0f7 28 cid=95538ee0cd9b8cae round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut | prior: 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. ack complexity 97a23cbeae7feb30 39 wave merge 2026-08-20 (harvestexec): collectFacts +12 LOC / +2 cx, from lane/resolver-precision. The executable part is ONE guard — 'if role == RefRole::Type: continue' — and 8 of the 12 added lines are the comment that makes it honest (the rest are the if, its braces and the continue; 'the other eleven lines' was this ack's own overstatement, corrected 2026-08-20), because a silent skip here is indistinguishable from an oversight. Two reasons are recorded in it: a member declaration 'Shared m_a;' already contributes a site through its HAS-A compose reference over the SAME bytes, so admitting the type mention too would count one thing a reader must read twice; and the coupling/cohesion thresholds this lens feeds were calibrated over the pre-Type reference stream (docs/EVALS.md section 9), so widening its input from a lane registered to change the USE-SITE index would invalidate a calibrated instrument as a side effect. That makes the growth a scoping decision with its own follow-up round, not debt: deleting the comment would shrink the number and destroy the only record of why the lens holds still. ack complexity 97e844e27c205f75 73 cid=2ba2aaf4fee7426c M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. ack complexity 983814f2b5912a90 17 cid=1ea6676dedf249db at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack -ack complexity 9ebbafddddd086b4 87 cid=8703e3bd00478ec4 F5 sibling: packConnect grows by the over_ceiling decision and its re-pricing — three statements beside the est_tokens it must agree with. Splitting them out would separate the label from the number it is about, which is the defect being fixed. | prior: H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. +ack complexity 9ebbafddddd086b4 97 cid=5538119bcafa2857 now=97 was=95 p=src/mcpverbs.h:3460 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: F5 sibling: packConnect grows by the over_ceiling decision and its re-pricing — three statements beside the est_tokens it must agree with. Splitting them out would separate the label from the number it is about, which is the defect being fixed. ack complexity a3a317502ee383bc 18 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack complexity a4a8c133e40fe0ac 28 cid=0dfc15f8d6edef3d H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. ack complexity a5728903bed77b57 45 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity a783c75feea9f253 35 cid=7345e4b8de2407c9 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit ack complexity a8b774025a21bdc6 112 cid=a577dfc85e2ff192 P17 slice+edit_check join MCP batch: runBatchSub gains two else-if arms (each calling the standalone builder, sliceText/editCheckText, so a batched answer cannot drift from the live one) plus three argument reads; dispatchMcpLine gains the post_check boolean read for the three edit verbs. Both are dispatch chains growing in kind. runBatchSub is NOT decomposed in this commit on purpose: lane L6 is changing this same function's sub-query GRAMMAR in this wave, and restructuring it here would guarantee a merge conflict over a concurrent lane's work — the decomposition is recorded as a follow-up, not skipped silently. ack complexity a9f76a08efdb3d50 21 cid=d951d606551bf2d0 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. +ack complexity b1e9f4168d9f11cc 25 cid=e55be8bafc0b0aaf now=25 was=23 p=src/verbs_navigate.h:2680 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity b8aadfa58812b307 19 cid=ef705bbe14951c78 now=19 was=0 p=src/receiverevidence.h:607 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity baa5f1e81f16d0f0 19 cid=24e92a420ed526db now=19 was=16 p=src/graph.h:4601 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity bb42e692522f57ff 30 cid=aa3e0713df809f6d capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity bcb3377087f2e034 66 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity c17db5239ed07cf7 63 cid=04f50efd091db7eb L10b finding 3: --recall --top-k=0 gets its own guard message (small, bounded verbosity growth in validateConfig's existing guard block) | prior: capture-audit 2026-09-04 lane L1 (flag-table universe), the seven gating rows of this lane's own five commits, each the documented cost of the guard/table it lands: parseArgs +4 cx is the --exclude= empty-value guard, one branch in the hand-written residue, the exact shape of its --and=/--not= siblings (M6); validateConfig +4 cx/+17 LOC is the bare --quality-ack pairing refusal plus the =REASON implication moved out of the parser arm (H10); validateModifierGuards +4 cx/+21 LOC is two pairing guards (--naming-locals refuse, --no-stable notice) in the helper every prior guard grew the same way (M16); kShapingVerbs 19->62 LOC is the ledger of every verb outside honorsPaging — 36 rows for 36 verbs, a table is its length, and test/shapingflagcheck.sh arm (F) is what stops it rotting (H3); main +11 LOC is the --json check moved ahead of the CLI edit bridge plus one call to refuseInertMainModifiers (H2/M16) — the two guards themselves and jsonUnsupportedVerb's allow-list walk live in their own helpers. Gates for all five written red first; battery green at this head. churn=self rows on jsonUnsupportedVerb/kViewFlags/writeAckRecords are this lane's own rewrites and are invisible in ref-pair mode by construction. -ack complexity c22ce1db6b79b087 22 cid=d71d453313b90a4d round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut | prior: H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. +ack complexity c22ce1db6b79b087 31 cid=428e1247a5ef36e0 now=31 was=27 p=src/mcpverbs.h:691 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack complexity c29edcedb6d64b02 157 cid=4d78247c50c7e7c8 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch | prior: lane V1 N3 (342493b, helper lifted a8e994d): the explicit-selector branch of the multi-root --situ loop — a selector sentence instead of the empty-diff zero ack complexity c4465160da273627 65 cid=a5da89d8d9da8562 L10 finding 9b: buildStructRow now copies layout.h's caveat kind(s) into a new AffStruct.why field, writeFieldAffinity emits why= on unmodeled rows — the complexity/verbosity growth is that copy+emit cost ack complexity c4f94cdc43457375 22 cid=040f24a59a23b677 P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) ack complexity c5726c667bc4ffa4 77 r2 pkg-dir mention lift: applyMentionBoost ccx 74->77 is the new (c) branch, measured +1.6pp strict@10 with a dedicated gate (mentioncheck vi); score.py any10/strict10 are called inside sum() comprehensions — dead-code detector scope limitation, verified used -ack complexity ca97a4b6bf07887b 201 cid=b2c9107cb2d8ac03 H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack complexity c79b36c44111aca3 30 cid=c0aaf33a6026048d now=30 was=0 p=src/ingest_binds.h:189 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity c94040903501f1fe 26 cid=c0513ed9cc8bd60f now=26 was=11 p=src/ingest_binds.h:267 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack complexity ca97a4b6bf07887b 279 cid=cfe6534d0f63b7c4 now=279 was=271 p=src/serialize.h:2626 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. +ack complexity cab2f93db3992faf 131 cid=34e51af7ebbf56db now=131 was=0 p=src/ingest_binds.h:4183 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity cb7342964b38db9c 33 cid=3af54969638510bc by=src/* member-variable round (card A3): usesText grows the Owner.field member-selector branch (resolveFieldSelector + bare-name refusal listing spellings), the registered contract of this round | prior: R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack complexity ce8ba13980fd838f 19 cid=16497434baf13239 now=19 was=0 p=src/ingest_binds.h:4104 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity d03215f48bec3886 17 cid=d622ffa6d33014f4 E1 (terminality round A, lane E): +1 branch: the redaction-marker refusal beside the NUL one (one ladder, one vocabulary) ack complexity d295ac7080c14dd5 18 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity d42c85b67bd0956f 52 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity d63db6944aa504a7 536 cid=70fc957435aa6310 lane for-widen (L-W): the file page, coverage= and the thin next= land in the three --for dispatch/emit bodies the 0.6.1 lanes touched this week — this lane's own churn, not thrash | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack complexity d721ea3d108a3ba8 24 cid=9bfd37570d23d571 now=24 was=15 p=src/serialize.h:5803 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity dbe6ed5269d328a4 130 cid=9c4a7dc830d23192 M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack complexity dd02b378ae6b5b75 30 cid=5a0958e5bca4aaf3 L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack complexity dd627540f10bba76 197 cid=4170efd4f9c90d2f by=src/* t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign | prior: round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes @@ -494,6 +512,7 @@ ack complexity e14feca13c7ae680 57 cid=29dde7810cd9e120 M4 (lane ca-L2): writeHa ack complexity e1b4964cea59171b 18 cid=8ba981522099e145 H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. ack complexity e514a69013d0934c 33 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack complexity e7f50422948f2c09 17 cid=6c7e771d42d81c16 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch +ack complexity e836c18ca4452097 19 cid=d0f46b9a11a3932c now=19 was=17 p=src/mcpverbs.h:475 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity ec2848a4801493a2 51 cid=ed0825d14daf74d4 H11 (lane ca-L2): validateModifierGuards grows one more modifier arm (--allow-dirty, refused alone) in the flat if-chain this function IS; every sibling arm is the same 6 lines and splitting one out would hide it from the guard roster | prior: capture-audit 2026-09-04 lane L1 (flag-table universe), the seven gating rows of this lane's own five commits, each the documented cost of the guard/table it lands: parseArgs +4 cx is the --exclude= empty-value guard, one branch in the hand-written residue, the exact shape of its --and=/--not= siblings (M6); validateConfig +4 cx/+17 LOC is the bare --quality-ack pairing refusal plus the =REASON implication moved out of the parser arm (H10); validateModifierGuards +4 cx/+21 LOC is two pairing guards (--naming-locals refuse, --no-stable notice) in the helper every prior guard grew the same way (M16); kShapingVerbs 19->62 LOC is the ledger of every verb outside honorsPaging — 36 rows for 36 verbs, a table is its length, and test/shapingflagcheck.sh arm (F) is what stops it rotting (H3); main +11 LOC is the --json check moved ahead of the CLI edit bridge plus one call to refuseInertMainModifiers (H2/M16) — the two guards themselves and jsonUnsupportedVerb's allow-list walk live in their own helpers. Gates for all five written red first; battery green at this head. churn=self rows on jsonUnsupportedVerb/kViewFlags/writeAckRecords are this lane's own rewrites and are invisible in ref-pair mode by construction. ack complexity ed05ee0357d016f1 49 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack complexity ed10de1685178c0e 21 cid=18c2d19fcd3157cf R1: +2 ccx on editplan::prepare and +1 on editpreview::run — ONE guarded early-return each (the redaction-marker gate moved to where the replaced bytes are known); both were already over the bar before this change and neither gained a nested branch @@ -502,7 +521,7 @@ ack complexity efe685bb50482275 58 cid=10ded7c311552cd1 L10 finding 9b: buildStr ack complexity f1aa10922e41f258 27 cid=6670a895f0520c6a E3 (terminality round A, lane E): run_editsuite.py: the --cost-arith subcommand (E3's arithmetic, per task) ack complexity f307304e87733112 17 cid=76db6c1cce778585 fix-round follow-on: the recall-ceiling MCP branch (+3 ccx on the dispatcher), the apostrophe word-boundary guards in firstQuotedLiteral (the refusal logic IS the fix), and re-touch churn on the flag table / RecallShape comment / installer timeout re-pin ack complexity f8d747d123b4143d 39 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. -ack complexity fce48dedc40bc8eb 16 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack complexity fce48dedc40bc8eb 18 cid=4b3785b8a81e3973 now=18 was=15 p=src/mcpverbs.h:3166 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity fd4cc5c6c0ad17d9 34 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack dead-code:new-symbol 033294a4fc57eac8 0 cid=08917f8cd6a0890c timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack dead-code:new-symbol 0a892722cd821466 0 cid=e801e16ce70d1cd0 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -513,6 +532,7 @@ ack dead-code:new-symbol 86cc58659800f0fb 0 cid=2c493bc89aeb6a5f timsort vendori ack dead-code:new-symbol aba70b100ec42e27 0 cid=2c493bc89aeb6a5f timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack dead-code:new-symbol e9ba36b269911b50 0 cid=e45729c79053d829 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack dead-code:preexisting f50c3faf097e1b50 0 cid=5b5fb108c58d5881 load_samples is called indirectly via the RunSpec/build_runs loader reference (loader(args.data, name)) in main() — a first-class function value, not a literal call site, so the name-based call graph cannot see the edge; verified by grep and by running run_arb.py --help/--determinism-check +ack duplication 005bb1ea0ec27f8e 105 now=105 was=0 p=src/flipimpact.h:434 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 0097b5800ff95c8e 304 now=304 was=0 prov=recon Chip-trio merge 2026-08-23, finding 1 of 4. The claude session parser (parse_claude_session_metrics) shares 304 normalized tokens with parse_opencode_ndjson_metrics, and the shape IS deliberately parallel: both walk a per-message JSONL stream, pick tool_use/tool-call blocks out of it, and split them into command_calls / ripwire evidence / native_read_calls through the SAME classify_native_read() rule so the three counts stay comparable within a harness. What is NOT shared is what the duplication metric cannot see: the two streams have different envelopes (claude nests under message.content with type=='assistant' gating; opencode is a flat NDJSON event stream with its own part kinds), different token/cost fields, and different notions of which record carries the model id. Factoring the walk into one generic reader would need a per-harness descriptor for every one of those, which is more coupling between two independently-versioned external formats than the ~40 shared lines cost — and a change to one CLI's stream shape would then be a change to the other harness's parser. The house rule this follows is the one already applied to _codex_metrics / _opencode_metrics / _claude_metrics, which are three sibling parsers by the same argument and were acked the same way. Revisit if a fourth harness lands: three parallel parsers is a pattern, four is a table. ack duplication 00b0c885f543c639 50 by=test/macroreparsefix now=50 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, member-macro re-parse fixtures (test/macroreparsefix): sumDraft (leak_plain.cpp) and tallyCargo (leak_anon.cpp, control/semi.cpp) are deliberately ordinary C loops around the member macros, and test/macroreparsecheck.sh pins them BY NAME as the functions a derailed parse loses, so their bytes are the measured input. Their token streams meet unrelated plain loops (abicheck KindCounts::sumWhere, vendored dynamic_map.hpp node_rank::lt/le, bench geometry perimeter, the cloneidiomfix and sliceflowsensfix fixtures) with no shared identifier or contract; rewording a fixture to dodge a token match would game the number. ack duplication 03945adfda18f84d 20 now=20 was=0 prov=recon cache-pack round, reviewed each: fixture functions deliberately mirror common loop shapes (token-matches to unrelated helpers are the fixture doing its job); needsLoopContext is a 3-clause tag predicate shape-matching other tiny predicates; mergeCachePack parallels mergeAtomsPack by documented same-shape contract @@ -531,6 +551,7 @@ ack duplication 0d8219d50310fcf9 44 by=test/macroreparsefix now=44 was=0 prov=re ack duplication 0fcefd71958f2838 44 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 1030d8274cf87965 58 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack duplication 1096a473dedb29cf 128 now=128 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff +ack duplication 10e00e8b12aad57c 38 now=38 was=0 p=src/namingconsistency.h:136 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 1104136ee19ceb5c 58 now=58 was=0 prov=recon octocode F3: matchNearestKindClause follows the established purpose-named optional-clause-builder idiom already used twice in this tree (mcprefusal.h::blankPayloadClause, selectorrefuse.h::selectorFaultClause) — the guard-empty/build-prefix/conditional-suffix shape recurs by that idiom's own design, not by accidental copy-paste; sharing a generic builder with AnchorList::clause (an unrelated --for routing internal) would couple two unrelated subsystems for a shape-only match ack duplication 110c4184a5bed17b 37 now=37 was=0 prov=recon incidental shape similarity of small JSON/string builders across unrelated protocols (#279 LSP wire helpers vs MCP/CLI/quality code); sharing a helper would couple the LSP wire format to them ack duplication 12a98f49622cc5c4 39 now=39 was=0 prov=recon main.cpp split 2026-08-29: NOT new duplication — the 39-token AnchorKey::operator< / model.h lessUnindexedExt near-miss pair existed verbatim at the base (operator< moved byte-identical from main.cpp to verbs_report.h); clone groups key on their member set, so the moved path minted a new group id for a pre-split pair. argvdiffcheck proves the bodies unchanged @@ -540,6 +561,7 @@ ack duplication 177c6a61c6c2c3a2 28 the three sidecar read seams keep one O_NOFO ack duplication 189c716a1f2298cc 42 now=42 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication 197e1f32ffe97006 42 by=src/* now=42 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 199088adf212a996 95 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. +ack duplication 199643b5605561aa 274 now=274 was=0 p=src/receiverevidence.h:491 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 19b0ff8a7ad07b3c 63 now=63 was=0 prov=recon F4: withHeaderField and withHeaderAttrAfter now share headerFieldDigits; the 63 tokens left are the four-line frame both need (copy the line, locate, edit, return). Folding those into one function with a mode flag would be worse than the clone. ack duplication 1a6cabebe02fa29f 44 now=44 was=0 prov=recon dynmapsimd harness oracles deliberately restate the scalar contract (independent test oracle); dynamic_map ctor churn is this feature's own edit window ack duplication 1b10462b15da9a73 64 now=64 was=0 p=src/infra/shquote.h:19 the shSingleQuote|show Type-3 pair (64 tokens, test/jsonwalk_unit.cpp's failure-message renderer) is the SAME clone the base tree already had at 0.87 with jsonesc.h::shSingleQuote; the body moved verbatim to the standard-library-only shquote.h so os.h can include it without a cross-layer forward declaration, which re-keys the group by path. Nothing new is duplicated; show() escapes a different character set for a test message and folding it into the shell quoter would name neither @@ -555,6 +577,7 @@ ack duplication 24891af819b15ead 23 now=23 was=0 p=test/fnliteralfix/arrows.ts:1 ack duplication 274d9cad9a1f18de 31 now=31 was=0 prov=recon wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD ack duplication 296b8816e4cc2ce6 40 now=40 was=0 prov=recon facet=switch-name-table P3.2 plan-lint mechanical footprint: printUsage grows with the new help block; glyphName clones accessshape::shapeName (an enum-to-string switch idiom, the exact false-positive class P2.1's clone-idiom demotion targets, not yet landed); gitBlameLineSha clones quality.h::gitBlameRangeHasWindowCommit's blame-porcelain invocation shape — a shared-invocation extraction was tried and reverted as net-worse (it only moved the cost onto that untouched function's own churn), see planlint.h's own comment on the decision ack duplication 2d090ba54aef3c26 589 now=589 was=0 prov=recon WAVE-2 close (2026-08-19), finding 2 of 3 triaged separately. duplication emitCochangeGroups | emitCochangePairs (589 tokens, src/main.cpp:4317). These two cochange emitters were already structurally parallel before the wave; W2-E's root-relative p= landing added the SAME four-line preamble to each (xxSingleRoot / xxRootPrefix / xxRootAttr from rw::sarif, plus the rootRelPathsLegend call and the root=-before-at= placement), which is what carried the pair over the clone bar. The shared parts that COULD be hoisted already were: rootRelPathsLegend/kRootRelPathsLegend are one definition serving all eighteen legends (the S B4 echo-site rule, see ack api-surface 48efeb1d). What remains per-site is three locals computed from each emitter's OWN cfg/ing bindings in its own scope; folding those into a helper would hand every emitter a struct it must unpack, at ~30 call sites, to save three lines each. Accepted as the cost of the uniform root= contract, NOT as a claim that the preamble is optimal: the ~30-site preamble echo is recorded as a wave-2 follow-up for a dedicated round, where it can be measured and gated instead of refactored blind at integration close. +ack duplication 3058a1783a1178be 29 now=29 was=0 p=src/graph.h:533 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 318844328dfb16c8 50 now=50 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 32420ebb102c4aaf 36 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 33c5d907ab904cbe 43 by=src/* now=43 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) @@ -570,6 +593,7 @@ ack duplication 3caf742bad9a9545 56 now=56 was=0 prov=recon --quality-panel: the ack duplication 3ccdcc678c7957e9 44 now=44 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 3ed186c9821ed68e 50 now=50 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 41d9fbec47832262 37 now=37 was=0 prov=recon cachefix fixture functions deliberately mirror common loop shapes; token-matches to unrelated src helpers are the fixture doing its job +ack duplication 481141c5d4757226 74 now=74 was=0 p=src/quality.h:1216 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 4892aee1410d832a 76 now=76 was=0 prov=recon capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept ack duplication 48e69eae0fc9e9db 34 now=34 was=0 p=src/tracelocus.h:962 token-shape duplication false positive from the F3 base-name hoist: immediateScope (afterLast then skipTemplateDisambiguator) shares no logic with traceDeliveredBytes (compactDeliveredBytes or a size fallback). The real clone (noteRead / immediateScope / baseNameOf) is gone. ack duplication 499d9f6f77638965 65 now=65 was=0 prov=recon Integration train 1b (PR #244 via train 2b): jsLitCtorName's pairings with accessshape shapeName, namingconsistency styleTag and planlint glyphName appear only on the merged tree; each side was acked on its own range (2b acked #244's Lit* helper rows) | prior: Integration train 2b (PR #244, TS/JS literal receivers): isJsTsLitRecv (a five-case RecvKind membership test), jsLitCtorName (a five-case RecvKind -> constructor-name switch) and isJsTsBuiltinCtor (a five-string membership test) are each the smallest spelling of one fact about the Lit* enumerators appended in the same PR. Their token runs match refRoleTag, styleTag, votingStyle, langUsesHashComment, elixirTypedAttribute and a clone-idiom fixture by SHAPE (a short enum switch, an OR of equality tests), not by shared logic: no helper exists they could call, and folding unrelated enums into one table would couple them. Acked at the train that first measured them on the merged tree; the three stay adjacent in model.h beside the enum they describe. @@ -592,8 +616,10 @@ ack duplication 5bfc60bf2eec4379 58 now=58 was=0 prov=recon readability-wave1 (c ack duplication 5c9174022e5a0734 44 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 5e4956ffb34ce360 36 now=36 was=0 prov=recon M1: a 36-token shape coincidence, not duplication. isMcpProtocolVersionSupported walks a version list and mcpVerbDeclaresLegend walks a field list; both are the C++ spelling of 'is X in this list', over different tables, different types and different call sites. Folding them into one generic helper would couple the protocol handshake to the legend family for no shared meaning. ack duplication 5f01b66132b916bd 44 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) +ack duplication 5fad9d85c2552890 32 now=32 was=0 p=src/receiverevidence.h:142 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 5fc8d4b1ed98eaf3 36 idiom-class clone: a NAMED one-line std::find predicate. What matches is the std::find idiom itself — 36 normalized tokens, zero shared domain identifiers with ncAnyOf/namesNode, different value types, different subsystems. Inlining it at its two call sites to dodge the row would be metric-gaming; the name is the documentation. ack duplication 6464ab2300b93b7a 32 now=32 was=0 prov=recon scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. +ack duplication 649a99661f221ead 18 now=18 was=0 p=test/receiverevidencefix/js/lib/application.js:48 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 64b9cdaf064825c4 18 now=18 was=0 prov=recon sendAll (mcpserver.h, network send-all-tolerating-short-writes) and pathguard::writeAll (file write-all-tolerating-short-writes) are the same RETRY-LOOP shape over different syscalls (send vs write/EINTR) and different domains (socket vs file descriptor) — the tree already acks this idiom repeatedly (e.g. the three os:: syscall passthroughs vs profilePmc.h): no shared helper exists to call, and folding network and file write-loops together would put a dispatch where a three-line loop is ack duplication 64c50349786b7508 26 now=26 was=0 prov=recon nested-closure span-attribution fix (jsnestedcheck): spanContains is sameSpan's sibling predicate (containment vs identity — 26 shared tokens are the four ts_node_*_byte reads, nothing mergeable); captureTagsFacts verbosity is the constraint comment on the containment stop + churn=self is this fix editing the tags pass; webpack nest>=4 individually-scoped rows 18%->98%, gate jsnestedcheck green ack duplication 6562d36ca101b8a5 35 now=35 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -603,6 +629,7 @@ ack duplication 689ab18d529fb67c 21 now=21 was=0 prov=recon P3.2 plan-lint model ack duplication 689e8ff37b877fd0 169 P3 duplication: editCheckCallSites vs quality::topLevelCalleeNameHashes, 169 tokens. Both are 'one pass over ing.references with continue-filters, then sort+unique' — an IDIOM collision, not a reusable helper: different predicates, different element types (a pair vs a uint64 name hash) and different questions. Extracting a shared filter-sort-unique-over-references template parameterised by a lambda would be less readable than either and is the abstraction-for-its-own-sake G2 warns about. Kept as two clear loops, deliberately. ack duplication 68b1751fdaff3631 19 now=19 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11: test/extentfix/leak.cpp and test/extentfix/control/leak.cpp are the extent detector's designed fixture PAIR (the control is the leak with semicolons added), so their free functions are byte-identical by construction; the pair only re-keyed because the leak's macro moved to lowercase (the member-macro re-parse repairs the ALL-CAPS spelling, so the detector needs a shape that still derails) and the leak copies are filed under IndiaError:: ack duplication 68b8ec360ff58496 38 a two-field DISCLOSE sink: the switch maps each reason to its own field, the shape every such sink shares +ack duplication 698b647d75b377de 73 now=73 was=0 p=src/lintrules.h:311 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 69bea33de722cde5 23 now=23 was=0 prov=recon Integration train 1b (lane/reader-fuzzers @ 8f6f86f5): the fuzz harness appendPod deliberately re-implements the on-disk writers (qsnapPut's byte append) so the harness shares no code with the reader it attacks ack duplication 6a595c85c47abd60 74 now=74 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction ack duplication 6a7680fbd8a5083d 20 by=src/* now=20 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) @@ -614,11 +641,13 @@ ack duplication 6edeb2565e612fbb 56 now=56 was=0 prov=recon DISCLOSE sinks: a re ack duplication 7195353aaff6ffd4 46 now=46 was=0 prov=recon incidental shape similarity of small JSON/string builders across unrelated protocols (#279 LSP wire helpers vs MCP/CLI/quality code); sharing a helper would couple the LSP wire format to them ack duplication 72d40216c74f7fec 26 incidental shape similarity of small JSON/string builders across unrelated protocols (#279 LSP wire helpers vs MCP/CLI/quality code); sharing a helper would couple the LSP wire format to them ack duplication 73b08512b41c61f2 101 E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. +ack duplication 73e06cf71f4ea8bc 38 now=38 was=0 p=src/ingest_binds.h:1263 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 7589afcf46d892d1 38 by=src/* now=38 was=0 prov=recon rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. ack duplication 75b5d63d5a992e21 44 now=44 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication 767f5200ffbdb095 54 now=54 was=0 prov=recon E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack duplication 77278c286d945470 22 now=22 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication 774b8194ffdecea7 59 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. +ack duplication 78dd7140b7c84cd3 20 now=20 was=0 p=src/cachelint.h:154 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 7981b72e3b2a8a8b 42 now=42 was=0 prov=recon mcpIsGitOnlyVerb (finding #8, 2026-08-15 harvest) is a 6-line linear-scan-and-compare over the SAME small kMcpGitOnlyVerbs table mcpGitOnlyNotBatchServed already loops beside it (mcp.h:127-144); query.h::isKnownLayerWord is the same trivial 'is w one of these known short strings' idiom over an UNRELATED domain (the graph-query layer vocabulary). Per this repo's own precedent (V6/isKnownVerb, ROUTING_LEDGER.md's V6 lane), forcing a shared cross-file generic membership helper across two unrelated small closed vocabularies is the wrong abstraction for a token-shape match, not a real duplication to collapse ack duplication 7ad6b911a1122824 71 now=71 was=0 prov=recon a probe TU deliberately mirrors layout declarations (#224 structlayout_probe.cpp) ack duplication 7ad7c707204ff7b3 23 now=23 was=0 prov=recon qsnapPut is constrained to types with unique object representations instead of asserting trivially-copyable, which leaves its body the same one-line byte append ByteW::u32 is; the two live in different translation units on purpose (quality.h cannot include the ingest cache) @@ -632,6 +661,7 @@ ack duplication 8a652c25d25d8e47 37 by=src/* now=37 was=0 prov=recon member-vari ack duplication 8b236ff8e22bac99 43 now=43 was=0 prov=recon dynmapsimd harness oracles deliberately restate the scalar contract (independent test oracle); dynamic_map ctor churn is this feature's own edit window ack duplication 8c1252540ce9a74e 56 now=56 was=0 prov=recon OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack duplication 8c49388791b303b8 56 now=56 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) +ack duplication 8cbe787ba8c6fce6 95 now=95 was=0 p=src/receiverevidence.h:363 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 8fff3a3ea6d42578 75 by=src/verbs_report.h now=75 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/verbs_report.h): extentSuspectCountsByFile and recall.h docFileMask are the one-pass-over-ing.symbols per-fileId tally idiom (lintrules.h dependencyCapableMask is a third), with different element types and questions: a uint32 count of flagged definitions per file vs a char mask of files holding Markdown symbols. Same class as ack 689e8ff37b877fd0: a filter-and-tally template parameterised by a lambda would read worse than either five-line loop. ack duplication 91876bb9e456fd42 30 now=30 was=0 prov=recon wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD ack duplication 9497791b092d0735 19 now=19 was=0 prov=recon M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. @@ -648,17 +678,21 @@ ack duplication 9dc9e2676d6a3d0c 311 now=311 was=0 prov=recon timsort vendoring: ack duplication a051e0695aee77da 65 now=65 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction ack duplication a15350d5f326584b 53 now=53 was=0 prov=recon dynmapsimd harness Arm C (sparseCsr x86 SSE2 round): refDot is the deliberate independent double-accumulated oracle for the csr kernels — structural echo of cloneSketchMatches' accumulate loop is coincidental, not a reuse site ack duplication a183672c2cfcd17b 39 by=src/* now=39 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) +ack duplication a32200f6101b4432 47 now=47 was=0 p=src/model.h:246 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication a405a9c684c97953 781 now=781 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication a42cd763fdab0077 101 a probe TU deliberately mirrors layout declarations (#224 structlayout_probe.cpp) ack duplication a5943aaae0184b8f 30 readability-wave1 (naming lens): ncAnyOf is a one-expression std::find wrapper, sortNotes a one-expression std::stable_sort wrapper — 30 normalized tokens of the SAME STL-adapter idiom over unrelated types (a char-class set test vs a note sort). There is no call ncAnyOf could make to sortNotes, and rewriting the std::find as a hand loop would only hide the detector. Kept as written. ack duplication a702e882c0354944 44 now=44 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication a7aef5cdfefbb6e0 89 now=89 was=0 prov=recon sibling row-emitter PAIR (writeUnindexedExtRows/writeLangRows) - exactly TWO members: the key a7aef5cdfefbb6e0 is fnv1a64('writeLangRows\nwriteUnindexedExtRows\n') and nothing else. WAVE-3 VERIFIER CORRECTION (P6-6): this reason previously named writeHealthRows as a third member, which is in NO clone group at all - that made the ack unverifiable against the ledger and made '2-instance' read as a three-way judgement it never was; and it cited 'the DOD doctrine' (G2), a DATA-LAYOUT guardrail that says nothing about a six-line emitter running once per report. The engineering call is unchanged and was independently attacked and NOT refuted. Its real authority is --quality-delta's own 'a wrong abstraction beats a low score' plus this repo's own fold threshold: src/mergescout.h:637 writeSymRows and src/main.cpp:8746 writeDropRows were folded because the attribute SCHEMA is identical and only the tag varies, while writeOversizeRows/writeDropRows sit deliberately UNFOLDED in the same block at a LARGER similarity=0.90 tokens=139, for exactly the discriminator that applies here - differing attribute sets. writeLangRows/writeUnindexedExtRows differ in the key attribute name (x= vs n=), the key SOURCE (a stored field ue.ext vs a derived rw::langTag) and the attribute arity (files= vs files=+symbols=), so they fall on the repo's own keep-separate side; every consolidation sketch is net longer, needs two callables per site, and drags the fixed-buffer snprintf into a lambda past test/fixedbufsweep.sh:259's per-site census (which this wave already had to re-pin 191->193). TREND, recorded rather than left silent: this six-emitter block now carries TWO unfolded clone pairs (139 preexisting + 89 new) and accretes sibling emitters by design. At a THIRD same-schema sibling, writeSymRows' three-caller precedent flips the answer - and it cannot creep there silently, because this ack is keyed at exactly tokens=89 over exactly these two members, so a third member is a new member set, a new group, and a new finding ack duplication a8ae4177bad34c1b 47 now=47 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction +ack duplication aaaee0ca15e7adfd 56 now=56 was=0 p=src/model.h:246 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication aada023eba533a38 35 now=35 was=0 prov=recon lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). ack duplication aafecfcf6097b13e 20 now=20 was=0 prov=recon sendAll (mcpserver.h, network send-all-tolerating-short-writes) and pathguard::writeAll (file write-all-tolerating-short-writes) are the same RETRY-LOOP shape over different syscalls (send vs write/EINTR) and different domains (socket vs file descriptor) — the tree already acks this idiom repeatedly (e.g. the three os:: syscall passthroughs vs profilePmc.h): no shared helper exists to call, and folding network and file write-loops together would put a dispatch where a three-line loop is +ack duplication ac8c6ea52654e17b 33 now=33 was=0 p=test/pargates.py:510 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication af2e342eda9648ed 50 wave merge 2026-08-20 (harvestexec): isTypeMentionNode, from lane/resolver-precision. Both duplication rows pair it with an EXISTING member of src/ingest.cpp's kind-table family (isBaseTypeNode, isJsonShapeModifier) — a static const char* const table with one commented row per language, scanned by strcmp. What the clone detector sees is that shared idiom, and it is right that it is shared: it is the file's house pattern for a node-kind predicate, and every future language row lands as one commented line rather than an edit to a condition. Neither collapse is available. Merging into isBaseTypeNode would be a CORRECTNESS change, not a cleanup: base-type admits eight kinds (qualified_identifier, generic_type, user_type...) and a type MENTION must admit only type_identifier — that narrowness is precisely the round-1 ACCEPT result, and test/typerefcheck.sh fails if it widens. Collapsing today's single-row table to a bare strcmp would remove the per-language comment slot the siblings use and would have to be undone by the first non-C++ grammar added. Acked as a deliberate instance of an established shape, not as a shortcut. ack duplication b16454c12b64a72e 52 now=52 was=0 prov=recon cachefix fixture functions deliberately mirror common loop shapes; token-matches to unrelated src helpers are the fixture doing its job ack duplication b3ba17bfe8eaffde 38 taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff +ack duplication b3f940da0f8dbdd7 65 now=65 was=0 p=src/quality.h:1216 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication b54c8d83f3ad053f 60 now=60 was=0 p=src/infra/namesplit.h:157 token-shape duplication false positive from the F3 base-name hoist: namesplit::afterLast (rfind + remove_prefix) shares no logic with doctorWhichVersionLine (popen, find newline, resize). The real clone (noteRead / immediateScope / baseNameOf) is gone. ack duplication b6e9e385c201d22f 45 now=45 was=0 prov=recon incidental shape similarity of small JSON/string builders across unrelated protocols (#279 LSP wire helpers vs MCP/CLI/quality code); sharing a helper would couple the LSP wire format to them ack duplication b7f09332215eedbc 70 now=70 was=0 prov=recon W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d @@ -678,6 +712,7 @@ ack duplication c0e6e599da682c8a 36 now=36 was=0 prov=recon idiom-class clone: a ack duplication c0f9c47a32e956c7 71 now=71 was=0 prov=recon a probe TU deliberately mirrors layout declarations (#224 structlayout_probe.cpp) ack duplication c4adad88b18202d6 50 now=50 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication c622254cf9e13d87 104 now=104 was=0 prov=recon cache-pack round, reviewed each: fixture functions deliberately mirror common loop shapes (token-matches to unrelated helpers are the fixture doing its job); needsLoopContext is a 3-clause tag predicate shape-matching other tiny predicates; mergeCachePack parallels mergeAtomsPack by documented same-shape contract +ack duplication c63561350b4b9a0a 38 now=38 was=0 p=test/receiverevidencefix/js/lib/search.js:3 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication c665833b2734f5d0 28 now=28 was=0 prov=recon two --skipped parse-time classes, each a one-call wrapper over collectSkipClass; each conditional legend clause rides only with its own rows ack duplication c72c4118f59d3ca3 34 now=34 was=0 prov=recon D3 Windows port (lane/win32-d3-onto-os): four short token clones between os.h's Windows half and unrelated code, each a generic idiom rather than a shared rule — a one-line std::ranges::any_of membership lambda (win32TableProduces | cli.h shipsViewFlag, 34 tokens), a case-folding endsWith (oswin::endsWithAsciiCaseless | darkflags endsWithView, 45 tokens; os_win32_logic.h is a leaf infra header with no project includes, so it cannot call darkflags), an RAII reset (os_win32.cpp Unique::reset | pathguard OwnedFd::reset, 26 tokens; a HANDLE/SOCKET/HLOCAL owner cannot be an fd owner), and a three-term suffix OR (isWslLauncher | two unrelated predicates, 26 tokens). The larger clones (table scans, the PATHEXT reader) were refactored away instead of acked. ack duplication c7ac66bb47a14f56 155 now=155 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction @@ -708,6 +743,7 @@ ack duplication dc9bb84f09aa1b94 50 now=50 was=0 prov=recon DISCLOSE sinks: a re ack duplication ddce9efe392269e6 61 now=61 was=0 prov=recon 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. ack duplication de772ce9497e6cc2 425 now=425 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication dec50094e5aef506 74 now=74 was=0 prov=recon OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack duplication ded26f9f9c045bfc 74 now=74 was=0 p=src/receiverevidence.h:50 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication df5a70dc16c85001 33 now=33 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication e0406bb747448d85 154 now=154 was=0 prov=recon ingest() decomposition: emitBindingAliases/emitRouteDefs are the FFI-alias and route-def sort ladders moved verbatim out of ingest()'s body into named phase helpers (ingest_model.h); as inline statements the clone scanner could not see them, as small functions their comparator shape token-matches sortLintRows - different element types and orderings, no shareable contract, zero behavior change (argvdiff byte-identical) ack duplication e2d22ac45d11d555 19 now=19 was=0 prov=recon review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. @@ -721,6 +757,8 @@ ack duplication e73a3cff0eab73b6 44 now=44 was=0 prov=recon DISCLOSE sinks: a re ack duplication e75742478839369d 31 now=31 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication e762f1286e66d100 38 now=38 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) ack duplication e79604c61e955de9 130 now=130 was=0 prov=recon facet=switch-name-table a distinct display vocabulary over the same enum: #279's LSP hover words (function/variable/interface), not the terse XML tag table symTag +ack duplication e7b8b8f3fb63b7c5 65 now=65 was=0 p=src/quality.h:1216 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked +ack duplication e828c02eebfa38f2 20 now=20 was=0 p=src/model.h:663 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication e91c5e004c251a91 24 now=24 was=0 prov=recon module-constant round (2026-08-12, test/moduleconstcheck.sh): the four short-horizon-churn rows are the documented extraction-bump protocol itself — kParserVer and its quality.h mirror MUST move in the same diff (qextractionkeycheck), dropConstantCapture is the policy function this round exists to change, and cudaMemorySpaceQualifierOf's edit is the dedup the quality gate itself demanded (169-token clone dissolved into childTokenAmong). The 24-token ncBoolTypeName|cudaMemorySpaceQualifierOf pair is a cross-domain wrapper-shape coincidence (naming-lens vocab membership vs tree-sitter child scan over disjoint token sets in different files); merging them would be the wrong abstraction the delta header warns against. ack duplication e94fce0d5caa811d 66 now=66 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication ea4ae03ca699bce4 35 now=35 was=0 prov=recon lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). @@ -785,6 +823,7 @@ ack new-clone-of-reused-helper 6a595c85c47abd60 5 now=5 was=0 prov=recon facet=s ack new-clone-of-reused-helper 6a7680fbd8a5083d 4 by=src/* now=4 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack new-clone-of-reused-helper 6df9a7992177fa30 4 by=src/extentsuspect.h now=4 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. ack new-clone-of-reused-helper 73b08512b41c61f2 4 E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. +ack new-clone-of-reused-helper 73e06cf71f4ea8bc 5 now=5 was=0 p=src/ingest_binds.h:1263 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack new-clone-of-reused-helper 7981b72e3b2a8a8b 3 now=3 was=0 prov=recon mcpIsGitOnlyVerb (finding #8, 2026-08-15 harvest) is a 6-line linear-scan-and-compare over the SAME small kMcpGitOnlyVerbs table mcpGitOnlyNotBatchServed already loops beside it (mcp.h:127-144); query.h::isKnownLayerWord is the same trivial 'is w one of these known short strings' idiom over an UNRELATED domain (the graph-query layer vocabulary). Per this repo's own precedent (V6/isKnownVerb, ROUTING_LEDGER.md's V6 lane), forcing a shared cross-file generic membership helper across two unrelated small closed vocabularies is the wrong abstraction for a token-shape match, not a real duplication to collapse ack new-clone-of-reused-helper 7ad7c707204ff7b3 14 now=14 was=0 prov=recon qsnapPut is constrained to types with unique object representations instead of asserting trivially-copyable, which leaves its body the same one-line byte append ByteW::u32 is; the two live in different translation units on purpose (quality.h cannot include the ingest cache) ack new-clone-of-reused-helper 845088dc7642af45 4 now=4 was=0 prov=recon V3 harvest 2026-08-15: two MCP gate harnesses. Every MCP gate in this suite is deliberately STANDALONE — the house rule in their own headers is 'does NOT edit regression.sh or any other existing test file', so a gate carries its own 3-4 line JSON-RPC transport wrapper (mcp_call in mcphandlecheck, mcpCall in mcpeditpresencecheck, the curl wrapper in mcpremotecheck). test/mcptoolprunecheck.sh needs BOTH transports (HTTP for the pinned-root arms A-E/G, stdio for arm F, and the pair is the point: pinning is exactly what makes the omission provable), so its http_call and stdio_call land as the 9th and 10th members of two families that already exist. Extracting a shared test/lib harness would couple every MCP gate to one file and is a suite-wide refactor, not this lane's; sharing one of the two existing spellings instead would make this gate fail whenever an unrelated gate edits its own helper. No production code involved @@ -800,6 +839,7 @@ ack new-clone-of-reused-helper b96b091322ca90c2 3 by=src/extentsuspect.h now=3 w ack new-clone-of-reused-helper bea5a095bdc44cd5 5 now=5 was=0 prov=recon Integration train 2b: the gate-local one-line helpers run(){ perl -e 'alarm 30; exec @ARGV' "$BIN" "$@" 2>/dev/null; }, cnt() and defs() in test/cpptmplscopecheck.sh (#256) beside test/cppqualcheck.sh and test/callformcheck.sh (#243) are the self-contained-gate idiom this repo spells inline in each gate; test/lib has no shared runner or attribute reader to source, and a gate is meant to read and run on its own. The clone count moved from 3 to 5 only because both lanes' gates now share one tree; #256 acked the same idiom at 3. ack new-clone-of-reused-helper c085689a09c879ee 3 now=3 was=0 prov=recon 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. ack new-clone-of-reused-helper c0e6e599da682c8a 3 now=3 was=0 prov=recon idiom-class clone: a NAMED one-line std::find predicate. What matches is the std::find idiom itself — 36 normalized tokens, zero shared domain identifiers with ncAnyOf/namesNode, different value types, different subsystems. Inlining it at its two call sites to dodge the row would be metric-gaming; the name is the documentation. +ack new-clone-of-reused-helper c63561350b4b9a0a 3 now=3 was=0 p=test/receiverevidencefix/js/lib/search.js:3 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack new-clone-of-reused-helper c7ac66bb47a14f56 3 now=3 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction ack new-clone-of-reused-helper c8b32435256b863e 4 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack new-clone-of-reused-helper ca79cc414dda8197 5 now=5 was=0 prov=recon Integration train 2b (PR #244, TS/JS literal receivers): isJsTsLitRecv (a five-case RecvKind membership test), jsLitCtorName (a five-case RecvKind -> constructor-name switch) and isJsTsBuiltinCtor (a five-string membership test) are each the smallest spelling of one fact about the Lit* enumerators appended in the same PR. Their token runs match refRoleTag, styleTag, votingStyle, langUsesHashComment, elixirTypedAttribute and a clone-idiom fixture by SHAPE (a short enum switch, an OR of equality tests), not by shared logic: no helper exists they could call, and folding unrelated enums into one table would couple them. Acked at the train that first measured them on the merged tree; the three stay adjacent in model.h beside the enum they describe. @@ -813,6 +853,7 @@ ack new-clone-of-reused-helper dec50094e5aef506 3 now=3 was=0 prov=recon OPTREMA ack new-clone-of-reused-helper e5435e09a9b2e2be 3 now=3 was=0 prov=recon the quality baseline read seam takes its DISCLOSE sink like the notes and arch seams; the three seams keep one O_NOFOLLOW shape by design ack new-clone-of-reused-helper e6ae1365cb6c154a 3 V3 harvest 2026-08-15: two MCP gate harnesses. Every MCP gate in this suite is deliberately STANDALONE — the house rule in their own headers is 'does NOT edit regression.sh or any other existing test file', so a gate carries its own 3-4 line JSON-RPC transport wrapper (mcp_call in mcphandlecheck, mcpCall in mcpeditpresencecheck, the curl wrapper in mcpremotecheck). test/mcptoolprunecheck.sh needs BOTH transports (HTTP for the pinned-root arms A-E/G, stdio for arm F, and the pair is the point: pinning is exactly what makes the omission provable), so its http_call and stdio_call land as the 9th and 10th members of two families that already exist. Extracting a shared test/lib harness would couple every MCP gate to one file and is a suite-wide refactor, not this lane's; sharing one of the two existing spellings instead would make this gate fail whenever an unrelated gate edits its own helper. No production code involved ack new-clone-of-reused-helper e79604c61e955de9 42 now=42 was=0 prov=recon facet=switch-name-table a distinct display vocabulary over the same enum: #279's LSP hover words (function/variable/interface), not the terse XML tag table symTag +ack new-clone-of-reused-helper e828c02eebfa38f2 4 now=4 was=0 p=src/model.h:663 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack new-clone-of-reused-helper ef69b96712422a31 3 by=src/extentsuspect.h now=3 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. ack new-clone-of-reused-helper f10ce50bdc680d80 6 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. ack new-clone-of-reused-helper f5a96185fd557a83 3 now=3 was=0 prov=recon R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. @@ -820,6 +861,7 @@ ack new-clone-of-reused-helper ff14ff2b821304c8 4 by=src/* slice honesty round ( ack new-clone-of-reused-helper ffbb89e07f18c870 42 now=42 was=0 prov=recon per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction ack params 00864cc4801c708a 6 cid=88b8c4067d691df7 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 060a064b6ffa7775 9 cid=f210de8290422917 now=9 was=8 p=src/quality.h:7477 lane/ack-provenance-backfill: the legacy-ack backfill (clone kinds reconstructed from the current tree, marked prov=recon), its refresh/unverified freshness rule, and qackconcurrencycheck arm 9 — plus this lane's own minor footprint on the ack reader/writer it extends +ack params 08c6875a741930ba 9 cid=38a064a2fb06dae6 now=9 was=8 p=src/verbs_for.h:2103 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 08d796d1b006560c 6 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params 0a3d16d6f3139408 10 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack params 0d7b8741392284d1 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. @@ -830,9 +872,11 @@ ack params 19e15f944de795a8 6 cid=e1412db291c8eaa4 timsort vendoring: every row ack params 1c11c9480374c3a4 7 cid=5ca90f74bad127e0 owner 2026-09-19: MCP for twin discloses stubbed sections; readable || kept over a gate-shaped |; forTaskText split is a separate refactor | prior: lane for-widen (L-W): the file page, coverage= and the thin next= land in the three --for dispatch/emit bodies the 0.6.1 lanes touched this week — this lane's own churn, not thrash ack params 28c51bbd0032f9ec 9 cid=bdd33965f128de92 bench/capsweep write_screen renders the nine screen.tsv columns capsweepcheck pins by name; a record object would move the schema the gate reads ack params 3478654139c90f0f 7 cid=8524565c31be122d review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. -ack params 3561d0281d324276 13 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path +ack params 3561d0281d324276 17 cid=6013544ad9e00162 now=17 was=15 p=src/serialize.h:6226 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path +ack params 382f69e9fab00162 7 cid=3f6e5cc8c76d7cb6 now=7 was=0 p=src/graph.h:5207 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. +ack params 45bb9f7c5bc37916 12 cid=c8ed741b38b4c0a9 now=12 was=10 p=src/serialize.h:6655 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 4b788e6f0a75bc80 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack params 4d421276056367a9 6 cid=5a1a2f3caf33a19e timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -843,9 +887,11 @@ ack params 5710beada2095a34 9 cid=3371683faa811117 preloaded-corpus hoist: lexic ack params 59f050855874875f 6 cid=79ec141006215e25 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack params 5ec38fbd414fa4d4 6 cid=35883c203ba33e28 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh ack params 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params 6e58b0a307757079 23 cid=912117812c5cc12d by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. +ack params 6e58b0a307757079 27 cid=cd68c661a9c12758 by=src/* now=27 was=26 p=src/serialize.h:8704 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. +ack params 7b4ba1b688c902ab 9 cid=d53ccc7632e3889a now=9 was=8 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate +ack params 7c8bcef86c83b1aa 10 cid=280a01aa1f0483f0 now=10 was=9 p=src/verbs_for.h:1867 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 80a2f3a7c92fbed4 6 cid=b272c333d7d8f297 Narrower's inline members are measured under its constructor; the growth is the reviewed using-declaration re-export and Rule 2c member-field veto rules (lane/using-base-reexport, #280) ack params 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack params 851e83b4505f10f6 12 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. @@ -858,8 +904,10 @@ ack params a6718fdf5bbb9f01 6 cid=0a16bab15ec4d994 timsort vendoring: every row ack params a8b774025a21bdc6 6 cid=8c396521251254f3 M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. ack params ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params c02fa9205afe3baf 7 pack-task budget round (verifier K1+K2, 2026-08-19): +4 ccx / +14 LOC in packTaskBundleText is the section REORDER (bodies allocated last, after the four fine-grained prefix sections) plus the one-shot reflow lap's ranking branch; the three list-section top-ups were factored into reflowListSection rather than inlined, which is why the delta is this small. selectMonotoneBodySubset +1 ccx / +8 LOC is the one early return that admits the top-ranked candidate at every pool. churn=self on both is this change's own edit window. Measured payoff on this repo, --pack-task=rank the call graph: callers 13/20 to 20/20 and fill 52.5% to 59.8% at the default 6000-token budget, 43.5% to 90.6% at 8000; the task-named body now survives every budget increase. Both properties gated red-first in test/packtaskmonotoncheck.sh -ack params ca97a4b6bf07887b 25 cid=72922b04b834af89 by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. +ack params c4d50b7393d16274 8 cid=62d07177d287ad05 now=8 was=7 p=src/verbs_navigate.h:74 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack params ca97a4b6bf07887b 29 cid=cfe6534d0f63b7c4 by=src/* now=29 was=28 p=src/serialize.h:2626 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params cc1e357ae3abf4b0 6 cid=27af8baf185c5ada main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical +ack params cfa725743786fd0c 11 cid=43654511ac4641e4 now=11 was=10 p=src/verbs_for.h:2174 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params d9c7b2164cb0b8c5 6 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. @@ -869,6 +917,7 @@ ack params ed4b3f43f7f19909 9 T3 disclosure-gap fix 2026-08-22: verbosity/churn ack params eea83c3db0f03d69 19 cid=012e11d249150604 by=src/* A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack params f5c01847f9a77e62 6 cid=68a5c5ed63c1ce7b timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params f6dedd851e4b2a25 10 cid=8b170f73ca092237 or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS +ack params f843cd4aaded1a41 7 cid=23a2ed80b7310ad0 now=7 was=0 p=src/ingest_binds.h:4141 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack short-horizon-churn 0024ff89023ae1f8 5 S2c classifier-gap round: churn=self on meter_init/meter_classify_bash/meter_classify_other is this round's own edit window on hooks/ripwire-nudge.sh, which the S2b sweep round, the fixture-isolation round and this one all landed in on the same day — the horizon is counting one wave of meter work, not an unstable symbol. No behaviour is defended by the ack: every classifier verdict is pinned line-by-line by test/hookcheck.sh section (12b) arms C4-C11, each verified RED against this file at origin/main before the fix existed, and the arm contract by A1-A4. The verbosity row on meter_classify_bash (59 to 67 LOC, minor, unacked) is the resolution order becoming explicit — head, walk, scan, shell-misc — and is left visible on purpose ack short-horizon-churn 00e484ac1f953c3d 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack short-horizon-churn 0130d0ed5ed1f4cd 2 cid=ced9892f114d85a0 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. @@ -1527,10 +1576,12 @@ ack verbosity 218041bccb277b92 104 cid=69bc85c464d51fa1 lane B1 cap disclosure: ack verbosity 225d0a144176c808 115 cid=fdf6f7041c20fcbe OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack verbosity 24637f7c69207cb9 1135 cid=b7513601dadd2b3e t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign ack verbosity 28449092af4cecb7 74 cid=ca3960652d2d85b9 by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). +ack verbosity 2878bcd6447c8e5c 87 cid=1e8475e13c08c82f now=87 was=71 p=src/graph.h:2998 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 28bbae8cfa29c022 86 cid=696dc4894e340595 by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). | prior: impact tested-partition (card A6): the MCP impact twin gains the radius_tested/radius_untested root counts and the per-row tested= mark, mirroring the CLI so mcpclidiffcheck stays byte-parity — +10 LOC of emission, no new branch ack verbosity 2a2f2487082cc6d8 153 cid=ba772c32f101b4bb or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS | prior: mechanical flag-addition footprint of --scope: one help entry, one composition guard, one kViewFlags row and one Config member. printUsage/validateConfig grow by construction on every flag this tool adds. ack verbosity 2abd26fdd3d1e6df 215 cid=80501bdf3db6fba6 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. | prior: P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) ack verbosity 2b0d39faec60fc08 91 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack verbosity 2bea60141008e4d0 80 cid=0f6117d2b7b8058c now=80 was=0 p=src/receiverevidence.h:261 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 31105421c3ce88ae 249 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack verbosity 3298be65bf6058ef 1265 cid=7327ba02472bb8c7 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack verbosity 3561d0281d324276 277 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path @@ -1546,6 +1597,8 @@ ack verbosity 3d87404c1cdf50ec 180 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the l ack verbosity 3e0a074094789d60 79 cid=3256a8c50529082d Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. ack verbosity 44b41056b999b501 111 D2 audit-regression fixes: renderWholeFiles/chooseExpandServe gain a shaping param each (compress + pack-budget composition, both single-caller, callers updated in the same edit); anchor-row loop and disclosure comments add the LOC/ccx; churn is this lane's own self-churn on the four symbols it edited ack verbosity 45b52ada32f63c11 565 cid=877f2f471ee33bc4 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack +ack verbosity 45bb9f7c5bc37916 108 cid=c8ed741b38b4c0a9 now=108 was=101 p=src/serialize.h:6655 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack verbosity 48768a385100895f 99 cid=5cbec57476270c83 now=99 was=97 p=src/verbs_for.h:3595 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 49ea9954a4e77a06 384 cid=638312475b7a6935 round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut | prior: capture-audit 2026-09-04 wave-1 close, lane L3 H4 (lane-L3.md §quality): emitGrepReport +15 LOC is the window's legend clause and the auxPage line the H4 paging fix requires; L3 measured it as 'is the fix', trimmed once in ab3ae08 ack verbosity 4a7106e488a2aa80 68 cid=da287af067736058 M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack verbosity 4b309450f25c2b44 1731 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean @@ -1566,17 +1619,21 @@ ack verbosity 6031be13b40a1b6f 205 cid=58496e25cbf67431 round ec5e3c3..HEAD, the ack verbosity 6302e2e27e23bcde 64 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack verbosity 639de1c3670999f9 185 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack verbosity 685ade09a168535e 96 cid=124bb29c88050ae1 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean -ack verbosity 6979fa794f2a5154 423 cid=9132efbcc6dfcbb7 now=423 was=420 p=src/ingest_sidecap.h:1706 fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam | prior: ruby-attr class-DSL lane (review round; kParserVer 119->120): duplication/new-clone elixirArguments|elixirTarget is token-shape coincidence only - elixirTarget is the shared fieldIdentifierText primitive plus a null guard, elixirArguments reads a different field with no identifier gate; no shared logic to lift. captureTagsFacts +1cx/+4verb is the two-line gated call site of the inline-visibility lift. +ack verbosity 6979fa794f2a5154 466 cid=027d9c38f3666ebb now=466 was=464 p=src/ingest_sidecap.h:1732 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam ack verbosity 6a38d70787d09e64 74 cid=f6a0c09dfc153ece Kotlin port (PR #126): the leading whitespace sample is walked by codepoint (jsonesc::utf8SeqLen) so invalid UTF-8 folds into errNodes/errBytes, deduplicated against the ERROR spans tree-sitter recovery already counted; without it a garbage byte run swallowed by recovery reads as a healthy file in --skipped. ack verbosity 6acbcaa854eada23 289 cid=e5c009ae9a0d8393 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. +ack verbosity 6e58b0a307757079 227 cid=cd68c661a9c12758 now=227 was=222 p=src/serialize.h:8704 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 7004309695fb79f1 265 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. ack verbosity 7051b3950aaf4c14 82 cid=7919a3d486bba416 lane B1 cap disclosure: doc_mentions_capped= is contracted to be PROVABLE, never guessed, and that proof is the +6/+16. It is the in-loop test that a doc below its anchor's lift target was the one a cap turned away, plus docLiftWasRefused for the other half of the total cap, which ends the OUTER loop over anchors already consulted. A one-line flag would instead say 'there might be more', which is the fabricated fact in the other direction. +ack verbosity 71459175b4b3d120 122 cid=31f157ab1a674d7b now=122 was=121 p=src/ingest_binds.h:4410 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 719e8441be665024 959 cid=e508c8076e15f8e3 lane win-build-green (review F1/F2/F4): ci.yml's jobs key grows because the windows job stopped asserting exit codes and started asserting answers — a --version SHAPE check (semver + emit= + built_from=), fixture-map floors that separate a real crawl from the 951-byte empty one a Windows path bug produces, ctest --no-tests=error, and the cl leg's own expectation (configure must succeed, build must fail, no D8xxx flag regression hiding behind the seam, every erroring file ours, first error one of the extension codes) built with -k 0 so its log is the inventory. Every added line is an assertion or the reason for one; shrinking this key means deleting the checks the review asked for. Same shape as the release.yml jobs acks above. ack verbosity 73ac37ff2580b0b0 86 cid=b07db8431c1b7c18 R1 identity round (harvest Wave-2 R1, 2026-08-24). Five gating rows, ONE change: finding identity now survives a rename or a move. (1) runQualityDelta complexity 201->205 and verbosity 419->462: the +43 lines are overwhelmingly the LEGEND this repo requires — the identity mechanism is a claim about what an ack means, so the report has to say what it rested on (both mechanisms, both truncation caps, the two per-route counters), and legendcoveragecheck arm (A) requires every new attribute be named there. The code added to this dispatcher is three lines: the ledger is now taken from the already-healed basis instead of re-read (re-reading it here would undo the healing), one countAckRescues call, and one call to quality::identityDisclosure — which is itself an EXTRACTION made in this lane precisely because --quality-delta flagged the inline version. The residual +4 complexity is the cid ternary at the ack-write site. Decomposing runQualityDelta is its own round, recorded as such by the W2-E ack above and unchanged by this one. (2) resolveDeltaBasis verbosity 73->86 + churn=self: the two healIdentity calls (ref-pair and working-tree arms) and the comment explaining why the healing must precede computeDelta — heal the baseline after the delta is taken and the delta has already been computed against the stale identity. churn=self on it and on mergescout::injectFileLevelFallback is this one edit window, not a history signal. (3) injectFileLevelFallback: its hand-rolled fopen/fseek/fread/fclose block was retired onto docparse::detail::readWholeFile, the canonical whole-file read, after this lane extracted quality.h's copy and the reuse-decline kind reported the pair as a 320-token clone. Re-read before the swap: short read or ftell failure clears the buffer and the file contributes nothing, byte-identical to what it replaced. Gate: test/identitycheck.sh, 19 arms, 9 RED on the pre-lane binary. ack verbosity 763ff87870373241 84 cid=0f3e66f03f7be1f1 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 767211b0b7743f79 86 cid=f8439fcff51f596b capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 775b773b1a3d2349 780 cid=6b4320dfd43b49dd the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack verbosity 7a04eee0ff6ec2d7 87 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean +ack verbosity 7ab4bd7047f034ec 576 cid=df13b023c4001487 now=576 was=0 p=src/receiverevidence.h:114 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows +ack verbosity 7b4ba1b688c902ab 75 cid=d53ccc7632e3889a now=75 was=62 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 7c0e356e60b323ba 70 cid=4451e903bc990da2 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack verbosity 7e26e5533d486c7a 265 cid=aa596d9bb05d621d cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. | prior: round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes ack verbosity 7ed8ad2c213537a4 375 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). @@ -1585,7 +1642,7 @@ ack verbosity 80a2f3a7c92fbed4 590 cid=b272c333d7d8f297 by=src/* Narrower's inli ack verbosity 81fbe59b4a35659b 171 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 851e83b4505f10f6 129 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. ack verbosity 86db2ff4e22cae54 70 cid=b9c9829a2aac834e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack verbosity 89b3fd14c192c899 1494 cid=bf9296f0f9d8d2a3 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: wave merge 2026-08-20 (harvestexec): TWO seams in buildGraph, both from this wave, measured together at the merged head (cx 753->764, LOC 1327->1360). (a) The RefRole::Type namespace gate, from lane/resolver-precision: +8 cx / +23 LOC, ONE inserted block - a stable in-place filter over the assembled candidate set (12 lines of code) plus the 9-line derivation comment above it, plus a 3-line re-route of the isClassLike lambda through the same predicate. Not extracted to a helper on purpose, and the reason is the gate's own subject matter - on THIS loop the predicate is a provable no-op (the loop admits only role=Call, un-narrowed by resolve.h doctrine, and role=Macro, which retagMacroCallReferences already proves uniquely macro), so the block exists to be the ONE seam a future round edits, and hiding it behind a call would move the lines without removing them while making that edit invisible. test/nsfiltercheck.sh arm 2 is the executable form of the no-op claim and fails if the narrowing ever starts biting here. The measurable effect lives in contextratio.h's all-roles resolution, and the role that reaches it is RefRole::Extends, NOT RefRole::Type - collectFacts continues on Type 28 lines before it calls resolveCandidates, so a Type reference can never arrive there, and what keeps a type mention from spraying across same-named functions is that continue, not this predicate. What the predicate keeps from spraying is a BASE CLAUSE: on test/nsfilterfix, 'class Derived : public Handler' binds to both 'class Handler' and the free 'int Handler( int )' without it (ents 1->2, amb 0->1). Corrected 2026-08-20 by adversarial verification (V-5): the two source comments this ack leaned on named the wrong role, and test/nsfiltercheck.sh arm 5 now pins the real effect on the real role - the gate is red under full removal of the narrowing, which it was not before. Acked separately below. (b) The S5-E compose lang guard, from claude/kind-kepler-0c9b90: +3 cx / +10 LOC, one langCompatible branch plus its rationale comment, so HAS-A resolution applies the same language gate as every other admission site; before it a C++ member bound to a Python/TS same-named class and the duplicate defeated the (ownerSym,fieldName) dedup. Recorded red-first by test/composelangcheck.sh (mixed-lang fixture test/composelangfix, C<->C++ bridge control kept green). Cost is bounded and one-time for both: buildGraph does not grow again when a role or a language is added, only when a seam is. +ack verbosity 89b3fd14c192c899 1711 cid=9fe16468f6ebb08e by=src/* now=1711 was=1458 p=src/graph.h:5400 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack verbosity 8a92173ded649e17 158 cid=d49e411bf180293e by=src/* P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack verbosity 8c07caf47e2f33a4 134 cid=0002f98cf93b4127 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. ack verbosity 8d58de9bb922f582 119 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean @@ -1618,13 +1675,16 @@ ack verbosity c4465160da273627 184 cid=a5da89d8d9da8562 L10 finding 9b: buildStr ack verbosity c5726c667bc4ffa4 172 cid=524bd36f6a257651 lane B1 cap disclosure: +17 lines of census and the argument that it is free. The three caps are read off the loops WITHOUT moving a break point — the stop reason is now read from the surviving loop index instead of a hoisted guard — and the comments pinning that equivalence are what stops the next reader simplifying the disclosure back into a wrong number. Complexity came back under its own baseline in this lane's refactor commit; only the line count is left. ack verbosity c5f22701d0c88cb6 80 cid=0dd38dd533f8c01b capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity c769c8e4d28544b7 87 cid=3815c5b4e0b5991d L10b finding 1: doc_mentions=/mention_anchored= root attrs on --for (small, bounded conditional growth) +ack verbosity c79b36c44111aca3 75 cid=c0aaf33a6026048d now=75 was=0 p=src/ingest_binds.h:189 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity c840f9a6ec2e6c6b 73 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack verbosity c8c7bb0104aa16b8 76 cid=d54042b64ff7b78e H11 (lane ca-L2): writeBaseline gains a DEFAULTED absorbedGating parameter so a dirty pin's absorbed count reaches the sidecar; --edit-check reports incompatible=0 (both existing callers still bind) | prior: W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d ack verbosity ca97a4b6bf07887b 653 cid=b2c9107cb2d8ac03 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack verbosity cab2f93db3992faf 224 cid=34e51af7ebbf56db now=224 was=0 p=src/ingest_binds.h:4183 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity cb7342964b38db9c 112 cid=3af54969638510bc by=src/* member-variable round (card A3): usesText grows the Owner.field member-selector branch (resolveFieldSelector + bare-name refusal listing spellings), the registered contract of this round | prior: lane/r10-cheap-buckets (r10 GitNexus fix round, LB-A + LB-G). SIX gating rows, ONE lane, read one by one before acking. THREE api-surface contract-changes, all deliberate parameter additions that ARE the feature: (a) mcpverbs usesText 2->3 params, taking McpPageArgs exactly as impactText already did, because the MCP uses verb gained the same default site cap as the CLI and an MCP-only agent that reads capped=1 needs a hatch it can reach (mcpclidiffcheck LENS 1 pins the two surfaces' root-attribute sets equal, so capping one and not the other is a divergence, not a saving); both dispatch sites AND kMcpVerbFields updated in the SAME commit, verified by mcpclidiffcheck/mcpverbscheck/usescheck green. (b)+(c) serialize packSignatures 17->18 and packSignaturesJson 11->12, both taking a trailing hasRelevanceFloor bool, default false so every non---for caller is byte-identical (verified: default map, pack-task, expand, exemplar, recall, hotspots, callers, grep, impact all unchanged). The flag cannot be replaced by passing a smaller topN, because those emitters read topN==0 as ALL, so a query nothing scores on would emit the whole corpus. THREE verbosity rows are the new code itself, already cut twice in this lane: duplicated rule bodies hoisted into relevanceFloorCut/pathTierIndexOver/compareTierThenPath, then the restated rationale moved to those helpers' headers - together taking gating from 13 to 6. What remains is runForLens +10, runCallHierarchy +11 and usesText +14 lines of genuinely new behaviour (the floor cut and its note plumbing; the tier index, the page window and the conditional legend clause). Splitting runForLens is a real refactor of its own - it was 658 lines before this lane touched it - and does not belong in an output-composition fix round. ack verbosity d42c85b67bd0956f 214 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack verbosity d44595768a7cf3af 106 cid=ad934f87aaaed8df preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack verbosity d63db6944aa504a7 1388 cid=1943300f4961480a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through +ack verbosity d721ea3d108a3ba8 73 cid=9bfd37570d23d571 now=73 was=56 p=src/serialize.h:5803 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity dbe6ed5269d328a4 325 cid=f7116f48272d6777 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack verbosity dd02b378ae6b5b75 126 cid=9ec35c817a609c0f capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost ack verbosity dd627540f10bba76 643 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck @@ -1642,6 +1702,7 @@ ack verbosity ed0cbdfe7fa72e8c 62 cid=efa4525003131a5f capture-audit 2026-09-04 ack verbosity eea83c3db0f03d69 518 cid=6fe919c0dd008ef5 by=src/* capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack verbosity efe685bb50482275 200 cid=10ded7c311552cd1 L10 finding 9b: buildStructRow now copies layout.h's caveat kind(s) into a new AffStruct.why field, writeFieldAffinity emits why= on unmodeled rows — the complexity/verbosity growth is that copy+emit cost | prior: audit pass 2026-08-06: per-cause as_* disclosure blocks in writeFieldAffinity follow the sibling one-if-per-attr pattern; chaseTypeCanPoint/isChaseRhsRow/looksLikePath are 2-call string predicates whose shared helper would be a worse abstraction; walks.cpp fixture twins are deliberate discriminating traps; short-horizon-churn is the audit editing just-landed code; runDefaultMap growth is the mapCtxOpenBytes fix + its measurement comment ack verbosity f1aa10922e41f258 84 cid=6670a895f0520c6a E3 (terminality round A, lane E): run_editsuite.py: the --cost-arith subcommand (E3's arithmetic, per task) +ack verbosity f7cf3b3c2851b336 135 cid=b44353ed67df2205 now=135 was=131 p=src/compactlegend.h:305 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity f8d747d123b4143d 94 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack verbosity fce48dedc40bc8eb 63 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack verbosity fd4cc5c6c0ad17d9 154 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean From 81abd3287167f0d16146e125d3bfc9dbcbfc1589 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 17:51:35 -0400 Subject: [PATCH 125/229] =?UTF-8?q?test(quality):=20a=20red=20gate=20for?= =?UTF-8?q?=20the=20defect-shape=20--quality-delta=20kind=20=E2=80=94=20fo?= =?UTF-8?q?rmat-arity,=20utf8-cut,=20dedup-first,=20vacuous-assert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defectshapecheck pins a twelfth --quality-delta kind, defect-shape, whose rows carry a defect= facet for four review-recurring defect shapes that a syntactic check can find when the code is written: - format-arity (C++ std::format family, fmt::, rw::emitTo/formatTo; Python "literal".format) gates: replacement fields that do not match the arguments. - utf8-cut (C++) is report-only: a byte cut plus an appended ellipsis with no UTF-8 boundary back-off. - dedup-first (C++) is report-only: a std::unique keep-first over a type with a severity field that neither the predicate nor the preceding sort reads. - vacuous-assert (Bash, test-script paths only) is report-only: an absence assertion read off a command whose failure is never checked. Every facet has positive arms and near-miss negatives: escapes, specs, positional and nested fields, non-literal formats, a continuation-byte back-off, a severity-ordered sort, rc and presence checks, and a script outside a test path. The gate also checks the legend, the --json and MCP surfaces, and that acking one facet leaves another facet on the same symbol. The current binary fails 47 arms (every positive and every surface arm) and passes the 77 negative and structural arms. Registered in regression.sh (gate count 672) and given a shard weight. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/defectshapecheck.sh | 944 ++++++++++++++++++++++++++++ test/regression.sh | 2 +- 6 files changed, 954 insertions(+), 9 deletions(-) create mode 100644 test/defectshapecheck.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 46492fcb1..c67b1d417 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -112,6 +112,7 @@ "deckclaimcheck.sh": 0.1, "deeptailcheck.sh": 1.0, "defaultceilingcheck.sh": 20.1, +"defectshapecheck.sh": 14.2, "defoverdeclcheck.sh": 1.4, "degradedhintcheck.sh": 2.5, "dependencypincheck.sh": 2.0, diff --git a/README.md b/README.md index f7523c566..8ff7b107f 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-671 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +672 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **671 gate scripts** and is the authoritative list; +`test/regression.sh` names **672 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 3326be7ac..d2c3210c1 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` | 671 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 672 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5866,7 +5866,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **671 gate scripts**, all of which exist on disk. +naming **672 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6969,7 +6969,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 671. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 672. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index a1e01bf65..0eb5532eb 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 = [ - ["671 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["672 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -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, "671", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "672", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 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"], - ["671 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["672 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh new file mode 100644 index 000000000..d38f1c958 --- /dev/null +++ b/test/defectshapecheck.sh @@ -0,0 +1,944 @@ +#!/usr/bin/env bash +# defectshapecheck.sh — gate for the twelfth --quality-delta kind, defect-shape: four shapes of a REAL defect +# that review keeps finding by hand and that a deterministic syntactic check finds at write time. Each row is +# , one kind with four facets so the kind list grows once and +# --ack-only=FACET selects one shape: +# format-arity — a literal std::format-family / rw::emitTo / rw::formatTo / fmt:: format string, or a Python +# "literal".format(...), whose replacement fields do not match its arguments. GATES (a dropped +# argument is a defect, not debt — so it gates whatever the origin; see §1 freshbad). +# std::format_string rejects too FEW arguments at compile time and accepts too MANY silently; +# Python raises on too few at run time and ignores too many. +# utf8-cut — C++: a byte cap on display text (a cut plus an appended ellipsis) with no UTF-8 boundary +# back-off, or an ellipsis appended at a byte cap reached by pushing bytes. REPORT-ONLY. +# dedup-first — C++: std::unique / std::ranges::unique with a predicate over a type that carries a +# severity-like field, where neither the predicate nor the sort before it reads that field, +# so the FIRST row of a run survives, not the most severe. REPORT-ONLY. +# vacuous-assert — Bash, test-script paths only: an ABSENCE assertion (grep -q PAT whose match is the failure +# branch) read off a command whose failure is never checked, so a crash reads as PASS. +# REPORT-ONLY. +# Like every kind, a row fires only on what THIS change introduced (a count that grew against the baseline), +# never on pre-existing debt; each facet has near-miss negatives the new code could plausibly mis-handle. +# +# Every absence assertion in this gate is read off a delta run that SUCCEEDED (rc 0 or 2 and a +# /dev/null 2>&1 || { echo " SKIP defectshapecheck (git not available)"; exit 0; } + +WORK="$( mktemp -d )" || { echo " FAIL defectshapecheck: mktemp -d failed"; exit 1; } +[ -n "$WORK" ] && [ -d "$WORK" ] || { echo " FAIL defectshapecheck: no temp dir"; exit 1; } +trap 'rm -rf "$WORK"' EXIT +echo "defectshapecheck: BIN=$BIN (temp corpora)" + +# ── harness ───────────────────────────────────────────────────────────────────────────────────────────── +fx_init(){ # NAME → a fresh git repo at $WORK/NAME + local d="$WORK/$1" + mkdir -p "$d" && ( cd "$d" && git init -q && git config user.email t@t && git config user.name t ) \ + || { no "$1: fixture repo could not be created"; return 1; } +} +fx_commit(){ ( cd "$WORK/$1" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) || no "$1: baseline commit failed"; } +QD_OUT=""; QD_RC=99; QD_OK=0 +fx_run(){ # NAME [extra args] → QD_OUT, QD_RC, QD_OK (1 only when the run succeeded and produced its root) + local d="$WORK/$1"; shift + QD_OUT="$( cd "$d" && "$BIN" . --quality-delta --no-cache "$@" 2>/dev/null )" + QD_RC=$? + QD_OK=0 + if { [ "$QD_RC" = 0 ] || [ "$QD_RC" = 2 ]; } && printf '%s' "$QD_OUT" | grep -q '' '\n' | grep '}" +} +hdr_attr(){ printf '%s' "$QD_OUT" | tr '>' '\n' | grep '= MIN, + # and the exit is 2 exactly when some row gates. + [ "$QD_OK" = 1 ] || { no "$1: cannot judge gating off a failed run"; return; } + local hg all fa other + hg="$( hdr_attr gating )" + case "$hg" in ''|*[!0-9]*) no "$1: header gating= is missing or not numeric ('$hg')"; return ;; esac + all="$( printf '%s' "$QD_OUT" | tr '>' '\n' | grep '= $2)"; else no "$1: $fa format-arity rows gate, want >= $2"; fi + if [ "$all" -gt 0 ]; then + if [ "$QD_RC" = 2 ]; then ok "$1: exit 2 with $all gating rows"; else no "$1: $all gating rows but exit $QD_RC"; fi + else + if [ "$QD_RC" = 0 ]; then ok "$1: exit 0 with no gating row"; else no "$1: no gating row but exit $QD_RC"; fi + fi +} + +# ── 1) FORMAT-ARITY, C++ ──────────────────────────────────────────────────────────────────────────────── +# Every function exists at the baseline with a CORRECT call. The edit turns some into a mismatch (the +# positives) and the rest into correct-but-tricky spellings the counter must get right (the near-misses): +# {{ }} escapes, format specs, positional {0}, nested dynamic width {:>{}} (it consumes an argument), a +# non-literal or macro format (skipped, never guessed), a "{}" inside an ARGUMENT, a raw string, a wide +# literal, a member function that happens to be called format, a commented-out call, a char argument '{'. +fx_init fmt_cpp +mkdir -p "$WORK/fmt_cpp/src" +FMT_HDR='#include +#include +#include +#include +#include +#include + +namespace rw +{ +template void emitTo( std::FILE* stream, std::format_string f, A&&... a ); +template std::size_t formatTo( char* buf, std::size_t cap, std::format_string f, A&&... a ); +template void emitRaw( std::FILE* stream, const S& text ); +} + +#define FMT_PAIR "{} {}" +constexpr const char* kFmt = "{} and {}"; + +struct Log +{ + std::string format( const char* f, int a ) const; +}; +' +{ printf '%s' "$FMT_HDR"; cat <<'EOF' +std::string extra( int a, int b ) { return std::format( "a={} b={}", a, b ); } +std::string fewer( int a, int b ) { return std::format( "a={} b={}", a, b ); } +void concat( int a, int b ) +{ + rw::emitTo( stdout, "\n", + a, b ); +} +std::size_t formatto( int a, int b ) { char buf[64]; return rw::formatTo( buf, sizeof( buf ), "", a, b ); } +std::string formattostd( int a, int b ) { std::string out; std::format_to( std::back_inserter( out ), "{}-{}", a, b ); return out; } +std::string positional( int a, int b ) { return std::format( "{0} {1}", a, b ); } +std::string dynwidthbad( const std::string& s, int w ) { return std::format( "{:>{}}", s, w ); } +void printstream( int a, int b ) { std::print( stderr, "{} {}\n", a, b ); } +std::string fmtns( int a, int b ) { return fmt::format( "{} {}", a, b ); } +std::string escaped( int a ) { return std::format( "{}", a ); } +std::string spec( int a, double d, int b ) { return std::format( "{} {} {}", a, d, b ); } +std::string posok( int a, int b ) { return std::format( "{} {}", a, b ); } +std::string dynwidth( const std::string& s, int w, double d, int p ) { return std::format( "{} {} {} {}", s, w, d, p ); } +std::string nonliteral( int a, int b ) { return std::format( "{} {}", a, b ); } +std::string fmtconstarg( int a ) { return std::format( "{}", a ); } +std::string macro( int a, int b ) { return std::format( "{} {}", a, b ); } +std::string inarg( int a ) { return std::format( "{}", a ); } +std::string rawstr( int a ) { return std::format( "{}", a ); } +std::wstring wide( int a, int b ) { return std::format( L"{}", a + b ); } +std::string member( const Log& log, int a ) { return log.format( "{}", a ); } +std::string commented( int a ) { return std::format( "{}", a ); } +std::string charlit( int a ) { return std::format( "{}", a ); } +void printok( int a ) { std::println( "{}", a ); } +void raw() { rw::emitRaw( stdout, "plain" ); } +std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } +int drive() { return 0; } +EOF +} >"$WORK/fmt_cpp/src/fmt.cpp" +fx_commit fmt_cpp +{ printf '%s' "$FMT_HDR"; cat <<'EOF' +std::string extra( int a, int b ) { return std::format( "a={} b={}", a, b, a + b ); } +std::string fewer( int a, int b ) { return std::format( "a={} b={} c={}", a, b ); } +void concat( int a, int b ) +{ + rw::emitTo( stdout, "\n", + a, b, a, b ); +} +std::size_t formatto( int a, int b ) { char buf[64]; return rw::formatTo( buf, sizeof( buf ), "", a, b ); } +std::string formattostd( int a, int b ) { std::string out; std::format_to( std::back_inserter( out ), "{}-{}", a, b, a ); return out; } +std::string positional( int a, int b ) { return std::format( "{0} {1}", a, b, a ); } +std::string dynwidthbad( const std::string& s, int w ) { return std::format( "{:>{}}", s ); } +void printstream( int a, int b ) { std::print( stderr, "{} {}\n", a ); } +std::string fmtns( int a, int b ) { return fmt::format( "{} {}", a ); } +std::string escaped( int a ) { return std::format( "{{literal}} {} }}{{", a ); } +std::string spec( int a, double d, int b ) { return std::format( "{:>8} {:.2f} {:#x}", a, d, b ); } +std::string posok( int a, int b ) { return std::format( "{0} and {0} and {1}", a, b ); } +std::string dynwidth( const std::string& s, int w, double d, int p ) { return std::format( "{:>{}} {:{}.{}f}", s, w, d, w, p ); } +std::string nonliteral( int a, int b ) { return std::format( kFmt, a, b, a ) + std::vformat( kFmt, std::make_format_args( a, b, a ) ); } +std::string fmtconstarg( int a ) { return std::format( kFmt, "{}" ); } +std::string macro( int a, int b ) { return std::format( FMT_PAIR, a, b, a ); } +std::string inarg( int a ) { return std::format( "{}", "{} {}" ); } +std::string rawstr( int a ) { return std::format( R"x()x", a ); } +std::wstring wide( int a, int b ) { return std::format( L"{} {}", a, b ); } +std::string member( const Log& log, int a ) { return log.format( "{} {} {}", a ); } +std::string commented( int a ) +{ + // return std::format( "{} {}", a ); + return std::format( "{}", a ); +} +std::string charlit( int a ) { return std::format( "{}{}", '{', a ); } +void printok( int a ) { std::println( "{} {}", a, a ); } +void raw() { rw::emitRaw( stdout, "{} {} {}" ); } +std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } +std::string freshbad( int a ) { return std::format( "{}", a, a ); } +std::string freshok( int a ) { return std::format( "{} {}", a, a ); } +int drive() { return 1; } +EOF +} >"$WORK/fmt_cpp/src/fmt.cpp" +fx_run fmt_cpp +L="format-arity (c++)" +ds_has "$L" format-arity extra gating +ds_has "$L" format-arity fewer gating +ds_has "$L" format-arity concat gating +ds_has "$L" format-arity formatto gating +ds_has "$L" format-arity formattostd gating +ds_has "$L" format-arity positional gating +ds_has "$L" format-arity dynwidthbad gating +ds_has "$L" format-arity printstream gating +ds_has "$L" format-arity fmtns gating +ds_has "$L" format-arity freshbad gating-new +ds_wasnow "$L" format-arity extra 0 1 +ds_wasnow "$L" format-arity concat 0 1 +ds_none "$L" escaped "{{ and }} are escapes, not fields" +ds_none "$L" spec "a format spec is part of its field" +ds_none "$L" posok "{0} twice and {1} need two arguments" +ds_none "$L" dynwidth "a nested {} width/precision consumes an argument" +ds_none "$L" nonliteral "a named format constant or vformat is skipped, never guessed" +ds_none "$L" fmtconstarg "the literal is an ARGUMENT, not the format" +ds_none "$L" macro "a macro format is skipped, never guessed" +ds_none "$L" inarg "a {} inside an argument string is not a field" +ds_none "$L" rawstr "a raw string with a custom delimiter and a )\" inside, one field" +ds_none "$L" wide "an L-prefixed literal counts like any other" +ds_none "$L" member "Log::format is not the format family" +ds_none "$L" commented "a commented-out call is not code" +ds_none "$L" charlit "a '{' char argument is an argument" +ds_none "$L" printok "std::println with matching arguments" +ds_none "$L" raw "emitRaw prints bytes, it does not format" +ds_none "$L" legacy "a mismatch the change did not introduce (untouched function)" +ds_none "$L" freshok "a new function with a matching call" +gating_split "$L" 10 +OFMT="$QD_OUT" +fx_run fmt_cpp +if [ "$QD_OK" = 1 ] && [ "$OFMT" = "$QD_OUT" ]; then ok "$L: delta byte-identical run-to-run"; else no "$L: non-deterministic delta"; fi +if command -v xmllint >/dev/null 2>&1; then + if printf '%s' "$OFMT" | xmllint --noout - 2>/dev/null; then ok "$L: xml well-formed"; else no "$L: xml malformed"; fi +else + echo " SKIP $L: xml well-formedness (xmllint not installed)" +fi + +# ── 1b) FORMAT-ARITY, Python "literal".format(...) ────────────────────────────────────────────────────── +# Too few raises IndexError/KeyError at run time; too many is ignored silently. Named fields count against +# keyword arguments, auto/positional fields against positional ones; *args / **kwargs make the count +# unknowable (skipped). f-strings, %-formatting and a non-literal receiver are out of the shape. +fx_init fmt_py +printf 'VERSION = "1"\nBANNER = "v{}".format(VERSION)\n' >"$WORK/fmt_py/banner.py" +cat >"$WORK/fmt_py/fmt.py" <<'EOF' + + +def extra(a, b): + return "a={} b={}".format(a, b) + + +def fewer(a, b): + return "a={} b={}".format(a, b) + + +def namedmissing(x, y): + return "{x} {y}".format(x=x, y=y) + + +def concat(a, b): + return ("a={} " "b={}").format(a, b) + + +def escaped(a): + return "{}".format(a) + + +def star(pair): + return "{} {}".format(pair[0], pair[1]) + + +def kwstar(d): + return "{a} {b}".format(a=d["a"], b=d["b"]) + + +def attr(f): + return "{}".format(f.name) + + +def index(d): + return "{}".format(d["k"]) + + +def nested(a): + return "{}".format(a) + + +def mixed(a, b): + return "{} {}".format(a, b) + + +def fstring(a, b): + return "{} {}".format(a, b) + + +def nonliteral(tmpl, a, b): + return tmpl.format(a, b) + + +def percent(a): + return "%s" % (a,) + + +def triple(a, b): + return """{} {}""".format(a, b) + + +def legacy(a): + return "{} {}".format(a, a, a) +EOF +fx_commit fmt_py +printf 'VERSION = "1"\nBANNER = "v{} build {}".format(VERSION)\n' >"$WORK/fmt_py/banner.py" +cat >"$WORK/fmt_py/fmt.py" <<'EOF' + + +def extra(a, b): + return "a={} b={}".format(a, b, a + b) + + +def fewer(a, b): + return "a={} b={} c={}".format(a, b) + + +def namedmissing(x, y): + return "{x} {y}".format(x=x) + + +def concat(a, b): + return ("a={} " "b={} " "c={}").format(a, b) + + +def escaped(a): + return "{{}} {} {{x}}".format(a) + + +def star(pair): + return "{} {} {}".format(*pair) + + +def kwstar(d): + return "{a} {b} {c}".format(**d) + + +def attr(f): + return "{0.name} {0.size}".format(f) + + +def index(d): + return "{0[k]} {0[j]}".format(d) + + +def nested(a): + return "{!r:>{w}}".format(a, w=3) + + +def mixed(a, b): + return "{0} {name}".format(a, name=b) + + +def fstring(a, b): + return f"{a} {b} {{}}" + + +def nonliteral(tmpl, a, b): + return tmpl.format(a, b, a) + + +def percent(a): + return "%s %s" % (a,) + + +def triple(a, b): + return """{} +{}""".format(a, b) + + +def legacy(a): + return "{} {}".format(a, a, a) + + +def freshbad(a): + return "{}".format(a, a) +EOF +fx_run fmt_py +L="format-arity (python)" +ds_has "$L" format-arity extra gating +ds_has "$L" format-arity fewer gating +ds_has "$L" format-arity namedmissing gating +ds_has "$L" format-arity concat gating +ds_has "$L" format-arity freshbad gating-new +ds_has_p "$L" format-arity banner.py gating +ds_none "$L" escaped "{{ }} are escapes" +ds_none "$L" star "*args makes the count unknowable" +ds_none "$L" kwstar "**kwargs makes the names unknowable" +ds_none "$L" attr "{0.name} {0.size} need one positional argument" +ds_none "$L" index "{0[k]} {0[j]} need one positional argument" +ds_none "$L" nested "a nested named width {w} is a keyword argument" +ds_none "$L" mixed "one positional, one named, both given" +ds_none "$L" fstring "an f-string has no argument list" +ds_none "$L" nonliteral "a non-literal receiver is skipped, never guessed" +ds_none "$L" percent "%-formatting is not this shape" +ds_none "$L" triple "a triple-quoted literal with two fields and two arguments" +ds_none "$L" legacy "a mismatch the change did not introduce" +gating_split "$L" 6 + +# ── 2) UTF8-CUT, C++ (report-only) ────────────────────────────────────────────────────────────────────── +# The corpus shapes: a cut (resize / substr(0,N) / erase(N)) plus an appended ellipsis on the SAME string +# with no UTF-8 back-off in the function, and an ellipsis appended once a byte-pushing loop reaches the +# cap (the cut lands mid-sequence and the ellipsis can fire with nothing dropped). Near-misses: a +# continuation-byte back-off (0xC0/0x80), a utf8-named helper, a cut with no ellipsis (a buffer, not text — +# a stated floor), an ellipsis on a different variable, an ellipsis that replaces the value, a u32string. +fx_init utf8 +mkdir -p "$WORK/utf8/src" +U8_HDR='#include +#include + +constexpr std::size_t kCap = 80; +std::string truncateUtf8WithEllipsis( std::string_view s, std::size_t cap ); +' +{ printf '%s' "$U8_HDR"; cat <<'EOF' +std::string excerpt( std::string s ) { return s; } +std::string clip( const std::string& s, std::size_t n ) { return s; } +std::string build( std::string_view s ) +{ + std::string out; + for( char c : s ) + { + out.push_back( c ); + } + return out; +} +std::string erasecut( std::string s, std::size_t n ) { return s; } +std::string guarded( std::string s ) { return s; } +std::string helper( const std::string& s ) { return s; } +std::string noellipsis( std::string s, std::size_t n ) { return s; } +std::string othervar( std::string buf, std::string label, std::size_t n ) { return buf + label; } +std::string replaced( const std::string& s, std::size_t n ) { return s; } +std::string loading( std::string out ) { return out; } +std::u32string wide( std::u32string s, std::size_t n ) { return s; } +std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } +int drive() { return 0; } +EOF +} >"$WORK/utf8/src/text.cpp" +fx_commit utf8 +{ printf '%s' "$U8_HDR"; cat <<'EOF' +std::string excerpt( std::string s ) +{ + if( s.size() > 117 ) + { + s.resize( 117 ); + s += "..."; + } + return s; +} +std::string clip( const std::string& s, std::size_t n ) { return s.size() > n ? s.substr( 0, n ) + "\xE2\x80\xA6" : s; } +std::string build( std::string_view s ) +{ + std::string out; + for( char c : s ) + { + out.push_back( c ); + if( out.size() >= kCap ) + { + out += "\xE2\x80\xA6"; + break; + } + } + return out; +} +std::string erasecut( std::string s, std::size_t n ) +{ + if( s.size() > n ) + { + s.erase( n ); + s.append( "..." ); + } + return s; +} +std::string guarded( std::string s ) +{ + if( s.size() > 117 ) + { + std::size_t n = 117; + while( n > 0 && ( static_cast( s[n] ) & 0xC0 ) == 0x80 ) + { + --n; + } + s.resize( n ); + s += "..."; + } + return s; +} +std::string helper( const std::string& s ) { return truncateUtf8WithEllipsis( s, 117 ); } +std::string noellipsis( std::string s, std::size_t n ) +{ + if( s.size() > n ) + { + s.resize( n ); + } + return s; +} +std::string othervar( std::string buf, std::string label, std::size_t n ) +{ + buf.resize( n ); + label += "..."; + return buf + label; +} +std::string replaced( const std::string& s, std::size_t n ) { return s.size() > n ? std::string( "..." ) : s; } +std::string loading( std::string out ) { out += "loading..."; return out; } +std::u32string wide( std::u32string s, std::size_t n ) +{ + if( s.size() > n ) + { + s.resize( n ); + s += U"…"; + } + return s; +} +std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } +std::string freshcut( std::string s ) { if( s.size() > 60 ) { s = s.substr( 0, 60 ) + "..."; } return s; } +int drive() { return 1; } +EOF +} >"$WORK/utf8/src/text.cpp" +fx_run utf8 +L="utf8-cut (c++)" +ds_has "$L" utf8-cut excerpt minor +ds_has "$L" utf8-cut clip minor +ds_has "$L" utf8-cut build minor +ds_has "$L" utf8-cut erasecut minor +ds_has "$L" utf8-cut freshcut new-symbol +ds_wasnow "$L" utf8-cut excerpt 0 1 +ds_none "$L" guarded "the cut backs off continuation bytes (0xC0/0x80)" +ds_none "$L" helper "a utf8 truncation helper does the cut" +ds_none "$L" noellipsis "a cut with no ellipsis is not judged display text (stated floor)" +ds_none "$L" othervar "the ellipsis goes on a different string than the one cut" +ds_none "$L" replaced "the ellipsis replaces the value, nothing is cut" +ds_none "$L" loading "a literal that merely ends in ... with no cap" +ds_none "$L" wide "a u32string holds code points, a cut cannot split one" +ds_none "$L" legacy "a cut the change did not introduce" +gating_split "$L" 0 + +# ── 3) DEDUP-FIRST, C++ (report-only) ─────────────────────────────────────────────────────────────────── +# The corpus shape (a two-pass merge, sorted on (line, rule), uniqued keeping the first) plus its ranges and +# unsorted siblings. Near-misses: the sort orders on severity (worst first), the predicate compares severity, +# a type with no severity-like field, a field whose name only CONTAINS a severity word (indentLevel — a +# bounded token match), a predicate-less unique, a unique over ints. +fx_init dedup +mkdir -p "$WORK/dedup/src" +DD_HDR='#include +#include +#include + +enum class Severity { Info, Warn, Critical }; +struct Finding +{ + int line = 0; + std::string rule; + Severity sev = Severity::Info; +}; +struct Row +{ + int line = 0; + std::string rule; + std::string text; +}; +struct Node +{ + int line = 0; + std::string rule; + int indentLevel = 0; +}; +' +{ printf '%s' "$DD_HDR"; cat <<'EOF' +void mergeFirst( std::vector& v ) { (void)v; } +void rangesFirst( std::vector& v ) { (void)v; } +void unsortedFirst( std::vector& v ) { (void)v; } +void worstFirst( std::vector& v ) { (void)v; } +void predSev( std::vector& v ) { (void)v; } +void noSev( std::vector& v ) { (void)v; } +void indent( std::vector& v ) { (void)v; } +void defaultEq( std::vector& v ) { (void)v; } +void ints( std::vector& v ) { (void)v; } +void legacy( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); +} +int drive() { return 0; } +EOF +} >"$WORK/dedup/src/merge.cpp" +fx_commit dedup +{ printf '%s' "$DD_HDR"; cat <<'EOF' +void mergeFirst( std::vector& v ) +{ + std::stable_sort( v.begin(), v.end(), []( const Finding& a, const Finding& b ) + { return a.line != b.line ? a.line < b.line : a.rule < b.rule; } ); + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) + { return a.line == b.line && a.rule == b.rule; } ), + v.end() ); +} +void rangesFirst( std::vector& v ) +{ + std::ranges::sort( v, []( const Finding& a, const Finding& b ) { return a.line < b.line; } ); + v.erase( std::ranges::unique( v, []( const Finding& a, const Finding& b ) { return a.line == b.line; } ).begin(), v.end() ); +} +void unsortedFirst( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.rule == b.rule; } ), v.end() ); +} +void worstFirst( std::vector& v ) +{ + std::stable_sort( v.begin(), v.end(), []( const Finding& a, const Finding& b ) + { return a.line != b.line ? a.line < b.line : a.sev > b.sev; } ); + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); +} +void predSev( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) + { return a.line == b.line && a.sev == b.sev; } ), + v.end() ); +} +void noSev( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Row& a, const Row& b ) { return a.line == b.line; } ), v.end() ); +} +void indent( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Node& a, const Node& b ) { return a.line == b.line; } ), v.end() ); +} +void defaultEq( std::vector& v ) +{ + std::sort( v.begin(), v.end() ); + v.erase( std::unique( v.begin(), v.end() ), v.end() ); +} +void ints( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( int a, int b ) { return a / 10 == b / 10; } ), v.end() ); +} +void legacy( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); +} +void freshFirst( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); +} +int drive() { return 1; } +EOF +} >"$WORK/dedup/src/merge.cpp" +fx_run dedup +L="dedup-first (c++)" +ds_has "$L" dedup-first mergeFirst minor +ds_has "$L" dedup-first rangesFirst minor +ds_has "$L" dedup-first unsortedFirst minor +ds_has "$L" dedup-first freshFirst new-symbol +ds_none "$L" worstFirst "the sort before the unique orders on severity, worst first" +ds_none "$L" predSev "the predicate keeps rows of different severity apart" +ds_none "$L" noSev "the element type has no severity-like field" +ds_none "$L" indent "indentLevel only contains a severity word (bounded token match)" +ds_none "$L" defaultEq "a predicate-less unique compares whole values" +ds_none "$L" ints "a unique over ints carries no severity" +ds_none "$L" legacy "a keep-first the change did not introduce" +gating_split "$L" 0 + +# ── 4) VACUOUS-ASSERT, Bash gate scripts (report-only) ────────────────────────────────────────────────── +# One script per arm (top-level code has no enclosing symbol, so its row anchors on the file and a file +# per arm keeps the arms apart). Baseline: each script has its reporters and one sound assertion; the edit +# appends the arm. Positives: an absence assertion read off an unchecked $( ) capture, off a direct pipe +# (with or without pipefail — under pipefail a crash makes the pipeline false, which takes the same PASS +# branch), the if/else spelling, backslash continuations, the !-negated pass spelling, other reporter +# names, inside a function, and a brand-new script. Near-misses: the capture's rc checked, `|| no` on the +# capture, a presence check, a positive assertion on the same output first, the match-is-pass polarity, +# a literal producer, a commented-out shape, a pre-existing vacuous line, a sound if, and the same shape in +# a script outside a test path. +fx_init vac +mkdir -p "$WORK/vac/test" "$WORK/vac/scripts" +VHDR="$( cat <<'EOF' +#!/usr/bin/env bash +BIN="${1:?}" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +V0="$( "$BIN" --version 2>&1 )"; RC0=$? +[ "$RC0" = 0 ] && ok "version runs" || no "version failed: $V0" +EOF +)" +VHDR="$VHDR +" +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok; do + printf '%s' "$VHDR" >"$WORK/vac/test/$f.sh" +done +printf '%s' "$VHDR" >"$WORK/vac/scripts/build.sh" +cat >>"$WORK/vac/test/n_legacy.sh" <<'EOF' +OLD="$( "$BIN" --old 2>/dev/null )" +printf '%s' "$OLD" | grep -q 'stale' && no "stale present" || ok "stale absent" +EOF +cat >"$WORK/vac/test/t_names.sh" <<'EOF' +#!/usr/bin/env bash +BIN="${1:?}" +status=0 +report_pass(){ echo "PASS $*"; } +report_fail(){ echo "FAIL $*"; status=1; } +"$BIN" --version >/dev/null 2>&1 && report_pass "runs" || report_fail "does not run" +EOF +cat >>"$WORK/vac/test/t_fn.sh" <<'EOF' +absent(){ + local out + out="$( "$BIN" --version 2>&1 )" || { no "version failed"; return; } + printf '%s' "$out" | grep -q "$1" && no "$1 present" || ok "$1 absent" +} +absent zzz +EOF +fx_commit vac +cat >>"$WORK/vac/test/t_var.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/t_pipe.sh" <<'EOF' +"$BIN" --list 2>/dev/null | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +{ printf 'set -o pipefail\n'; cat <<'EOF' +"$BIN" --list 2>/dev/null | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +} >>"$WORK/vac/test/t_pipefail.sh" +cat >>"$WORK/vac/test/t_if.sh" <<'EOF' +OUT2="$( "$BIN" --list 2>/dev/null )" +if printf '%s' "$OUT2" | grep -q 'bad'; then + no "bad present" +else + ok "bad absent" +fi +EOF +cat >>"$WORK/vac/test/t_cont.sh" <<'EOF' +MAP="$( "$BIN" --map 2>/dev/null )" +printf '%s' "$MAP" | grep -q 'suspect=' \ + && no "(f) a clean file carries suspect=" \ + || ok "(f) no suspect= on the clean file" +EOF +cat >>"$WORK/vac/test/t_neg.sh" <<'EOF' +NOUT="$( "$BIN" --list 2>/dev/null )" +! printf '%s' "$NOUT" | grep -q 'bad' && ok "bad absent" || no "bad present" +EOF +cat >>"$WORK/vac/test/t_names.sh" <<'EOF' +R="$( "$BIN" --list 2>/dev/null )" +echo "$R" | grep -q 'bad' && report_fail "bad present" || report_pass "bad absent" +EOF +cat >>"$WORK/vac/test/t_fn.sh" <<'EOF' +absent_unchecked(){ + local out + out="$( "$BIN" --list 2>/dev/null )" + printf '%s' "$out" | grep -q "$1" && no "$1 present" || ok "$1 absent" +} +absent_unchecked yyy +EOF +{ printf '%s' "$VHDR"; cat <<'EOF' +FRESH="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$FRESH" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +} >"$WORK/vac/test/t_new.sh" +cat >>"$WORK/vac/test/n_rc.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )"; RC=$? +[ "$RC" = 0 ] || no "the list run failed (rc=$RC)" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_orno.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" || no "the list run failed" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_presence.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +[ -n "$OUT" ] || no "the list run printed nothing" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_positive.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q '>"$WORK/vac/test/n_polarity.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q 'good' && ok "good present" || no "good missing" +EOF +cat >>"$WORK/vac/test/n_literal.sh" <<'EOF' +X="fixed text" +printf '%s' "$X" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_comment.sh" <<'EOF' +# OUT="$( "$BIN" --list 2>/dev/null )" +# printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +echo done +EOF +cat >>"$WORK/vac/test/n_legacy.sh" <<'EOF' +echo "an unrelated line after the pre-existing assertion" +EOF +cat >>"$WORK/vac/test/n_ifok.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )"; RC=$? +if [ "$RC" = 0 ] && ! printf '%s' "$OUT" | grep -q 'bad'; then ok "bad absent"; else no "bad present or the run failed (rc=$RC)"; fi +EOF +cat >>"$WORK/vac/scripts/build.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +fx_run vac +L="vacuous-assert (bash)" +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names; do + ds_has_p "$L" vacuous-assert "test/$f.sh" minor +done +ds_has "$L" vacuous-assert absent_unchecked new-symbol +ds_has_p "$L" vacuous-assert test/t_new.sh new-symbol +ds_none "$L" absent "the capture's failure is handled (|| { no; return; }) — and the function is untouched" +ds_none_p "$L" test/n_rc.sh "the capture's rc is checked before the absence" +ds_none_p "$L" test/n_orno.sh "|| no on the capture" +ds_none_p "$L" test/n_presence.sh "a presence check before the absence" +ds_none_p "$L" test/n_positive.sh "a positive assertion on the same output first" +ds_none_p "$L" test/n_polarity.sh "a match is the PASS branch: a crash fails" +ds_none_p "$L" test/n_literal.sh "the producer is a literal, not a command" +ds_none_p "$L" test/n_comment.sh "a commented-out shape is not code" +ds_none_p "$L" test/n_legacy.sh "a vacuous line the change did not introduce" +ds_none_p "$L" test/n_ifok.sh "the if checks the rc in its condition" +ds_none_p "$L" scripts/build.sh "not a test-script path: the kind is scoped to gate scripts" +gating_split "$L" 0 + +# ── 5) SURFACES: legend, JSON twin, MCP, and the ack identity of a facet ───────────────────────────────── +L="surfaces" +LEG="$( cd "$WORK/fmt_cpp" && "$BIN" . --quality-delta --no-cache --legend=full 2>/dev/null )" +if printf '%s' "$LEG" | grep -q '/dev/null 2>&1; then + JS="$( cd "$WORK/fmt_cpp" && "$BIN" . --quality-delta --no-cache --json 2>/dev/null )" + JV="$( printf '%s' "$JS" | python3 -c ' +import json, sys +try: d = json.load(sys.stdin) +except Exception as e: print("BROKEN %s" % e); raise SystemExit +rows = [r for r in d.get("r", []) if r.get("kind") == "defect-shape" and r.get("defect") == "format-arity"] +print("ROWS %d" % len(rows))' )" + case "$JV" in ROWS\ *) n="${JV#ROWS }"; [ "$n" -ge 10 ] && ok "$L: --json carries the $n format-arity rows (kind + defect keys)" \ + || no "$L: --json carries $n format-arity rows, want >= 10" ;; + *) no "$L: --json probe: $JV" ;; esac + MC="$( printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"quality_delta","arguments":{"path":"'"$WORK/fmt_cpp"'"}}}' \ + | "$BIN" --mcp 2>/dev/null | tail -1 )" + MV="$( printf '%s' "$MC" | python3 -c ' +import json, sys +try: + r = json.load(sys.stdin) + t = r["result"]["content"][0]["text"] + d = json.loads(t) +except Exception as e: print("BROKEN %s" % e); raise SystemExit +rows = [x for x in d.get("r", []) if x.get("kind") == "defect-shape" and x.get("defect") == "format-arity"] +print("ROWS %d" % len(rows))' )" + case "$MV" in ROWS\ *) n="${MV#ROWS }"; [ "$n" -ge 10 ] && ok "$L: MCP quality_delta carries the $n format-arity rows" \ + || no "$L: MCP quality_delta carries $n format-arity rows, want >= 10" ;; + *) no "$L: MCP quality_delta probe: $MV" ;; esac +else + echo " SKIP $L: --json and MCP row checks (python3 not installed)" +fi + +# Ack identity (checklist 16): one symbol with BOTH a format-arity and a utf8-cut row. Acking only the +# format-arity facet must leave the utf8-cut row on the same symbol standing — a facet is part of the +# finding's identity, not a label on one shared per-symbol key. +fx_init both +mkdir -p "$WORK/both/src" +cat >"$WORK/both/src/b.cpp" <<'EOF' +#include +#include +std::string label( std::string s, int a ) { return std::format( "{}", a ) + s; } +int drive() { return 0; } +EOF +fx_commit both +cat >"$WORK/both/src/b.cpp" <<'EOF' +#include +#include +std::string label( std::string s, int a ) +{ + if( s.size() > 50 ) + { + s.resize( 50 ); + s += "..."; + } + return std::format( "{}", a, a ) + s; +} +int drive() { return 1; } +EOF +fx_run both +ds_has "$L ack" format-arity label gating +ds_has "$L ack" utf8-cut label minor +( cd "$WORK/both" && "$BIN" . --quality-delta --no-cache --ack-only=format-arity --quality-ack="deliberate in this fixture" >/dev/null 2>&1 ) +ARC=$? +if [ "$ARC" = 0 ]; then ok "$L ack: --ack-only=format-arity selects the facet (exit 0)"; else no "$L ack: --ack-only=format-arity exited $ARC"; fi +fx_run both +if [ "$QD_OK" = 1 ]; then + if [ -z "$( ds_row format-arity label )" ]; then ok "$L ack: the acked format-arity row is suppressed"; else no "$L ack: the format-arity row survived its ack: $( ds_row format-arity label )"; fi +fi +ds_has "$L ack" utf8-cut label minor +if [ "$QD_RC" = 0 ]; then ok "$L ack: with format-arity acked the run exits 0"; else no "$L ack: should exit 0 after the ack (got $QD_RC)"; fi + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/regression.sh b/test/regression.sh index 9435c5687..dda46ba96 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 9f59d94e4eec83ec820a2775b3c994de0dffbc2b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 18:20:10 -0400 Subject: [PATCH 126/229] test: pin the primer positive control to an explicit treatment arm (#381) The unconfigured default is now the auto split; A4 asserts that treatment still gets the SessionStart primer, not what the default is (M20/M20d assert that). Co-Authored-By: Claude Sonnet 5.5 --- test/hookcheck.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/hookcheck.sh b/test/hookcheck.sh index 4a9f08c4d..338200649 100755 --- a/test/hookcheck.sh +++ b/test/hookcheck.sh @@ -1482,9 +1482,11 @@ OUTA3="$( printf '%s' '{"session_id":"armss","cwd":"'"$REPO"'","source":"startup || no "A3b arm: control session-start row = [$( meterrowget "$LA3" 1 class )/$( meterrowget "$LA3" 1 arm )]" # A4: the positive control for A3. Suppressing the primer everywhere would pass A3 for the wrong reason. +# The arm is pinned to treatment explicitly: the unconfigured default is the auto split (issue #381), under +# which this session id could land in control; the default itself is asserted by M20/M20d. TA4="$TMP/ta4"; mkdir -p "$TA4"; LA4="$TMP/a4.jsonl" OUTA4="$( printf '%s' '{"session_id":"armsst","cwd":"'"$REPO"'","source":"startup"}' \ - | env HOME="$METERHOME" RIPWIRE_METER_LOG="$LA4" RIPWIRE_METER_FIXTURE=1 \ + | env HOME="$METERHOME" RIPWIRE_METER_LOG="$LA4" RIPWIRE_METER_FIXTURE=1 RIPWIRE_METER_ARM=treatment \ PATH="$WRAP_PATH" TMPDIR="$TA4" bash "$HOOK" --session-start )" [ -n "$OUTA4" ] && printf '%s' "$OUTA4" | is_valid_json \ && ok "A4 arm: the treatment arm still gets the SessionStart primer (A3's positive control)" \ From 5a8e823469f333c9295196afac5e6c71e9c5eecf Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 18:22:39 -0400 Subject: [PATCH 127/229] test(compactlegend): the P4 floor twin's PASS line reads as one sentence (manifestcheck parsed '; at' as a call) Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index df06985a1..de1d28b8a 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -880,7 +880,7 @@ for tb in 500 750; do p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=${pc:-?} over the budget AND over the full answer's ${pf:-?}" fi done -[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= at the rank 1..4 floor with over_ceiling=\"1\", the floor clause and an honest est_tokens; at 750 the paid next= fits with no label; compact never further over than full" +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= at the rank 1..4 floor with over_ceiling=\"1\", the floor clause and an honest est_tokens, the 750 run's paid next= fits with no label, compact never further over than full" # (P5) the BUDGET LEDGER survives compaction (orchestrator rule, METHODOLOGY §9.3/§9.4: never cut silently). --pack-task's # full legend ends with "budget=N bytes (T-token target, ceiling C) | ranking: … | bodies: … | callers: … | notes: … | tests: From dd6e4c8ed30ee9f98cbedc537dffeaa0a01f4c75 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 18:25:21 -0400 Subject: [PATCH 128/229] =?UTF-8?q?test(estcalib):=20re-pin=20for-budgeted?= =?UTF-8?q?=20=E2=80=94=20the=20document=20now=20carries=20its=20=20?= =?UTF-8?q?next=3D=20and=20the=20floor=20clause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --for=dedupe --token-budget=400 serves a one-row at its floor; it now names the call that serves it uncut and, landing past its ceiling, says why. The document changed, so its real token count was re-derived with tiktoken exactly as bench/tokenaudit/pin.py does (full legend, fixture copied outside any repo): o200k 484 / cl100k 479 (base binary's document today: 437 / 433). est_tokens=679 is +40%, inside the unchanged band. Only this row is re-pinned; the band rule is untouched. Co-Authored-By: Claude Opus 5.5 --- test/estcalib.manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/estcalib.manifest b/test/estcalib.manifest index a745e590f..aecd0a9fb 100644 --- a/test/estcalib.manifest +++ b/test/estcalib.manifest @@ -8,5 +8,5 @@ metrics 2381 2370 2509 --metrics pack-signatures 1801 1784 2116 --pack-signatures expand 541 542 455 --expand=billableTotal for-named 789 785 1050 --for=billableTotal -for-budgeted 419 415 598 --for=dedupe --token-budget=400 +for-budgeted 484 479 679 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe From 50790a143bc3d4b584d0e54cf95ab3ef458556a6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 18:31:47 -0400 Subject: [PATCH 129/229] =?UTF-8?q?test(quality):=20gates=20(review)=20?= =?UTF-8?q?=E2=80=94=20the=20defect-shape=20gate=20takes=20the=20early=20g?= =?UTF-8?q?ates=20review's=2011=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - D2 at every exit-predicate surface: the new-symbol format-arity row carries "gating": true in --json and MCP, the top-level "gating" equals the rows that carry it, and --ack-only=gating takes it (fresh copy). - One fixed phrase on every surface that states the never-gate rule ("new-symbol rows never gate, except defect-shape format-arity"): full and compact legend, --help, the MCP description, the quality-bar skill's trigger text, COMMANDS.md and README; no stale 11/eleven-kinds count on any of them. - The motivating case as an arm: a clean merge of two sides that each add the same field literal and one argument, in ref-pair mode; each side alone stays quiet. - vacuous-assert: a positive assertion after the absence, an enclosing if/case on the same capture and a plain capture under set -e are guards; local-capture and direct-pipe under set -e stay positive; a new function with a handled capture gives no row. - utf8-cut: a cut at a parsed delimiter with pack-syntax "..." and a whole-word append loop are negatives. - dedup-first: a heading level and a named predicate are negatives; the mild-first sort is a stated floor. - format-arity siblings: pack expansion, runtime formats, macro arguments and pieces, unqualified format(), the ostream print overload, format_to_n, positional nested width, chrono/fill specs (negatives); a std::locale first argument and an unreferenced positional index (positives); Python unused keyword, raw literal and unparenthesised implicit concat (positives), auto nested width and format_map (negatives). - swap (one fixed, one added in a function) gates; a mismatch moved to a new file and renamed does not. - The ack arm judges its exit by attribution and adds --ack-only=defect-shape on a fresh copy. Co-Authored-By: Claude Opus 5.5 --- test/defectshapecheck.sh | 478 +++++++++++++++++++++++++++++++++++---- 1 file changed, 434 insertions(+), 44 deletions(-) diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh index d38f1c958..b03a6e54f 100644 --- a/test/defectshapecheck.sh +++ b/test/defectshapecheck.sh @@ -16,8 +16,22 @@ # vacuous-assert — Bash, test-script paths only: an ABSENCE assertion (grep -q PAT whose match is the failure # branch) read off a command whose failure is never checked, so a crash reads as PASS. # REPORT-ONLY. -# Like every kind, a row fires only on what THIS change introduced (a count that grew against the baseline), -# never on pre-existing debt; each facet has near-miss negatives the new code could plausibly mis-handle. +# Like every kind, a row fires only on what THIS change introduced, never on pre-existing debt: a site is new +# when its normalized text is not among the baseline's sites of that facet (repo-wide), so an untouched, +# MOVED or RENAMED mismatch gives no row and a swap (one fixed, one added in the same function) still does. +# was/now count the anchor's offending sites. Each facet has near-miss negatives the new code could plausibly +# mis-handle. +# +# format-arity is the ONE exception to "new-symbol rows never gate": new-symbol rows never gate, except +# defect-shape format-arity (§1 freshbad, §5 legend/help/MCP/skill/docs arms). Every surface that applies the +# exit predicate (XML gating=, --json and MCP "gating", --ack-only=gating) must agree on it (§1 ackgating, §5). +# +# Stated floors (no row is not a verdict on these): Python %-formatting and f-strings; a format held in a +# named constant, a macro, a pack expansion or a runtime/vformat wrapper; an unqualified format()/print(); +# the std::print ostream overload; a dedup whose sort reads severity in the MILD-first direction, or whose +# predicate/comparator is a named function (skipped, never guessed); a utf8 cut with no ellipsis; and the +# utf8/dedup fixtures cannot see an exit-only bug on their own (their clone near-misses gate duplication) — +# §4 vac and §5 both carry the exit judgment for the report-only facets. # # Every absence assertion in this gate is read off a delta run that SUCCEEDED (rc 0 or 2 and a # /dev/null )" + QD_RC=$? + QD_OK=0 + if { [ "$QD_RC" = 0 ] || [ "$QD_RC" = 2 ]; } && printf '%s' "$QD_OUT" | grep -q '' '\n' | grep ' +#include +#include #include +#include #include +#include #include #include +#include #include namespace rw @@ -152,7 +185,9 @@ template void emitRaw( std::FILE* stream, const S& text ); } #define FMT_PAIR "{} {}" +#define PAIR a, b constexpr const char* kFmt = "{} and {}"; +std::string format( const char* f, int a ); struct Log { @@ -175,7 +210,7 @@ std::string dynwidthbad( const std::string& s, int w ) { return std::format( "{: void printstream( int a, int b ) { std::print( stderr, "{} {}\n", a, b ); } std::string fmtns( int a, int b ) { return fmt::format( "{} {}", a, b ); } std::string escaped( int a ) { return std::format( "{}", a ); } -std::string spec( int a, double d, int b ) { return std::format( "{} {} {}", a, d, b ); } +std::string spec( int a, double d, int b, std::chrono::sys_days t, const std::string& s ) { return std::format( "{} {} {} {} {}", a, d, b, t, s ); } std::string posok( int a, int b ) { return std::format( "{} {}", a, b ); } std::string dynwidth( const std::string& s, int w, double d, int p ) { return std::format( "{} {} {} {}", s, w, d, p ); } std::string nonliteral( int a, int b ) { return std::format( "{} {}", a, b ); } @@ -189,10 +224,23 @@ std::string commented( int a ) { return std::format( "{}", a ); } std::string charlit( int a ) { return std::format( "{}", a ); } void printok( int a ) { std::println( "{}", a ); } void raw() { rw::emitRaw( stdout, "plain" ); } +std::string localebad( int a, int b ) { return std::format( std::locale( "C" ), "{} {}", a, b ); } +std::string unrefpos( int a, int b ) { return std::format( "{0} {1}", a, b ); } +template std::string packexp( A&&... a ) { return std::format( "{}", std::forward( a )... ); } +std::string runtimefmt( int a ) { return std::format( std::runtime_format( "{}" ), a ) + fmt::format( fmt::runtime( "{}" ), a ); } +std::string pairmacro( int a, int b ) { return std::format( "{}", a + b ); } +std::string pricat( std::uint64_t a ) { return std::format( "{}", a ); } +std::string unqualified( int a ) { return format( "{}", a ); } +void ostreamprint( int a ) { std::print( std::cout, "{}", a ); } +std::string formattonok( int a, int b ) { char out[16]; std::format_to_n( out, 10, "{}", a + b ); return out; } +std::string posnested( const std::string& s, int w ) { return std::format( "{}", s ); } +std::string swapped( int a, int b ) { return std::format( "{} {}", a, b, b ) + std::format( "{}", a ); } +std::string moved( int a ) { return std::format( "m={}", a, a ); } std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } int drive() { return 0; } EOF } >"$WORK/fmt_cpp/src/fmt.cpp" +printf '#include \n#define LOGF( ... ) std::format( "{}", __VA_ARGS__ )\n' >"$WORK/fmt_cpp/src/vamacro.h" fx_commit fmt_cpp { printf '%s' "$FMT_HDR"; cat <<'EOF' std::string extra( int a, int b ) { return std::format( "a={} b={}", a, b, a + b ); } @@ -210,7 +258,7 @@ std::string dynwidthbad( const std::string& s, int w ) { return std::format( "{: void printstream( int a, int b ) { std::print( stderr, "{} {}\n", a ); } std::string fmtns( int a, int b ) { return fmt::format( "{} {}", a ); } std::string escaped( int a ) { return std::format( "{{literal}} {} }}{{", a ); } -std::string spec( int a, double d, int b ) { return std::format( "{:>8} {:.2f} {:#x}", a, d, b ); } +std::string spec( int a, double d, int b, std::chrono::sys_days t, const std::string& s ) { return std::format( "{:>8} {:.2f} {:#x} {:%Y-%m-%d} {:*^10}", a, d, b, t, s ); } std::string posok( int a, int b ) { return std::format( "{0} and {0} and {1}", a, b ); } std::string dynwidth( const std::string& s, int w, double d, int p ) { return std::format( "{:>{}} {:{}.{}f}", s, w, d, w, p ); } std::string nonliteral( int a, int b ) { return std::format( kFmt, a, b, a ) + std::vformat( kFmt, std::make_format_args( a, b, a ) ); } @@ -228,12 +276,25 @@ std::string commented( int a ) std::string charlit( int a ) { return std::format( "{}{}", '{', a ); } void printok( int a ) { std::println( "{} {}", a, a ); } void raw() { rw::emitRaw( stdout, "{} {} {}" ); } +std::string localebad( int a, int b ) { return std::format( std::locale( "C" ), "{} {}", a, b, a ); } +std::string unrefpos( int a, int b ) { return std::format( "{1}", a, b ); } +template std::string packexp( A&&... a ) { return std::format( "{} {}", std::forward( a )... ); } +std::string runtimefmt( int a ) { return std::format( std::runtime_format( "{} {}" ), a ) + fmt::format( fmt::runtime( "{} {}" ), a ); } +std::string pairmacro( int a, int b ) { return std::format( "{} {}", PAIR ); } +std::string pricat( std::uint64_t a ) { return std::format( "{} " PRIu64, a ); } +std::string unqualified( int a ) { return format( "{} {}", a ); } +void ostreamprint( int a ) { std::print( std::cout, "{} {}", a ); } +std::string formattonok( int a, int b ) { char out[16]; std::format_to_n( out, 10, "{} {}", a, b ); return out; } +std::string posnested( const std::string& s, int w ) { return std::format( "{0:>{1}}", s, w ); } +std::string swapped( int a, int b ) { return std::format( "{} {}", a, b ) + std::format( "{}", a, b ); } std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } std::string freshbad( int a ) { return std::format( "{}", a, a ); } std::string freshok( int a ) { return std::format( "{} {}", a, a ); } int drive() { return 1; } EOF } >"$WORK/fmt_cpp/src/fmt.cpp" +printf '#include \n#define LOGF( ... ) std::format( "{} {}", __VA_ARGS__ )\n' >"$WORK/fmt_cpp/src/vamacro.h" +printf '#include \n#include \nstd::string movedRenamed( int a ) { return std::format( "m={}", a, a ); }\n' >"$WORK/fmt_cpp/src/moved.cpp" fx_run fmt_cpp L="format-arity (c++)" ds_has "$L" format-arity extra gating @@ -245,9 +306,13 @@ ds_has "$L" format-arity positional gating ds_has "$L" format-arity dynwidthbad gating ds_has "$L" format-arity printstream gating ds_has "$L" format-arity fmtns gating +ds_has "$L" format-arity localebad gating +ds_has "$L" format-arity unrefpos gating +ds_has "$L" format-arity swapped gating ds_has "$L" format-arity freshbad gating-new ds_wasnow "$L" format-arity extra 0 1 ds_wasnow "$L" format-arity concat 0 1 +ds_wasnow "$L" format-arity swapped 1 1 ds_none "$L" escaped "{{ and }} are escapes, not fields" ds_none "$L" spec "a format spec is part of its field" ds_none "$L" posok "{0} twice and {1} need two arguments" @@ -265,7 +330,19 @@ ds_none "$L" printok "std::println with matching arguments" ds_none "$L" raw "emitRaw prints bytes, it does not format" ds_none "$L" legacy "a mismatch the change did not introduce (untouched function)" ds_none "$L" freshok "a new function with a matching call" -gating_split "$L" 10 +ds_none "$L" packexp "a pack expansion makes the argument count unknowable (skipped)" +ds_none "$L" runtimefmt "std::runtime_format / fmt::runtime wrap a non-literal format (skipped)" +ds_none "$L" pairmacro "an object-like macro argument may expand to a comma list (skipped)" +ds_none "$L" pricat "an adjacent-literal concat with a macro piece is not a whole literal (skipped)" +ds_none "$L" unqualified "an unqualified in-repo format() is not the format family" +ds_none "$L" ostreamprint "the std::print ostream overload is skipped, never guessed (stated floor)" +ds_none "$L" formattonok "std::format_to_n puts the format at index 2: two fields, two arguments" +ds_none "$L" posnested "a positional nested width {0:>{1}} references both arguments" +ds_none_p "$L" src/vamacro.h "a format inside a macro body (__VA_ARGS__) is skipped" +ds_none "$L" moved "a mismatch moved out of this file is not new" +ds_none "$L" movedRenamed "a mismatch moved to a new file and renamed is not new (its call text was already in the baseline)" +ds_none_p "$L" src/moved.cpp "the moved mismatch's new file carries no row" +gating_split "$L" 13 OFMT="$QD_OUT" fx_run fmt_cpp if [ "$QD_OK" = 1 ] && [ "$OFMT" = "$QD_OUT" ]; then ok "$L: delta byte-identical run-to-run"; else no "$L: non-deterministic delta"; fi @@ -274,11 +351,78 @@ if command -v xmllint >/dev/null 2>&1; then else echo " SKIP $L: xml well-formedness (xmllint not installed)" fi +# The exit predicate's own ack token: --ack-only=gating selects whatever would exit 2, and a new-symbol +# format-arity row (freshbad) DOES exit 2, so the ack must take it too — the ack token and the XML gating= +# must read one predicate. On a fresh copy, so the later arms see the unacked fixture. +L="format-arity ack gating" +if printf '%s' "$OFMT" | tr '>' '\n' | grep '/dev/null 2>&1 ) + ARC=$? + if [ "$ARC" = 0 ]; then ok "$L: --ack-only=gating wrote its acks (exit 0)"; else no "$L: --ack-only=gating exited $ARC"; fi + fx_run fmt_ackg + if [ "$QD_OK" = 1 ]; then + left="$( ds_rows | grep -c 'defect="format-arity"' )" + if [ "$left" = 0 ]; then ok "$L: no format-arity row survives --ack-only=gating (freshbad included)" + else no "$L: $left format-arity rows survived --ack-only=gating: $( ds_rows | grep 'defect="format-arity"' | head -3 )"; fi + gating_split "$L" 0 + fi +else + no "$L: premise: the unacked fixture has no format-arity row on freshbad, so there is nothing for the ack to select" +fi + +# ── 1c) FORMAT-ARITY across a CLEAN MERGE (ref-pair mode) — the train-25 incident ─────────────────────── +# Each side adds the same field literal and one argument; git merges the two cleanly into 5 fields and 6 +# arguments, which compiles. Each side alone matches. --quality-delta=BASE..HEAD takes the ref-pair snapshot +# path, which no other arm here exercises. +fx_init merge +mkdir -p "$WORK/merge/src" +MERGE_BASE_SRC='#include +namespace rw { template void emitTo( std::FILE* stream, const char* f, A&&... a ); } +void legend( int a, int b, int c, int d, int e, int f ) +{ + rw::emitTo( stdout, "\n", + a, + e, + f, + d ); +} +int drive() { return 0; } +' +printf '%s' "$MERGE_BASE_SRC" >"$WORK/merge/src/legend.cpp" +fx_commit merge +MB="$( cd "$WORK/merge" && git rev-parse HEAD )" +( cd "$WORK/merge" && git checkout -q -b side_x \ + && sed -i.bak -e 's|^ "x=\\"\\" "$|&\ + "n=\\"{}\\" "|' -e 's|^ a,$|&\ + b,|' src/legend.cpp && rm -f src/legend.cpp.bak && git commit -qam x \ + && git checkout -q "$MB" && git checkout -q -b side_y \ + && sed -i.bak -e 's|^ "x=\\"\\" "$|&\ + "n=\\"{}\\" "|' -e 's|^ f,$|&\ + c,|' src/legend.cpp && rm -f src/legend.cpp.bak && git commit -qam y \ + && git checkout -q side_x && git merge -q --no-edit side_y >/dev/null 2>&1 ) \ + || no "merge: building the two sides and their clean merge failed" +MF="$( grep -c '{}' "$WORK/merge/src/legend.cpp" )" +MA="$( grep -cE '^ [a-f],$|^ d \);$' "$WORK/merge/src/legend.cpp" )" +if [ "$MF" = 3 ] && [ "$MA" = 6 ]; then ok "merge: the clean merge holds the duplicated literal once and both arguments (premise)" +else no "merge: premise: expected 3 field lines and 6 argument lines after the merge, got $MF / $MA"; cat "$WORK/merge/src/legend.cpp"; fi +L="format-arity (merge, ref-pair)" +fx_run_ref merge "$MB..HEAD" +ds_has "$L" format-arity legend gating +gating_split "$L" 1 +fx_run_ref merge "$MB..side_x" +ds_none "$L side x" legend "one side alone: five fields, five arguments" +fx_run_ref merge "$MB..side_y" +ds_none "$L side y" legend "the other side alone: five fields, five arguments" # ── 1b) FORMAT-ARITY, Python "literal".format(...) ────────────────────────────────────────────────────── # Too few raises IndexError/KeyError at run time; too many is ignored silently. Named fields count against # keyword arguments, auto/positional fields against positional ones; *args / **kwargs make the count -# unknowable (skipped). f-strings, %-formatting and a non-literal receiver are out of the shape. +# unknowable (skipped). f-strings, %-formatting and a non-literal receiver are out of the shape. An explicit +# keyword the format never names is ignored silently (a row); a raw r"" literal and an unparenthesised +# implicit concat ("a" "b".format) are one literal like any other. fx_init fmt_py printf 'VERSION = "1"\nBANNER = "v{}".format(VERSION)\n' >"$WORK/fmt_py/banner.py" cat >"$WORK/fmt_py/fmt.py" <<'EOF' @@ -344,6 +488,26 @@ def triple(a, b): return """{} {}""".format(a, b) +def kwunused(x, y): + return "{x}".format(x=x) + + +def rawbad(a): + return r"{}".format(a) + + +def concatbare(a, b): + return "a={} " "b={}".format(a, b) + + +def nestedauto(s, w): + return "{}".format(s) + + +def fmtmap(d): + return "{a}".format_map(d) + + def legacy(a): return "{} {}".format(a, a, a) EOF @@ -413,6 +577,26 @@ def triple(a, b): {}""".format(a, b) +def kwunused(x, y): + return "{x}".format(x=x, y=y) + + +def rawbad(a): + return r"{} {}".format(a) + + +def concatbare(a, b): + return "a={} " "b={}".format(a) + + +def nestedauto(s, w): + return "{:>{}}".format(s, w) + + +def fmtmap(d): + return "{a} {b}".format_map(d) + + def legacy(a): return "{} {}".format(a, a, a) @@ -428,6 +612,9 @@ ds_has "$L" format-arity namedmissing gating ds_has "$L" format-arity concat gating ds_has "$L" format-arity freshbad gating-new ds_has_p "$L" format-arity banner.py gating +ds_has "$L" format-arity kwunused gating +ds_has "$L" format-arity rawbad gating +ds_has "$L" format-arity concatbare gating ds_none "$L" escaped "{{ }} are escapes" ds_none "$L" star "*args makes the count unknowable" ds_none "$L" kwstar "**kwargs makes the names unknowable" @@ -440,18 +627,24 @@ ds_none "$L" nonliteral "a non-literal receiver is skipped, never guessed" ds_none "$L" percent "%-formatting is not this shape" ds_none "$L" triple "a triple-quoted literal with two fields and two arguments" ds_none "$L" legacy "a mismatch the change did not introduce" -gating_split "$L" 6 +ds_none "$L" nestedauto "an auto nested width {:>{}} consumes a positional argument" +ds_none "$L" fmtmap ".format_map takes a mapping, not an argument list (skipped)" +gating_split "$L" 9 # ── 2) UTF8-CUT, C++ (report-only) ────────────────────────────────────────────────────────────────────── # The corpus shapes: a cut (resize / substr(0,N) / erase(N)) plus an appended ellipsis on the SAME string # with no UTF-8 back-off in the function, and an ellipsis appended once a byte-pushing loop reaches the # cap (the cut lands mid-sequence and the ellipsis can fire with nothing dropped). Near-misses: a # continuation-byte back-off (0xC0/0x80), a utf8-named helper, a cut with no ellipsis (a buffer, not text — -# a stated floor), an ellipsis on a different variable, an ellipsis that replaces the value, a u32string. +# a stated floor), an ellipsis on a different variable, an ellipsis that replaces the value, a u32string, +# a cut at a parsed delimiter (the cut length must be the bound of a size comparison on the same string, or +# a cap-named constant/parameter), and a loop that appends whole words. A back-off guard anywhere in the +# function exempts it: a precision choice, stated in the legend. fx_init utf8 mkdir -p "$WORK/utf8/src" U8_HDR='#include #include +#include constexpr std::size_t kCap = 80; std::string truncateUtf8WithEllipsis( std::string_view s, std::size_t cap ); @@ -474,7 +667,8 @@ std::string helper( const std::string& s ) { return s; } std::string noellipsis( std::string s, std::size_t n ) { return s; } std::string othervar( std::string buf, std::string label, std::size_t n ) { return buf + label; } std::string replaced( const std::string& s, std::size_t n ) { return s; } -std::string loading( std::string out ) { return out; } +std::string packname( const std::string& text, bool pack ) { return text; } +std::string wordloop( const std::vector& words ) { std::string out; return out; } std::u32string wide( std::u32string s, std::size_t n ) { return s; } std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } int drive() { return 0; } @@ -545,7 +739,29 @@ std::string othervar( std::string buf, std::string label, std::size_t n ) return buf + label; } std::string replaced( const std::string& s, std::size_t n ) { return s.size() > n ? std::string( "..." ) : s; } -std::string loading( std::string out ) { out += "loading..."; return out; } +std::string packname( const std::string& text, bool pack ) +{ + std::string n = text.substr( 0, text.find( '=' ) ); + if( pack ) + { + n += "..."; + } + return n; +} +std::string wordloop( const std::vector& words ) +{ + std::string out; + for( const std::string& w : words ) + { + out += w; + if( out.size() >= kCap ) + { + out += "\xE2\x80\xA6"; + break; + } + } + return out; +} std::u32string wide( std::u32string s, std::size_t n ) { if( s.size() > n ) @@ -573,7 +789,8 @@ ds_none "$L" helper "a utf8 truncation helper does the cut" ds_none "$L" noellipsis "a cut with no ellipsis is not judged display text (stated floor)" ds_none "$L" othervar "the ellipsis goes on a different string than the one cut" ds_none "$L" replaced "the ellipsis replaces the value, nothing is cut" -ds_none "$L" loading "a literal that merely ends in ... with no cap" +ds_none "$L" packname "the cut is at a parsed ASCII delimiter, not a size cap, and ... is pack syntax (src/ingest_names.h cppTemplateParameterName)" +ds_none "$L" wordloop "whole strings are appended, so no byte sequence is split (the byte-push rule needs a single-char push)" ds_none "$L" wide "a u32string holds code points, a cut cannot split one" ds_none "$L" legacy "a cut the change did not introduce" gating_split "$L" 0 @@ -582,7 +799,9 @@ gating_split "$L" 0 # The corpus shape (a two-pass merge, sorted on (line, rule), uniqued keeping the first) plus its ranges and # unsorted siblings. Near-misses: the sort orders on severity (worst first), the predicate compares severity, # a type with no severity-like field, a field whose name only CONTAINS a severity word (indentLevel — a -# bounded token match), a predicate-less unique, a unique over ints. +# bounded token match), a predicate-less unique, a unique over ints, a heading level (bare level is not a +# severity token: severity-like = sev / severity / priority / prio), and a NAMED predicate (skipped). +# Stated floor: a sort that reads severity in the mild-first direction is not caught. fx_init dedup mkdir -p "$WORK/dedup/src" DD_HDR='#include @@ -608,6 +827,12 @@ struct Node std::string rule; int indentLevel = 0; }; +struct Heading +{ + int line = 0; + int level = 0; +}; +bool sameLineSev( const Finding& a, const Finding& b ); ' { printf '%s' "$DD_HDR"; cat <<'EOF' void mergeFirst( std::vector& v ) { (void)v; } @@ -619,6 +844,8 @@ void noSev( std::vector& v ) { (void)v; } void indent( std::vector& v ) { (void)v; } void defaultEq( std::vector& v ) { (void)v; } void ints( std::vector& v ) { (void)v; } +void headings( std::vector& v ) { (void)v; } +void namedPred( std::vector& v ) { (void)v; } void legacy( std::vector& v ) { v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); @@ -674,6 +901,15 @@ void ints( std::vector& v ) { v.erase( std::unique( v.begin(), v.end(), []( int a, int b ) { return a / 10 == b / 10; } ), v.end() ); } +void headings( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), []( const Heading& a, const Heading& b ) { return a.line == b.line; } ), v.end() ); +} +bool sameLineSev( const Finding& a, const Finding& b ) { return a.line == b.line && a.sev == b.sev; } +void namedPred( std::vector& v ) +{ + v.erase( std::unique( v.begin(), v.end(), sameLineSev ), v.end() ); +} void legacy( std::vector& v ) { v.erase( std::unique( v.begin(), v.end(), []( const Finding& a, const Finding& b ) { return a.line == b.line; } ), v.end() ); @@ -697,6 +933,8 @@ ds_none "$L" noSev "the element type has no severity-like field" ds_none "$L" indent "indentLevel only contains a severity word (bounded token match)" ds_none "$L" defaultEq "a predicate-less unique compares whole values" ds_none "$L" ints "a unique over ints carries no severity" +ds_none "$L" headings "a heading/nesting level is not a severity (bare level, risk and grade are not severity tokens)" +ds_none "$L" namedPred "a named predicate function is skipped, never guessed" ds_none "$L" legacy "a keep-first the change did not introduce" gating_split "$L" 0 @@ -706,10 +944,13 @@ gating_split "$L" 0 # appends the arm. Positives: an absence assertion read off an unchecked $( ) capture, off a direct pipe # (with or without pipefail — under pipefail a crash makes the pipeline false, which takes the same PASS # branch), the if/else spelling, backslash continuations, the !-negated pass spelling, other reporter -# names, inside a function, and a brand-new script. Near-misses: the capture's rc checked, `|| no` on the -# capture, a presence check, a positive assertion on the same output first, the match-is-pass polarity, -# a literal producer, a commented-out shape, a pre-existing vacuous line, a sound if, and the same shape in -# a script outside a test path. +# names, inside a function, and a brand-new script; under set -e a `local x="$( )"` capture (local masks the +# rc) and a direct pipe (left of &&, where errexit is suspended) stay positive. Near-misses: the capture's rc +# checked, `|| no` on the capture, a presence check, a positive assertion on the same output before OR after +# the absence, an enclosing if/case that requires content of the same capture, a plain capture under set -e +# (errexit aborts on its failure), the match-is-pass polarity, a literal producer, a commented-out shape, a +# pre-existing vacuous line, a sound if, a new function with a handled capture, and the same shape in a +# script outside a test path. fx_init vac mkdir -p "$WORK/vac/test" "$WORK/vac/scripts" VHDR="$( cat <<'EOF' @@ -724,9 +965,18 @@ EOF )" VHDR="$VHDR " -for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok; do +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn t_setelocal t_setepipe n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok n_posafter n_nestif n_case n_sete n_seteuo; do printf '%s' "$VHDR" >"$WORK/vac/test/$f.sh" done +for f in t_setepipe n_sete; do printf 'set -e\n' >>"$WORK/vac/test/$f.sh"; done +printf 'set -euo pipefail\n' >>"$WORK/vac/test/n_seteuo.sh" +cat >>"$WORK/vac/test/t_setelocal.sh" <<'EOF' +set -e +chk_local(){ + ok "placeholder" +} +chk_local +EOF printf '%s' "$VHDR" >"$WORK/vac/scripts/build.sh" cat >>"$WORK/vac/test/n_legacy.sh" <<'EOF' OLD="$( "$BIN" --old 2>/dev/null )" @@ -838,11 +1088,62 @@ cat >>"$WORK/vac/scripts/build.sh" <<'EOF' OUT="$( "$BIN" --list 2>/dev/null )" printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" EOF +cat >>"$WORK/vac/test/t_fn.sh" <<'EOF' +absent_checked(){ + local out + out="$( "$BIN" --list 2>/dev/null )" || { no "the list run failed"; return; } + printf '%s' "$out" | grep -q "$1" && no "$1 present" || ok "$1 absent" +} +absent_checked xxx +EOF +{ printf '%s' "$VHDR"; cat <<'EOF' +set -e +chk_local(){ + local x="$( "$BIN" --list 2>/dev/null )" + printf '%s' "$x" | grep -q 'bad' && no "bad present" || ok "bad absent" +} +chk_local +EOF +} >"$WORK/vac/test/t_setelocal.sh" +cat >>"$WORK/vac/test/t_setepipe.sh" <<'EOF' +"$BIN" --list 2>/dev/null | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_posafter.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +printf '%s' "$OUT" | grep -q '>"$WORK/vac/test/n_nestif.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +if printf '%s' "$OUT" | grep -q '>"$WORK/vac/test/n_case.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +case "$OUT" in + '>"$WORK/vac/test/$f.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +done fx_run vac L="vacuous-assert (bash)" -for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names; do +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_setepipe; do ds_has_p "$L" vacuous-assert "test/$f.sh" minor done +ds_has "$L" vacuous-assert chk_local minor ds_has "$L" vacuous-assert absent_unchecked new-symbol ds_has_p "$L" vacuous-assert test/t_new.sh new-symbol ds_none "$L" absent "the capture's failure is handled (|| { no; return; }) — and the function is untouched" @@ -856,54 +1157,132 @@ ds_none_p "$L" test/n_comment.sh "a commented-out shape is not code" ds_none_p "$L" test/n_legacy.sh "a vacuous line the change did not introduce" ds_none_p "$L" test/n_ifok.sh "the if checks the rc in its condition" ds_none_p "$L" scripts/build.sh "not a test-script path: the kind is scoped to gate scripts" +ds_none "$L" absent_checked "a NEW function whose capture failure is handled (|| { no; return; })" +ds_none_p "$L" test/n_posafter.sh "a positive assertion on the same capture AFTER the absence fails on a crash" +ds_none_p "$L" test/n_nestif.sh "the absence is nested in an if that requires content of the same capture" +ds_none_p "$L" test/n_case.sh "the absence is nested in a case arm that requires content of the same capture" +ds_none_p "$L" test/n_sete.sh "set -e: a failed plain capture aborts the script" +ds_none_p "$L" test/n_seteuo.sh "set -euo pipefail: a failed plain capture aborts the script" gating_split "$L" 0 -# ── 5) SURFACES: legend, JSON twin, MCP, and the ack identity of a facet ───────────────────────────────── +# ── 5) SURFACES: legend, help, MCP, skill and docs; JSON twin; MCP; the ack identity of a facet ─────────── +# D2: format-arity is the one exception to "new-symbol rows never gate", and every surface that states the +# rule states the exception in ONE fixed phrase (PHRASE below), so no surface still reads unqualified. L="surfaces" +PHRASE='new-symbol rows never gate, except defect-shape format-arity' +phrase_ok(){ # LABEL TEXT — the text states the exception, and every "new-symbol rows never gate" in it carries it + local all qual + all="$( printf '%s' "$2" | grep -o 'new-symbol rows never gate' | wc -l | tr -d ' ' )" + qual="$( printf '%s' "$2" | grep -o "$PHRASE" | wc -l | tr -d ' ' )" + if [ "$qual" -ge 1 ] && [ "$all" = "$qual" ]; then ok "$1: states '$PHRASE' ($qual), no unqualified 'new-symbol rows never gate'" + else no "$1: '$PHRASE' x$qual, 'new-symbol rows never gate' x$all — the exception is missing or a sentence still reads unqualified"; fi +} LEG="$( cd "$WORK/fmt_cpp" && "$BIN" . --quality-delta --no-cache --legend=full 2>/dev/null )" if printf '%s' "$LEG" | grep -q '}"; fi + if printf '%s' "$LEG" | grep -q 'report-only by facet, not by size'; then ok "$L: the legend says the report-only facets are sev=minor by facet, not by size (D3)" + else no "$L: the legend does not say 'report-only by facet, not by size' — sev=minor is otherwise defined as a small numeric delta"; fi + if printf '%s' "$LEG" | grep -q 'f-strings are not checked'; then ok "$L: the legend names the Python formats it does not check" + else no "$L: the legend does not say '%-formatting and f-strings are not checked' (no row is not a verdict on them)"; fi else no "$L: the --legend=full delta run produced no /dev/null )" +if printf '%s' "$CLEG" | grep -q '&1 )"; HRC=$? +if [ "$HRC" = 0 ] && printf '%s' "$HELP" | grep -q -- '--quality-delta'; then + if printf '%s' "$HELP" | grep -q 'across 11 kinds'; then no "$L: --help still says 'across 11 kinds'"; else ok "$L: --help no longer says 'across 11 kinds'"; fi + if printf '%s' "$HELP" | grep -q 'defect-shape'; then ok "$L: --help names defect-shape"; else no "$L: --help does not name defect-shape"; fi + phrase_ok "$L --help" "$HELP" +else + no "$L: --help failed (rc=$HRC) or does not list --quality-delta" +fi if command -v python3 >/dev/null 2>&1; then - JS="$( cd "$WORK/fmt_cpp" && "$BIN" . --quality-delta --no-cache --json 2>/dev/null )" - JV="$( printf '%s' "$JS" | python3 -c ' + TL="$( printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ + | "$BIN" --mcp 2>/dev/null | tail -1 )" + QDD="$( printf '%s' "$TL" | python3 -c ' import json, sys -try: d = json.load(sys.stdin) +try: r = json.load(sys.stdin) except Exception as e: print("BROKEN %s" % e); raise SystemExit -rows = [r for r in d.get("r", []) if r.get("kind") == "defect-shape" and r.get("defect") == "format-arity"] -print("ROWS %d" % len(rows))' )" - case "$JV" in ROWS\ *) n="${JV#ROWS }"; [ "$n" -ge 10 ] && ok "$L: --json carries the $n format-arity rows (kind + defect keys)" \ - || no "$L: --json carries $n format-arity rows, want >= 10" ;; - *) no "$L: --json probe: $JV" ;; esac - MC="$( printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ - '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"quality_delta","arguments":{"path":"'"$WORK/fmt_cpp"'"}}}' \ - | "$BIN" --mcp 2>/dev/null | tail -1 )" - MV="$( printf '%s' "$MC" | python3 -c ' +d = [t.get("description", "") for t in r.get("result", {}).get("tools", []) if t.get("name") == "quality_delta"] +print(d[0] if d else "MISSING")' )" + case "$QDD" in + BROKEN*|MISSING) no "$L: MCP tools/list has no quality_delta description: $QDD" ;; + *) if printf '%s' "$QDD" | grep -q '11 measured failure modes'; then no "$L: the MCP quality_delta description still says '11 measured failure modes'" + else ok "$L: the MCP quality_delta description has no stale kind count"; fi + if printf '%s' "$QDD" | grep -q 'defect-shape'; then ok "$L: the MCP quality_delta description names defect-shape"; else no "$L: the MCP quality_delta description does not name defect-shape"; fi + phrase_ok "$L MCP description" "$QDD" ;; + esac +else + echo " SKIP $L: MCP tools/list description (python3 not installed)" +fi +SKILL="$ROOT/skills/ripwire-quality-bar/SKILL.md" +for doc in "$SKILL" "$ROOT/docs/COMMANDS.md" "$ROOT/README.md"; do + rel="${doc#"$ROOT"/}" + if [ ! -f "$doc" ]; then no "$L: $rel is missing"; continue; fi + if grep -qiE '(^|[^0-9])11 kinds|eleven kinds|eleven quality kinds|ELEVEN KINDS' "$doc"; then no "$L: $rel still says 11/eleven kinds: $( grep -niE '(^|[^0-9])11 kinds|eleven kinds|eleven quality kinds' "$doc" | head -2 )" + else ok "$L: $rel has no stale 11/eleven-kinds count"; fi + phrase_ok "$L $rel" "$( cat "$doc" )" +done +if [ -f "$SKILL" ]; then + FM="$( awk 'NR==1 && /^---$/ {f=1; next} f && /^---$/ {exit} f' "$SKILL" )" + if printf '%s' "$FM" | tr '\n' ' ' | tr -s ' ' | grep -q "$PHRASE"; then ok "$L: the quality-bar skill's trigger text states the format-arity exception" + else no "$L: the quality-bar skill's trigger text (frontmatter description) does not state '$PHRASE'"; fi +fi +if command -v python3 >/dev/null 2>&1; then + # D2 at every exit-predicate site: the new-symbol freshbad row carries "gating": true, and the top-level + # "gating" equals the rows that carry it (the same predicate as the XML gating= and the exit code). + JPROBE=' import json, sys try: - r = json.load(sys.stdin) - t = r["result"]["content"][0]["text"] - d = json.loads(t) + raw = sys.stdin.read() + d = json.loads(raw) + if "result" in d: d = json.loads(d["result"]["content"][0]["text"]) except Exception as e: print("BROKEN %s" % e); raise SystemExit -rows = [x for x in d.get("r", []) if x.get("kind") == "defect-shape" and x.get("defect") == "format-arity"] -print("ROWS %d" % len(rows))' )" - case "$MV" in ROWS\ *) n="${MV#ROWS }"; [ "$n" -ge 10 ] && ok "$L: MCP quality_delta carries the $n format-arity rows" \ - || no "$L: MCP quality_delta carries $n format-arity rows, want >= 10" ;; - *) no "$L: MCP quality_delta probe: $MV" ;; esac +rs = d.get("r", []) +rows = [r for r in rs if r.get("kind") == "defect-shape" and r.get("defect") == "format-arity"] +fresh = [r for r in rows if r.get("sym") == "freshbad"] +ng = sum(1 for r in rs if r.get("gating") is True) +print("ROWS %d FRESH %d FRESHGATE %d TOP %s NG %d" % (len(rows), len(fresh), sum(1 for r in fresh if r.get("gating") is True), d.get("gating"), ng))' + JS="$( cd "$WORK/fmt_cpp" && "$BIN" . --quality-delta --no-cache --json 2>/dev/null )" + MC="$( printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"quality_delta","arguments":{"path":"'"$WORK/fmt_cpp"'"}}}' \ + | "$BIN" --mcp 2>/dev/null | tail -1 )" + for surf in json mcp; do + if [ "$surf" = json ]; then V="$( printf '%s' "$JS" | python3 -c "$JPROBE" )"; else V="$( printf '%s' "$MC" | python3 -c "$JPROBE" )"; fi + set -- $V + if [ "${1:-}" != ROWS ]; then no "$L $surf: probe: $V"; continue; fi + n="$2"; fr="$4"; frg="$6"; top="$8"; ng="${10}" + case "$n$fr$frg$ng" in *[!0-9]*) no "$L $surf: probe printed non-numbers: $V"; continue ;; esac + if [ "$n" -ge 13 ]; then ok "$L $surf: carries the $n format-arity rows (kind + defect keys)"; else no "$L $surf: carries $n format-arity rows, want >= 13"; fi + if [ "$fr" = 1 ] && [ "$frg" = 1 ]; then ok "$L $surf: the new-symbol freshbad format-arity row carries \"gating\": true" + else no "$L $surf: freshbad rows=$fr gating=$frg (want 1 and 1)"; fi + if [ "$top" = "$ng" ]; then ok "$L $surf: top-level \"gating\" ($top) equals the rows that carry \"gating\": true" + else no "$L $surf: top-level \"gating\" is $top but $ng rows carry \"gating\": true"; fi + done else echo " SKIP $L: --json and MCP row checks (python3 not installed)" fi # Ack identity (checklist 16): one symbol with BOTH a format-arity and a utf8-cut row. Acking only the # format-arity facet must leave the utf8-cut row on the same symbol standing — a facet is part of the -# finding's identity, not a label on one shared per-symbol key. +# finding's identity, not a label on one shared per-symbol key. --ack-only=defect-shape (the kind) takes +# both facets; each on a fresh copy. fx_init both mkdir -p "$WORK/both/src" cat >"$WORK/both/src/b.cpp" <<'EOF' @@ -930,15 +1309,26 @@ EOF fx_run both ds_has "$L ack" format-arity label gating ds_has "$L ack" utf8-cut label minor +fx_copy both both_kind ( cd "$WORK/both" && "$BIN" . --quality-delta --no-cache --ack-only=format-arity --quality-ack="deliberate in this fixture" >/dev/null 2>&1 ) ARC=$? if [ "$ARC" = 0 ]; then ok "$L ack: --ack-only=format-arity selects the facet (exit 0)"; else no "$L ack: --ack-only=format-arity exited $ARC"; fi fx_run both if [ "$QD_OK" = 1 ]; then if [ -z "$( ds_row format-arity label )" ]; then ok "$L ack: the acked format-arity row is suppressed"; else no "$L ack: the format-arity row survived its ack: $( ds_row format-arity label )"; fi + if [ -z "$( ds_rows | grep 'gating=' )" ]; then ok "$L ack: no defect-shape row carries gating= after the ack"; else no "$L ack: a defect-shape row still gates: $( ds_rows | grep 'gating=' | head -2 )"; fi fi ds_has "$L ack" utf8-cut label minor -if [ "$QD_RC" = 0 ]; then ok "$L ack: with format-arity acked the run exits 0"; else no "$L ack: should exit 0 after the ack (got $QD_RC)"; fi +gating_split "$L ack" 0 +( cd "$WORK/both_kind" && "$BIN" . --quality-delta --no-cache --ack-only=defect-shape --quality-ack="deliberate in this fixture" >/dev/null 2>&1 ) +ARC=$? +if [ "$ARC" = 0 ]; then ok "$L ack kind: --ack-only=defect-shape selects the kind (exit 0)"; else no "$L ack kind: --ack-only=defect-shape exited $ARC"; fi +fx_run both_kind +if [ "$QD_OK" = 1 ]; then + left="$( ds_rows | wc -l | tr -d ' ' )" + if [ "$left" = 0 ]; then ok "$L ack kind: both facets are suppressed by the kind token"; else no "$L ack kind: $left defect-shape rows survived --ack-only=defect-shape: $( ds_rows | head -2 )"; fi +fi +gating_split "$L ack kind" 0 [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 7acae5a0351533145a49f09de7aa9e107c054dfc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:06:31 -0400 Subject: [PATCH 130/229] value references: --uses and --path disclose the depth cut (CLI and MCP) --callers named the 512-level cut with value_refs_depth_capped= and value_refs_depth_at=, and its next= sent the reader to --uses, whose count carried no such mark: the use below the cap was missing and the count read as complete. --uses now carries both attributes and the legend clause on its root, and --path carries them beside to_value_refs= (only when no directed path exists, where it reads value references), on the CLI and the MCP uses and path_between twins. They come from the same ValueRefIndex::depthCutsFor and valueRefsDepthAttrXml/valueRefsDepthLegend that --callers uses; collectUseSites takes the verb's own index so the cut is read off the index the rows come from, and toValueRefsCount returns the count with its cut (toValueRefs). An answer over a tree with no cut is byte-identical. recallshapecheck RZ10-RZ17: the callers/uses/path shape on one fixture (a shallow store plus a 300-link chain), the MCP twins, a CLI/MCP byte-equality arm, and near misses (200 levels, another language family, a path that exists). RZ10-RZ14 are red on the previous head and on the base binary. Wording: the CHANGELOG and ARCHITECTURE sentence "every answer that reads value references" names its surfaces and the one named exception (--quality-delta's dead kind and the --quality overview); the --callees "one row" sentence is per binding site and written callee, with the tbl.k() / tbl["k"]() double spelling named as a floor; the compact value-ref-excluded definition says "in another function or at file scope"; the depth legend says "this language family (every family on the dead-code list)". The legend dictionary moves to dictv=cc0b20902eeb3e68 (entries 786). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 17 +++--- docs/ARCHITECTURE.md | 4 +- docs/COMMANDS.md | 2 +- docs/captures/COMMANDS_showcase_2026-09-14.md | 2 +- src/compactlegend.h | 4 +- src/graphlegend.h | 2 +- src/ingest_valuerefs.h | 3 +- src/mcpverbs.h | 16 ++++-- src/valuerefindex.h | 22 ++++--- src/verbs_navigate.h | 29 ++++++---- test/recallshapecheck.sh | 57 ++++++++++++++++++- 11 files changed, 119 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3579d6e1..f2c5d8341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,15 +95,18 @@ call-shaped references, so on such a function `--callers`, `--callees`, `--impac argument or a CommonJS `module.exports` table holds. - **Depth.** The capture does not descend past 512 levels of nesting (a 300-link C `else if` chain reaches it). That cut is disclosed, never silent: every answer that reads value references for the cut file's language — the - callers/callees roots, `--impact`, `--safe-delete`, `--dead-code`, `--json` and the MCP twins — carries - `value_refs_depth_capped=N` (files cut) and `value_refs_depth_at=FILE:LINE` (the first cut), so a missing row or a - listed dead function there reads as a floor. + callers/callees roots, `--impact`, `--safe-delete`, `--dead-code`, `--uses`, `--path` (beside `to_value_refs=`), + `--json` and the MCP twins — carries `value_refs_depth_capped=N` (files cut) and `value_refs_depth_at=FILE:LINE` + (the first cut), so a missing row, a `count=` or a listed dead function there reads as a floor. The one exception, + named: `--quality-delta`'s dead-code kind and the `--quality` overview's `value-ref-excluded=` read the rows and do + not yet carry it. An answer over a tree with no cut is byte-identical. - **Second review.** A parameter of a prototype, a function-pointer typedef or parameter, a C++ member declaration, a Go func type or interface method, or a TS function type, `declare function` or interface signature no longer hides a same-named function in the enclosing scope (`signal( SIGINT, handler )` beside `typedef void (*fn_t)( int handler )` was no row). A function that only stores ITSELF (`timer_set( tick )` inside tick) is no longer kept off `--dead-code` - or `dead_code_candidate`, as a recursive self-call is no caller. `--callees` rows are per binding site: two calls - through one slot are one row, and `sites=` counts binding sites, not calls. A C `[expr]` designator, a numeric key + or `dead_code_candidate`, as a recursive self-call is no caller. `--callees` rows are per binding site and written + callee: two calls `tbl.k(); tbl.k();` through one slot are one row, and `sites=` counts binding sites, not calls. A + known floor: one slot called under two spellings (`tbl.k()` and `tbl["k"]()`) is two written callees, so two rows. A C `[expr]` designator, a numeric key and an identifier key are capped like every other written slot. - **Cost.** A cold default map costs +3.6% CPU on this tree, +6.3% on django and +5.2% on webpack (median of 5, `sim/refval_cpu.sh`). @@ -111,7 +114,7 @@ call-shaped references, so on such a function `--callers`, `--callees`, `--impac - **Manifest.** The `tools/list` manifest grows 46,591 → 46,732 B: the two find descriptions name `valueRefs` as not a proven call. -Gate: `test/recallshapecheck.sh`. It has 177 arms across C, C++, JS, JSX, TS, TSX, Python and Go: +Gate: `test/recallshapecheck.sh`. It has 201 CLI arms (plus the parity checks) across C, C++, JS, JSX, TS, TSX, Python and Go: - positives; - near-miss negatives for every guard, each proven able to fail by a mutation (`sim/refval_mutate.sh`); - named floors; @@ -535,7 +538,7 @@ value-reference fixes take 145, above 143's full-use tag 144), `kCacheVersion` 2 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions held as values, then on a function that only stores itself). Every ingest cache written by an earlier build is refused and re-indexed once, and every cached quality snapshot is recomputed. The session legend dictionary is -`dictv=beedfcd755c9457f entries=786`. +`dictv=cc0b20902eeb3e68 entries=786`. ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8cc741b10..10508ea26 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -381,7 +381,9 @@ A row is a clue to follow, not a proven call; `--dead-code` and `--safe-delete` a reason a function is not dead (one made by another function or at file scope: a function that only stores itself stays dead, as a recursive self-call is no caller). The capture stops at 512 levels of nesting; a file it cut carries one depth-cut record, and every answer that reads value references for -that file's language says so (`value_refs_depth_capped=`, `value_refs_depth_at=`). +that file's language says so (`value_refs_depth_capped=`, `value_refs_depth_at=`): the callers/callees roots, +`--impact`, `--safe-delete`, `--dead-code`, `--uses`, `--path`, the JSON form and the MCP twins. The one exception is +`--quality-delta`'s dead-code kind and the `--quality` overview's `value-ref-excluded=`, which do not yet say so. ### graph — the CSR diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 921511641..d740a8f31 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4398,7 +4398,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=beedfcd755c9457f entries=786 +ripwire legend dictionary ripwire.dict/v1 dictv=cc0b20902eeb3e68 entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 6ae2466b8..75609f195 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -5351,7 +5351,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=beedfcd755c9457f entries=786 +ripwire legend dictionary ripwire.dict/v1 dictv=cc0b20902eeb3e68 entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/src/compactlegend.h b/src/compactlegend.h index 45e27512a..6da0c57ab 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -337,9 +337,9 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "to", "vr to= def=: the function used as a value and its definition; sites=N binding sites one to=/through= pair joins", false, "vr" }, { "through", "through=: the written callee this function may call it through", false, "vr" }, { "to_value_refs", "to_value_refs=N: to= is used as a value N times (matched by name); a run through such a slot is not a proven call and no hop here" }, - { "value-ref-excluded", "value-ref-excluded=N: internal functions kept off because another function's table, field or argument holds them (matched by name, not a proven call); a floor" }, + { "value-ref-excluded", "value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor" }, // The value walk's depth cut (ingest_valuerefs.h kVrMaxDepth, graphlegend.h kValueRefsDepthLegend): absent when none. - { "value_refs_depth_capped", "value_refs_depth_capped=N: N files nest past the value walk's 512-level cap; a value use below it is unseen, so value_refs= and every reading on it are floors" }, + { "value_refs_depth_capped", "value_refs_depth_capped=N: N files of this language family (every family on the dead-code list) nest past the value walk's 512-level cap; a value use below it is unseen, so value_refs= and every reading on it are floors" }, { "value_refs_depth_at", "value_refs_depth_at=: the first such cut, file:line; read the code there" }, { "role", "u role=value: the function is used as a VALUE there (stored or passed), matched by name; not a proven call", false, "u", MapHeaderRead::No, "value" }, // #220 part 1: the FILE graph's gauge (graphlegend.h importsUnresolvedAttrXml), absent at zero, on the --deps/--arch/ diff --git a/src/graphlegend.h b/src/graphlegend.h index 3e9bd0046..673b3f3d6 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -626,7 +626,7 @@ inline constexpr const char* kUsesValueRoleLegend = // The value walk's depth cut (ingest_valuerefs.h kVrMaxDepth, valuerefs.h valueRefsDepthAttrXml): rides EVERY answer // that carries value_refs_depth_capped=, whether value_refs= is there or not — a zero beside it is the floor it names. inline constexpr const char* kValueRefsDepthLegend = - "value_refs_depth_capped=N (absent when 0): N indexed files of this language nest deeper than the value-reference walk's 512-level cap (a long else-if chain, a deep expression), and a function used as a value below that depth is not seen: value_refs= (absent or not) is a floor there, and a reading built on it (dead_code_candidate=, risk=none-found, the dead-code list) may miss a table that holds the function. value_refs_depth_at= is the first cut, file:line: read the code there for value uses. "; + "value_refs_depth_capped=N (absent when 0): N indexed files of this language family (every family on the dead-code list) nest deeper than the value-reference walk's 512-level cap (a long else-if chain, a deep expression), and a function used as a value below that depth is not seen: value_refs= (absent or not) is a floor there, and a reading built on it (dead_code_candidate=, risk=none-found, the dead-code list) may miss a table that holds the function. value_refs_depth_at= is the first cut, file:line: read the code there for value uses. "; inline const char* valueRefsDepthLegend( bool on ) noexcept { return on ? kValueRefsDepthLegend : ""; } inline const char* usesValueRoleLegend( bool on ) noexcept { return on ? kUsesValueRoleLegend : ""; } inline const char* valueRefsLegend( bool on, bool callersSide ) noexcept diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index 1ec0aa258..5964bfd6e 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -37,7 +37,8 @@ // Depth cut (role Value, at most one per file, after its rows): name="" (no identifier is empty); qualifier="d" // (model.h kValueRefDepthCutScope); argCount=the subtrees kVrMaxDepth stopped the walk above (saturating); // startByte/line=the first one. Not a row: valuerefindex.h counts it, and every answer that reads value -// references discloses it (value_refs_depth_capped= / value_refs_depth_at=). +// references discloses it (value_refs_depth_capped= / value_refs_depth_at=), except --quality-delta's dead kind +// and the --quality overview (a named deferral). // A local/file Through survives only when the same scope fed that container a function value (the filter at // scope exit / file end), so an ordinary `x.m()` adds nothing; a parameter Through always survives — its // values arrive from callers elsewhere. diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 9751b7b1a..cef97af52 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3017,6 +3017,7 @@ inline std::optional usesText( const std::string& root, const std:: std::vector elixirDefs = resolveAllByNameQualified( ing, symbol ); std::erase_if( elixirDefs, [ & ]( NodeId node ) { return ing.symbols[ node ].lang != Lang::Elixir; } ); const UsesValueFilter valueFilter( ing, sym, defs, &valueRefIndexOf( ix ) ); // the CLI --uses' value-site filter, shared (valuerefs.h) + const ValueRefIndex::DepthCuts usDepthCut = valueRefIndexOf( ix ).depthCutsFor( defs ); // the CLI --uses' depth cut, off the same index for( std::uint32_t refIndex = 0; refIndex < ing.references.size(); ++refIndex ) { const Reference& r = ing.references[refIndex]; @@ -3085,7 +3086,8 @@ inline std::optional usesText( const std::string& root, const std:: "external=\"1\" means SYM has no definition in the indexed tree under ANY spelling (stdlib/third-party); " "qualified file:name and \"::\" spellings whose bare name IS defined refuse instead (the CLI uses verb narrows them). " "{}{}{}-->{}", kUsesLegendOpen, - usesValueRoleLegend( std::any_of( sites.begin(), sites.end(), []( const UseSite& u ) { return u.role == RefRole::Value; } ) ), + ( std::string( usesValueRoleLegend( std::any_of( sites.begin(), sites.end(), []( const UseSite& u ) { return u.role == RefRole::Value; } ) ) ) + + valueRefsDepthLegend( usDepthCut.files > 0 ) ).c_str(), // exactly when the root carries the depth disclosure, as the CLI capLegendClause( computePageDisclosure( upageRows, sites.size(), upw.end, page.limit, page.offset, usDiscloseCap ).active ), graphCountDisclosure( rw::graphGaugeClauses( ix.g ) ).c_str(), @@ -3097,8 +3099,9 @@ inline std::optional usesText( const std::string& root, const std:: const bool usSingleRoot = ing.realPaths.empty(); const std::string usRootPrefix = usSingleRoot ? sarif::rootPrefixOf( root ) : std::string(); const std::string usRootAttr = usSingleRoot ? ( " root=\"" + ex( root ) + "\"" ) : std::string(); - rw::emitTo( mem, "", - ex( symbol ).c_str(), defs.size(), external ? 1 : 0, sites.size(), usRootAttr.c_str(), upage, graphCountFloorAttrXml( ix.g ).c_str() ); // of= echoes the selector as TYPED (an @-seed stays an @-seed) + rw::emitTo( mem, "", + ex( symbol ).c_str(), defs.size(), external ? 1 : 0, sites.size(), usRootAttr.c_str(), upage, graphCountFloorAttrXml( ix.g ).c_str(), + valueRefsDepthAttrXml( ing, usDepthCut, VrRender{ usSingleRoot, usRootPrefix } ).c_str() ); // of= echoes the selector as TYPED (an @-seed stays an @-seed) for( std::size_t siteIndex = upw.begin; siteIndex < upw.end; ++siteIndex ) { const UseSite& u = sites[ siteIndex ]; @@ -3201,17 +3204,18 @@ inline std::optional pathText( const std::string& root, const std:: // verb has no legend of its own either, and the two dialects must not differ on what they explain. // H5: the same brief floor legend + marker the CLI --path prints (verbs_navigate.h) — one wording, two transports. // Reference-as-value round: the CLI --path's to_value_refs=, by the same call. - const std::size_t ptToValueRefs = toValueRefsCount( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); + const ToValueRefs ptToValueRefs = toValueRefs( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); rw::emitTo( mem, "{}", unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ).c_str(), - toValueRefsLegend( ptToValueRefs > 0 ), + ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rootRelPathsLegend( ptSingleRoot ) ); rw::emitTo( mem, " fns ) { ValueRefRows out; @@ -861,15 +862,22 @@ class UsesValueFilter std::vector m_sites; }; -// --path / path_between: with NO directed call path (`unreachable`), how often `dstDefs` are used as values; 0 when a -// path exists, so the attribute is absent and the answer byte-identical. -inline std::size_t toValueRefsCount( const IngestResult& ing, bool unreachable, std::span dstDefs, const ValueRefIndex* cached = nullptr ) +// --path / path_between: with NO directed call path (`unreachable`), how often `dstDefs` are used as values (0 when a +// path exists, so the attribute is absent and the answer byte-identical) and the depth cut over their language families +// (ValueRefRows::depthCut: a count read off a cut file is a floor, and the answer says so, as --callers' does). +struct ToValueRefs +{ + std::size_t count = 0; + ValueRefIndex::DepthCuts depthCut; +}; +inline ToValueRefs toValueRefs( const IngestResult& ing, bool unreachable, std::span dstDefs, const ValueRefIndex* cached = nullptr ) { if( !unreachable ) { - return 0; + return {}; } - return cached != nullptr ? valueRefCallerRows( ing, *cached, dstDefs ).rows.size() : valueRefCallerRows( ing, ValueRefIndex( ing ), dstDefs ).rows.size(); + const ValueRefRows rows = cached != nullptr ? valueRefCallerRows( ing, *cached, dstDefs ) : valueRefCallerRows( ing, ValueRefIndex( ing ), dstDefs ); + return { rows.rows.size(), rows.depthCut }; } } // namespace rw diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index d4a832d48..7f820ab73 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -553,7 +553,7 @@ struct UseSite { std::uint32_t fileId; std::uint32_t line; rw::RefRole role; std // that never matched the row's own root-relative p=, the map's id=, or a git path — the M6/L1/M0-5 finding. inline std::pair, std::size_t> collectUseSites( const rw::IngestResult& ing, const UsesSelector& sel, std::span isChosenCaller, - std::string_view rootForId = {}, std::span valueDefs = {} ) + std::string_view rootForId = {}, std::span valueDefs = {}, const rw::ValueRefIndex* valueIndex = nullptr ) { using namespace rw; std::vector sites; @@ -561,7 +561,7 @@ collectUseSites( const rw::IngestResult& ing, const UsesSelector& sel, std::span const ElixirResolver elixirResolver( ing ); // Reference-as-value round: valuerefs.h UsesValueFilter — role="value" sites the resolver binds to the selector's // definitions (the same rows --callers shows), never a Through, never a duplicate read row at a value site. - const UsesValueFilter valueFilter( ing, sel.siteMatchName, valueDefs ); + const UsesValueFilter valueFilter( ing, sel.siteMatchName, valueDefs, valueIndex ); for( std::uint32_t refIndex = 0; refIndex < ing.references.size(); ++refIndex ) { const Reference& r = ing.references[refIndex]; @@ -690,8 +690,12 @@ std::optional runUses( const MainDispatch& d ) const std::vector isChosenCaller = ( sel.fileQualified || sel.scopeNarrowed ) ? usesChosenCallers( ing, g, defs ) : std::vector{}; // the sorted use-sites, plus the un-narrowed call-role total the disclosure reports. + // The value index is this verb's own, so the depth cut the --callers answer discloses (valuerefs.h) is read off the + // SAME index the role="value" rows come from: a file nested past the walk's cap holds a use below it that no row shows. + const rw::ValueRefIndex usVri( ing ); auto [ sites, callSitesOfName ] = collectUseSites( ing, sel, isChosenCaller, - usSingleRoot ? std::string_view( cfg.roots[0] ) : std::string_view{}, defs ); + usSingleRoot ? std::string_view( cfg.roots[0] ) : std::string_view{}, defs, &usVri ); + const rw::ValueRefIndex::DepthCuts usDepthCut = usVri.depthCutsFor( defs ); rw::rankUseSites( ing, g, sites ); // cut-fix C: most-depended-on sites first, so the cap drops the lightest // §A6b(ii): a file: qualifier naming a file with NO definition of the name is a WRONG SELECTOR — its @@ -750,7 +754,8 @@ std::optional runUses( const MainDispatch& d ) "{}{}{}-->{}{}", rw::kUsesLegendOpen, ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Uses, usUnprovenDefs > 0 ) // H1: exactly when the root carries unproven_defs= + rw::declinedCallsLegendWithGate( usDeclinedCalls > 0, g.gateDeclinedCalls > 0 ) // exactly when it carries declined_calls= - + rw::usesValueRoleLegend( std::any_of( sites.begin(), sites.end(), []( const UseSite& u ) { return u.role == RefRole::Value; } ) ) ).c_str(), + + rw::usesValueRoleLegend( std::any_of( sites.begin(), sites.end(), []( const UseSite& u ) { return u.role == RefRole::Value; } ) ) + + rw::valueRefsDepthLegend( usDepthCut.files > 0 ) ).c_str(), // exactly when the root carries the depth disclosure rw::capLegendClause( rw::computePageDisclosure( pageRows, sites.size(), upw.end, cfg.pageLimit, cfg.pageOffset, usDiscloseCap ).active ), rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( usSingleRoot ), @@ -774,16 +779,18 @@ std::optional runUses( const MainDispatch& d ) const std::string attr = "of=\"" + ex( sym ) + "\" defs=\"" + std::to_string( defs.size() ) + "\" external=\"" + ( external ? "1" : "0" ) + "\" count=\"" + std::to_string( sites.size() ) + "\"" + rw::unprovenDefsAttrXml( usUnprovenDefs ) // H1: where the XML root carries it - + selectorAttrs + usRootAttr + upage + rw::graphCountFloorAttrXml( g ); // §H4 §3.4 + + selectorAttrs + usRootAttr + upage + rw::graphCountFloorAttrXml( g ) // §H4 §3.4 + + rw::valueRefsDepthAttrXml( ing, usDepthCut, rw::VrRender{ usSingleRoot, usRootPrefix } ); // the depth cut that makes count= a floor; absent when none emitColumnarUseSites( stdout, ing, attr, ufiles, ulines, uroles, uins, usRootPrefix ); return 0; } - rw::emitTo( stdout, "", + rw::emitTo( stdout, "", ex( sym ).c_str(), defs.size(), external ? 1 : 0, sites.size(), rw::unprovenDefsAttrXml( usUnprovenDefs ).c_str(), // H1: beside the count it qualifies; absent at zero selectorAttrs.c_str(), usRootAttr.c_str(), upage, - rw::graphCountFloorAttrXml( g ).c_str() ); + rw::graphCountFloorAttrXml( g ).c_str(), + rw::valueRefsDepthAttrXml( ing, usDepthCut, rw::VrRender{ usSingleRoot, usRootPrefix } ).c_str() ); // the depth cut that makes count= a floor; absent when none rw::writeMultiRootTable( stdout, ing ); // M12: the roots list this element's own root= cannot carry for( std::size_t siteIndex = upw.begin; siteIndex < upw.end; ++siteIndex ) { @@ -2159,17 +2166,19 @@ std::optional runPath( const MainDispatch& d ) // same brief sentence, on both transports (mcpverbs.h path_between mirrors this line). // Reference-as-value round: with no directed call path, how often to= is used as a VALUE — a run through such a // slot is not a hop the graph can show, so the count is the clue (the callers verb lists the binding sites). - const std::size_t pthToValueRefs = rw::toValueRefsCount( ing, path.empty(), dstDefs ); + const rw::ToValueRefs pthToValueRefs = rw::toValueRefs( ing, path.empty(), dstDefs ); rw::emitTo( stdout, "{}", rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ).c_str(), - rw::toValueRefsLegend( pthToValueRefs > 0 ), + ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), rw::graphCountFloorBrief( rw::graphGaugeClauses( g ) ).c_str(), rw::rootRelPathsLegend( pthSingleRoot ) ); rw::emitTo( stdout, ""$FX/proto/a.c" cat >"$FX/proto/b.c" <<'EOF' #include @@ -1682,7 +1682,7 @@ static void tock(void) { timer_set(tock); } void boot(void) { timer_set(tock); } /* @S_BOOT */ EOF arm "RS1 dead-code: tick, stored only by itself, is listed; value-ref-excluded= counts tock alone" \ - self --dead-code attr:count=1 nd:1 'd:n=tick' 'nod:n=tock' attr:value-ref-excluded=1 "legend:another function's" + self --dead-code attr:count=1 nd:1 'd:n=tick' 'nod:n=tock' attr:value-ref-excluded=1 'legend:in another function or at file scope' arm "RS2 safe-delete tick: a dead-code candidate again — its self-row is still served and still a use site" \ self --safe-delete=tick attr:dead_code_candidate=1 attr:value_refs=1 attr:uses=1 'vr:bind=@S_TICK;in_id=tick' arm "RS3 safe-delete tock (near miss): another function stores it — not a candidate" \ @@ -1724,6 +1724,14 @@ for i in range( 1, 300 ): s += " else return reg(handler);\n}\n" open( os.path.join( fx, "deep", "elif.c" ), "w" ).write( s ) # a 300-link else-if chain nests past the 512-level cap open( os.path.join( fx, "deep", "side.py" ), "w" ).write( "def py_fn():\n return 1\n\nPT = [py_fn]\n" ) +# deep2: the same chain, plus a SHALLOW store of the same function — the shape --callers' next=--uses sends a reader to: +# the shallow use is a row, the one below the cap is not, and a count that reads as complete is the false answer. +d2 = ( "static void handler( int s ) { (void)s; }\nvoid reg( void (*)( int ) );\n" + "static void table_user( void ) { reg( handler ); } /* @DZ_SHALLOW */\nvoid run( int x )\n{\n if( x == 0 ) { }\n" ) +for i in range( 1, 300 ): + d2 += " else if( x == %d ) { }\n" % i +d2 += " else { reg( handler ); }\n}\n" +open( os.path.join( fx, "deep2", "elif.c" ), "w" ).write( d2 ) open( os.path.join( fx, "deepneg", "near.c" ), "w" ).write( "static int handler(int x) { return x; }\nint reg(int (*f)(int));\nint install(void) { return reg(" + "(" * 200 + "handler" + ")" * 200 + "); }\n" ) # deep, but under the cap @@ -1783,6 +1791,51 @@ fi run_check "RZ9 callers legend (full) defines value_refs_depth_capped= and value_refs_depth_at=" "$FX/deep" "$TMP/full.deep.xml" \ 'attr:value_refs_depth_capped=1' 'legend:value_refs_depth_capped=' 'legend:value_refs_depth_at=' +# RZ10..RZ17: the readers a --callers answer's next= sends the reader to. --uses and --path read the same value references, +# so a count beside a cut file is a floor on them too (a shallow use is a row; the one below the cap is not). +arm "RZ10 uses handler: count=1 (the shallow store) and the root says the use below the cap is unseen" \ + deep2 --uses=handler attr:count=1 nu:1 'u:role=value;p=@DZ_SHALLOW' attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' 'legend:value_refs_depth_at=' +arm "RZ11 path run,handler: no directed call path, to_value_refs=1 (the shallow store) and the root says it is a floor" \ + deep2 --path=run,handler attr:reachable=0 attr:to_value_refs=1 attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' +arm "RZ11b callers handler: the disclosure --uses and --path repeat (one cut, read by every reader)" \ + deep2 --callers=handler attr:value_refs=1 attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' +cli deep2 --uses=handler "$TMP/cli.deep2.uses.xml" +cli deep2 --path=run,handler "$TMP/cli.deep2.path.xml" +mcp_call uses '{"path":"'"$FX/deep2"'","symbol":"handler","legend":"compact"}' >"$TMP/mcp.deep2.uses.xml" +mcp_call path_between '{"path":"'"$FX/deep2"'","from":"run","to":"handler","legend":"compact"}' >"$TMP/mcp.deep2.path.xml" +run_check "RZ12 MCP uses: count=1 and the depth disclosure, root and legend, as the CLI" "$FX/deep2" "$TMP/mcp.deep2.uses.xml" \ + attr:count=1 'u:role=value;p=@DZ_SHALLOW' attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' 'legend:value_refs_depth_at=' +run_check "RZ13 MCP path_between: to_value_refs=1 and the depth disclosure, root and legend, as the CLI" "$FX/deep2" "$TMP/mcp.deep2.path.xml" \ + attr:reachable=0 attr:to_value_refs=1 attr:value_refs_depth_capped=1 'attr:value_refs_depth_at!=' 'legend:value_refs_depth_capped=' +res="$( python3 - "$TMP/cli.deep2.uses.xml" "$TMP/mcp.deep2.uses.xml" "$TMP/cli.deep2.path.xml" "$TMP/mcp.deep2.path.xml" <<'EOF' +import re, sys +def disc( f ): + x = open( f, encoding="utf-8" ).read() + root = re.search( r"<(uses|path)\b[^>]*>", x ).group( 0 ) + return re.findall( r'value_refs_depth_(?:capped|at)="[^"]*"', root ) +for cli, mcp in ( ( sys.argv[1], sys.argv[2] ), ( sys.argv[3], sys.argv[4] ) ): + a, b = disc( cli ), disc( mcp ) + if len( a ) != 2 or a != b: + print( "FAIL %s carries %r, %s carries %r" % ( cli.rsplit( "/", 1 )[-1], a, mcp.rsplit( "/", 1 )[-1], b ) ); raise SystemExit +print( "OK" ) +EOF +)" +verdict "RZ14 the CLI and the MCP twin carry byte-equal value_refs_depth_capped= / _at= on --uses and --path" "$res" +arm "RZ15 uses handler (near miss): 200 levels is under the cap — the row, no disclosure" \ + deepneg --uses=handler attr:count=1 'u:role=value' noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +arm "RZ15b path install,handler (near miss): under the cap — to_value_refs=1 and no disclosure" \ + deepneg --path=install,handler attr:reachable=0 attr:to_value_refs=1 noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +arm "RZ15c uses py_fn (near miss): a Python function beside the cut C file — another family, no disclosure" \ + deep --uses=py_fn 'u:role=value' noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +mcp_call uses '{"path":"'"$FX/deepneg"'","symbol":"handler","legend":"compact"}' >"$TMP/mcp.deepneg.uses.xml" +mcp_call path_between '{"path":"'"$FX/deepneg"'","from":"install","to":"handler","legend":"compact"}' >"$TMP/mcp.deepneg.path.xml" +run_check "RZ16 MCP uses (near miss): under the cap — the row, no disclosure" "$FX/deepneg" "$TMP/mcp.deepneg.uses.xml" \ + attr:count=1 'u:role=value' noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +run_check "RZ16b MCP path_between (near miss): under the cap — to_value_refs=1 and no disclosure" "$FX/deepneg" "$TMP/mcp.deepneg.path.xml" \ + attr:to_value_refs=1 noattr:value_refs_depth_capped noattr:value_refs_depth_at 'nolegend:value_refs_depth_capped=' +arm "RZ17 path run,reg where a directed path EXISTS (near miss): no value-reference read, so no disclosure" \ + deep2 --path=run,reg attr:reachable=1 noattr:to_value_refs noattr:value_refs_depth_capped 'nolegend:value_refs_depth_capped=' + python3 - "$FX" >"$TMP/keys.expect" <<'EOF' import os, sys fx = sys.argv[1] From d5c08fa486a063f054fb136e77ac9d994c74cf4b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:10:38 -0400 Subject: [PATCH 131/229] bench: split routing_ab_report main (quality-delta) and gate a malformed --today (#381) Co-Authored-By: Claude Sonnet 5.5 --- bench/routing_ab_report.py | 44 +++++++++++++++++++++++++------------- test/routingreportcheck.sh | 3 +++ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/bench/routing_ab_report.py b/bench/routing_ab_report.py index a22259f75..944198b33 100755 --- a/bench/routing_ab_report.py +++ b/bench/routing_ab_report.py @@ -355,6 +355,31 @@ def report_one_router(router, routing_rows, meter_rows): return 0 +def parse_today(arg): + """`--today` as a date (default: the current UTC date), or None when it is not YYYY-MM-DD.""" + if not arg: + return datetime.datetime.now(datetime.timezone.utc).date() + try: + return datetime.date.fromisoformat(arg) + except ValueError: + return None + + +def print_readout(evals_path, today): + """The registered readout date lines. They belong to the PROMPT router's registration; the toolcall + router has its own registration and is not dated here.""" + for line in readout_lines(*read_readout_dates(evals_path), today): + print(line) + + +def pick_routers(requested, routing_rows): + """The routers to report: the one asked for, else every router in the log (prompt first), else the + empty prompt-router table so a log with nothing in it still shows its shape.""" + if requested: + return [requested] + return routers_present(routing_rows) or [DEFAULT_ROUTER] + + def main(): ap = argparse.ArgumentParser( description="adoption-within-two A/B readout for the Claude Code routers (docs/EVALS.md §4 and " @@ -388,9 +413,8 @@ def main(): % args.meter, file=sys.stderr) return 1 - try: - today = datetime.date.fromisoformat(args.today) if args.today else datetime.datetime.now(datetime.timezone.utc).date() - except ValueError: + today = parse_today(args.today) + if today is None: print("routing_ab_report: --today must be YYYY-MM-DD, got %r" % args.today, file=sys.stderr) return 2 @@ -402,19 +426,9 @@ def main(): len(meter_rows), meter_bad, "found" if meter_existed else "not found")) if args.since or args.until: print("window: [%s, %s)" % (args.since or "-inf", args.until or "+inf")) - # The registered readout date belongs to the PROMPT router's registration; the toolcall router has - # its own registration and is not dated here. - for line in readout_lines(*read_readout_dates(args.evals), today): - print(line) - - if args.router: - routers = [args.router] - else: - routers = routers_present(routing_rows) - if not routers: - routers = [DEFAULT_ROUTER] # nothing in the log yet -- still show the empty prompt-router table + print_readout(args.evals, today) - for router in routers: + for router in pick_routers(args.router, routing_rows): report_one_router(router, routing_rows, meter_rows) return 0 diff --git a/test/routingreportcheck.sh b/test/routingreportcheck.sh index a8f782101..a63ae6af7 100755 --- a/test/routingreportcheck.sh +++ b/test/routingreportcheck.sh @@ -372,6 +372,9 @@ python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" --today 2026-10-07 2>&1 \ | grep -Eq 'registered readout date: 20[0-9]{2}-[0-9]{2}-[0-9]{2} -- ' && ok "D9: the shipped docs/EVALS.md carries a parseable readout date" \ || no "D9: docs/EVALS.md's prompt-router registration has no parseable readout date" +python3 "$SCRIPT" --routing "$DC/routing.jsonl" --meter "$DC/substitution.jsonl" --today not-a-date >/dev/null 2>"$TMP/today.err"; RCT=$? +[ "$RCT" = 2 ] && grep -q -- '--today must be YYYY-MM-DD' "$TMP/today.err" && ok "D10: a malformed --today is a usage error (exit 2), not a silent default" \ + || no "D10: --today not-a-date gave rc=$RCT stderr=$( cat "$TMP/today.err" )" echo "" if [ "$fail" = 0 ]; then From e6758ae405df675d50ad2632c3063adce38b7202 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:18:03 -0400 Subject: [PATCH 132/229] defectshape.h: the four defect-shape scanners (format-arity, utf8-cut, dedup-first, vacuous-assert) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure functions over a file's bytes that return SITES: a facet, the byte the offending text starts at, and the text normalized (comments dropped, whitespace collapsed) — the site's identity across a change. - format-arity: C++ std::format / format_to / format_to_n / print / println, fmt::, rw::emitTo / formatTo with a literal format (adjacent literals concatenated, raw and wide literals read as written) at the family's fixed index, shifted only by a literal stdout / stderr or std::locale(...); Python "literal".format(...) and its parenthesised implicit concat. The replacement-field grammar counts automatic, positional and nested fields (Python named fields too); a mismatch is a field with no argument or an argument no field prints. Skipped, never guessed: a named-constant / macro / runtime format, a pack expansion, __VA_ARGS__, an all-caps macro argument, unbalanced template angles, mixed numbering, *args / **kwargs, f-strings and bytes, preprocessor lines. - utf8-cut: a resize / erase / substr(0, N) cut whose length is the bound of a size comparison on the same string (or a cap-named constant) followed by an ellipsis on that string, or an ellipsis appended once a single-char push loop reaches a size cap; a 0xC0/0x80 back-off or a utf8-named call anywhere in the function exempts it; wide literals are out. - dedup-first: std::unique / std::ranges::unique with a lambda over a same-file type carrying a severity field (bounded tokens sev / severity / priority / prio), read neither by the predicate nor by a sort before it in the function. - vacuous-assert: Bash logical lines (continuations joined, comments, heredoc bodies and multi-line quote tails dropped); an absence assertion (&&/|| list, ! form, if/else) read off an unguarded capture or a direct pipe. Guards: an rc read, || on the capture, if ! V=, a presence test, a case on the text, any requirement on the same capture in scope, and set -e for a plain (not local) capture. Co-Authored-By: Claude Opus 5.5 --- src/defectshape.h | 2466 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2466 insertions(+) create mode 100644 src/defectshape.h diff --git a/src/defectshape.h b/src/defectshape.h new file mode 100644 index 000000000..889bd2ae6 --- /dev/null +++ b/src/defectshape.h @@ -0,0 +1,2466 @@ +#pragma once + +// defectshape.h — the scanners behind --quality-delta's defect-shape kind: four shapes of a REAL defect that +// review keeps finding by hand and that a deterministic syntactic check finds at write time. +// +// format-arity C++: a literal std::format / format_to / format_to_n / print / println, fmt::format / +// format_to / format_to_n / print / println, rw::emitTo / rw::formatTo format string whose +// replacement fields do not match its arguments. Python: a "literal".format( ... ). +// std::format_string rejects too FEW arguments at compile time and accepts too MANY +// silently; Python raises on too few at run time and ignores too many. +// utf8-cut C++: a byte cap on display text — a cut (resize / erase / substr( 0, N )) plus an appended +// ellipsis on the same string, the cut length being the bound of a size comparison on that +// string or a cap-named constant — or an ellipsis appended once a single-char push loop +// reaches a size cap, with no UTF-8 boundary back-off anywhere in the function. +// dedup-first C++: std::unique / std::ranges::unique with a lambda predicate over a type that carries a +// severity-like field (a bounded name token sev / severity / priority / prio), where neither +// the predicate nor a sort before it in the function reads that field: the FIRST row of a +// run survives, not the most severe. +// vacuous-assert Bash test scripts: an ABSENCE assertion (grep -q PAT whose match is the failure branch) +// read off a command whose failure is never checked, so a crash reads as PASS. +// +// Each scanner returns SITES: a facet, the byte the offending text starts at (the caller anchors it on the +// innermost enclosing definition, or on the file), and the offending text NORMALIZED (comments dropped, +// whitespace runs collapsed). The normalized text is the site's identity across a change: --quality-delta +// reports a site only when its text is not among the baseline's sites of that facet, so an untouched, moved +// or renamed defect is not new, and an edited one is. +// +// PRECISION BEFORE RECALL. Every test below answers "not this shape" when it cannot decide — a format held in +// a named constant or a macro, a pack expansion, a named predicate, a producer this file never assigns — and +// each of those is a stated miss, never a finding. The legend names the floors. + +#include +#include +#include +#include +#include +#include +#include + +#include "infra/Diagnostics.h" + +namespace rw::defectshape +{ + +enum class Facet : std::uint8_t +{ + FormatArity = 0, + Utf8Cut = 1, + DedupFirst = 2, + VacuousAssert = 3, +}; +inline constexpr std::size_t kFacetCount = 4; +inline constexpr std::array kFacetNames = { "format-arity", "utf8-cut", "dedup-first", "vacuous-assert" }; + +inline constexpr std::string_view facetName( Facet f ) noexcept +{ + return kFacetNames[ static_cast( f ) ]; +} + +// The one facet whose rows gate, on ANY origin: a placeholder/argument mismatch on a literal format has no +// legitimate intent (an argument the format never prints is dropped output; a field with no argument is a +// compile error or a run-time exception), so it is a defect, not debt. The other three are report-only. +inline constexpr bool facetGatesOnAnyOrigin( Facet f ) noexcept +{ + return f == Facet::FormatArity; +} + +struct Site +{ + Facet facet = Facet::FormatArity; + std::uint32_t startByte = 0; + std::string text; +}; + +// A definition's byte span [begin, end) in the scanned file — the function bodies the per-function rules +// (utf8-cut, dedup-first) and the Bash scopes (vacuous-assert) read. +struct Span +{ + std::uint32_t begin = 0; + std::uint32_t end = 0; +}; + +namespace detail +{ + +inline bool isIdentStart( char c ) noexcept +{ + const unsigned char u = static_cast( c ); + return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || c == '_' || u >= 0x80; +} + +inline bool isIdentChar( char c ) noexcept +{ + return isIdentStart( c ) || ( c >= '0' && c <= '9' ); +} + +inline bool isDigit( char c ) noexcept +{ + return c >= '0' && c <= '9'; +} + +inline bool isSpace( char c ) noexcept +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; +} + +enum class TokKind : std::uint8_t +{ + Ident, + Number, + String, + Char, + Punct, +}; + +// One token of a C-family or Python source: [begin, end) in the file. A String token spans its prefix and +// quotes; contentBegin/contentEnd are the bytes between the quotes (between the raw delimiters for a C++ raw +// string, between the triple quotes for a Python long string). +struct Tok +{ + TokKind kind = TokKind::Punct; + std::uint32_t begin = 0; + std::uint32_t end = 0; + std::uint32_t contentBegin = 0; + std::uint32_t contentEnd = 0; +}; + +inline std::string_view tokText( std::string_view src, const Tok& t ) noexcept +{ + return src.substr( t.begin, t.end - t.begin ); +} + +inline std::string_view tokContent( std::string_view src, const Tok& t ) noexcept +{ + return src.substr( t.contentBegin, t.contentEnd - t.contentBegin ); +} + +inline bool tokIs( std::string_view src, const Tok& t, std::string_view s ) noexcept +{ + return tokText( src, t ) == s; +} + +// The longest punctuator at `i` from a small fixed table (longest first), else one byte. +inline std::uint32_t punctLen( std::string_view s, std::size_t i ) noexcept +{ + static constexpr std::array kPunct = { + "<<=", ">>=", "<=>", "->*", "...", "::", "->", "++", "--", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", + "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "**", ":=", + }; + for( std::string_view p : kPunct ) + { + if( s.substr( i, p.size() ) == p ) + { + return static_cast( p.size() ); + } + } + return 1; +} + +// Skip a quoted run starting at the opening quote s[i]; returns the index one past the closing quote (or the +// end of the line for an unterminated literal). Backslash escapes the next byte. +inline std::size_t skipQuoted( std::string_view s, std::size_t i, char q ) noexcept +{ + std::size_t j = i + 1; + while( j < s.size() && s[j] != q && s[j] != '\n' ) + { + j += ( s[j] == '\\' && j + 1 < s.size() ) ? 2 : 1; + } + return ( j < s.size() && s[j] == q ) ? j + 1 : j; +} + +// C++ tokens with comments and preprocessor directives dropped. A directive is skipped whole (continuation +// lines included): a format inside a macro body is never judged — its arguments are whatever the expansion +// site passes. +inline std::vector lexCpp( std::string_view s ) +{ + std::vector out; + std::size_t i = 0; + bool lineStart = true; + while( i < s.size() ) + { + const char c = s[i]; + if( c == '\n' ) + { + lineStart = true; + ++i; + continue; + } + if( isSpace( c ) ) + { + ++i; + continue; + } + if( c == '/' && i + 1 < s.size() && s[i + 1] == '/' ) + { + while( i < s.size() && s[i] != '\n' ) + { + ++i; + } + continue; + } + if( c == '/' && i + 1 < s.size() && s[i + 1] == '*' ) + { + const std::size_t e = s.find( "*/", i + 2 ); + i = ( e == std::string_view::npos ) ? s.size() : e + 2; + continue; + } + if( c == '#' && lineStart ) + { + while( i < s.size() && s[i] != '\n' ) + { + i += ( s[i] == '\\' && i + 1 < s.size() ) ? 2 : 1; + } + continue; + } + lineStart = false; + const std::uint32_t b = static_cast( i ); + if( isIdentStart( c ) ) + { + std::size_t j = i; + while( j < s.size() && isIdentChar( s[j] ) ) + { + ++j; + } + const std::string_view word = s.substr( i, j - i ); + const bool isStrPrefix = word == "L" || word == "u" || word == "U" || word == "u8" || word == "R" || word == "LR" || word == "uR" + || word == "UR" || word == "u8R"; + if( isStrPrefix && j < s.size() && s[j] == '"' ) + { + if( word.back() == 'R' ) + { + const std::size_t open = s.find( '(', j + 1 ); + if( open != std::string_view::npos && open - j - 1 <= 16 ) + { + const std::string close = ")" + std::string( s.substr( j + 1, open - j - 1 ) ) + "\""; + const std::size_t e = s.find( close, open + 1 ); + const std::size_t endAt = ( e == std::string_view::npos ) ? s.size() : e + close.size(); + const std::size_t cEnd = ( e == std::string_view::npos ) ? s.size() : e; + out.push_back( { TokKind::String, b, static_cast( endAt ), static_cast( open + 1 ), static_cast( cEnd ) } ); + i = endAt; + continue; + } + } + const std::size_t e = skipQuoted( s, j, '"' ); + out.push_back( { TokKind::String, b, static_cast( e ), static_cast( j + 1 ), static_cast( e > j + 1 ? e - 1 : j + 1 ) } ); + i = e; + continue; + } + if( isStrPrefix && word.back() != 'R' && j < s.size() && s[j] == '\'' ) + { + const std::size_t e = skipQuoted( s, j, '\'' ); + out.push_back( { TokKind::Char, b, static_cast( e ), b, static_cast( e ) } ); + i = e; + continue; + } + out.push_back( { TokKind::Ident, b, static_cast( j ), b, static_cast( j ) } ); + i = j; + continue; + } + if( isDigit( c ) || ( c == '.' && i + 1 < s.size() && isDigit( s[i + 1] ) ) ) + { + std::size_t j = i + 1; + while( j < s.size() && ( isIdentChar( s[j] ) || s[j] == '.' || s[j] == '\'' + || ( ( s[j] == '+' || s[j] == '-' ) && ( s[j - 1] == 'e' || s[j - 1] == 'E' || s[j - 1] == 'p' || s[j - 1] == 'P' ) ) ) ) + { + ++j; + } + out.push_back( { TokKind::Number, b, static_cast( j ), b, static_cast( j ) } ); + i = j; + continue; + } + if( c == '"' ) + { + const std::size_t e = skipQuoted( s, i, '"' ); + out.push_back( { TokKind::String, b, static_cast( e ), b + 1, static_cast( e > i + 1 ? e - 1 : i + 1 ) } ); + i = e; + continue; + } + if( c == '\'' ) + { + const std::size_t e = skipQuoted( s, i, '\'' ); + out.push_back( { TokKind::Char, b, static_cast( e ), b, static_cast( e ) } ); + i = e; + continue; + } + const std::uint32_t n = punctLen( s, i ); + out.push_back( { TokKind::Punct, b, b + n, b, b + n } ); + i += n; + } + return out; +} + +// Python tokens with comments dropped (newlines are not tokens: an implicit concat inside parentheses spans +// lines, and the shapes read here never need statement boundaries). +inline std::vector lexPython( std::string_view s ) +{ + std::vector out; + std::size_t i = 0; + const auto isPrefixChar = []( char c ) { return c == 'r' || c == 'R' || c == 'b' || c == 'B' || c == 'u' || c == 'U' || c == 'f' || c == 'F'; }; + while( i < s.size() ) + { + const char c = s[i]; + if( isSpace( c ) || c == '\\' ) + { + ++i; + continue; + } + if( c == '#' ) + { + while( i < s.size() && s[i] != '\n' ) + { + ++i; + } + continue; + } + const std::uint32_t b = static_cast( i ); + std::size_t q = i; + while( q < s.size() && q - i < 2 && isPrefixChar( s[q] ) ) + { + ++q; + } + if( q < s.size() && ( s[q] == '"' || s[q] == '\'' ) && ( q == i || !isIdentChar( i > 0 ? s[i - 1] : ' ' ) ) ) + { + const char quote = s[q]; + const bool triple = s.substr( q, 3 ) == std::string( 3, quote ); + std::size_t j = q + ( triple ? 3 : 1 ); + const std::size_t cb = j; + while( j < s.size() ) + { + if( s[j] == '\\' ) + { + j += 2; + continue; + } + if( triple ? s.substr( j, 3 ) == std::string( 3, quote ) : s[j] == quote ) + { + break; + } + if( !triple && s[j] == '\n' ) + { + break; + } + ++j; + } + const std::size_t ce = std::min( j, s.size() ); + const std::size_t end = std::min( s.size(), j + ( j < s.size() && s[j] != '\n' ? ( triple ? 3 : 1 ) : 0 ) ); + out.push_back( { TokKind::String, b, static_cast( end ), static_cast( cb ), static_cast( ce ) } ); + i = end; + continue; + } + if( isIdentStart( c ) ) + { + std::size_t j = i; + while( j < s.size() && isIdentChar( s[j] ) ) + { + ++j; + } + out.push_back( { TokKind::Ident, b, static_cast( j ), b, static_cast( j ) } ); + i = j; + continue; + } + if( isDigit( c ) ) + { + std::size_t j = i + 1; + while( j < s.size() && ( isIdentChar( s[j] ) || s[j] == '.' ) ) + { + ++j; + } + out.push_back( { TokKind::Number, b, static_cast( j ), b, static_cast( j ) } ); + i = j; + continue; + } + const std::uint32_t n = punctLen( s, i ); + out.push_back( { TokKind::Punct, b, b + n, b, b + n } ); + i += n; + } + return out; +} + +// The tokens [from, to] joined by single spaces: a site's identity text, immune to reformatting. +inline std::string joinTokens( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to ) +{ + std::string out; + for( std::size_t k = from; k <= to && k < toks.size(); ++k ) + { + if( !out.empty() ) + { + out.push_back( ' ' ); + } + out.append( tokText( src, toks[k] ) ); + } + return out; +} + +// The index of the bracket that closes the one at `open` (round, square or curly; the others nest inside), +// or toks.size() when unbalanced. +inline std::size_t matchClose( std::string_view src, const std::vector& toks, std::size_t open ) noexcept +{ + int depth = 0; + for( std::size_t k = open; k < toks.size(); ++k ) + { + if( toks[k].kind != TokKind::Punct ) + { + continue; + } + const char c = src[ toks[k].begin ]; + if( toks[k].end - toks[k].begin != 1 ) + { + continue; + } + if( c == '(' || c == '[' || c == '{' ) + { + ++depth; + } + else if( c == ')' || c == ']' || c == '}' ) + { + --depth; + if( depth == 0 ) + { + return k; + } + if( depth < 0 ) + { + return toks.size(); + } + } + } + return toks.size(); +} + +// A call's arguments: token index ranges [first, last] between the parentheses at `open` and `close`, split at +// top-level commas. An empty argument (a trailing comma) is kept as first > last. +struct ArgRange +{ + std::size_t first = 0; + std::size_t last = 0; + bool empty() const noexcept { return first > last; } +}; + +inline std::vector splitArgs( std::string_view src, const std::vector& toks, std::size_t open, std::size_t close ) +{ + std::vector out; + if( close == open + 1 ) + { + return out; + } + int depth = 0; + std::size_t start = open + 1; + for( std::size_t k = open + 1; k < close; ++k ) + { + if( toks[k].kind != TokKind::Punct || toks[k].end - toks[k].begin != 1 ) + { + continue; + } + const char c = src[ toks[k].begin ]; + if( c == '(' || c == '[' || c == '{' ) + { + ++depth; + } + else if( c == ')' || c == ']' || c == '}' ) + { + --depth; + } + else if( c == ',' && depth == 0 ) + { + out.push_back( { start, k - 1 } ); + start = k + 1; + } + } + out.push_back( { start, close - 1 } ); + return out; +} + +// ─── replacement-field grammars ───────────────────────────────────────────────────────────────────────── + +// What a format string asks of its argument list. ok=false: the string is not one this check can judge +// (mixed automatic and manual numbering, a lone brace, a named field where the family has none) — skipped. +struct FieldUse +{ + bool ok = false; + std::uint32_t autoFields = 0; + std::vector indices; // manual positional indices, one per use + std::vector names; // Python named fields +}; + +// The arg-id at s[i..]: digits → a manual index, an identifier → a name, empty → automatic. Advances i. +// Returns false on anything else. +inline bool readArgId( std::string_view s, std::size_t& i, FieldUse& use, bool allowNames, bool pythonFieldName ) +{ + const std::size_t b = i; + if( i < s.size() && isDigit( s[i] ) ) + { + std::uint32_t v = 0; + while( i < s.size() && isDigit( s[i] ) ) + { + if( v > 100000 ) + { + return false; // not a real argument index — refuse rather than wrap + } + v = v * 10 + static_cast( s[i] - '0' ); + ++i; + } + use.indices.push_back( v ); + } + else if( i < s.size() && isIdentStart( s[i] ) ) + { + if( !allowNames ) + { + return false; + } + while( i < s.size() && isIdentChar( s[i] ) ) + { + ++i; + } + use.names.emplace_back( s.substr( b, i - b ) ); + } + else + { + ++use.autoFields; + } + if( pythonFieldName ) + { + // attribute / index accessors after the arg name: .attr and [key], neither consumes an argument + while( i < s.size() && ( s[i] == '.' || s[i] == '[' ) ) + { + if( s[i] == '[' ) + { + const std::size_t e = s.find( ']', i ); + if( e == std::string_view::npos ) + { + return false; + } + i = e + 1; + } + else + { + ++i; + while( i < s.size() && isIdentChar( s[i] ) ) + { + ++i; + } + } + } + } + return true; +} + +// The std::format / fmt / Python str.format replacement-field grammar, over the literal's content as written +// (escape sequences do not produce braces, except Python's \N{...}, which is skipped). `python` selects the +// Python field-name and conversion syntax. +inline FieldUse parseFields( std::string_view s, bool python ) +{ + FieldUse use; + std::size_t i = 0; + while( i < s.size() ) + { + const char c = s[i]; + if( python && c == '\\' && i + 1 < s.size() && s[i + 1] == 'N' && i + 2 < s.size() && s[i + 2] == '{' ) + { + const std::size_t e = s.find( '}', i + 3 ); + if( e == std::string_view::npos ) + { + return use; + } + i = e + 1; + continue; + } + if( c == '}' ) + { + if( i + 1 < s.size() && s[i + 1] == '}' ) + { + i += 2; + continue; + } + return use; // a lone '}' — not a format this check can judge + } + if( c != '{' ) + { + ++i; + continue; + } + if( i + 1 < s.size() && s[i + 1] == '{' ) + { + i += 2; + continue; + } + ++i; + if( !readArgId( s, i, use, python, python ) ) + { + return use; + } + if( python && i < s.size() && s[i] == '!' ) + { + i += 2; // !r / !s / !a + } + if( i < s.size() && s[i] == ':' ) + { + ++i; + // the spec: nested replacement fields (dynamic width / precision) each consume an argument + while( i < s.size() && s[i] != '}' ) + { + if( s[i] == '{' ) + { + ++i; + if( !readArgId( s, i, use, python, python ) || i >= s.size() || s[i] != '}' ) + { + return use; + } + } + ++i; + } + } + if( i >= s.size() || s[i] != '}' ) + { + return use; + } + ++i; + } + use.ok = !( use.autoFields > 0 && !use.indices.empty() ); // mixed numbering is an error this check does not judge + return use; +} + +// True when `use` against `positional` arguments (and, for Python, the keyword names `kw`) is a mismatch. +inline bool fieldsMismatch( const FieldUse& use, std::uint32_t positional, const std::vector& kw ) +{ + bool bad = false; + if( !use.indices.empty() ) + { + std::vector seen( positional, 0 ); + for( std::uint32_t ix : use.indices ) + { + if( ix >= positional ) + { + bad = true; // a field with no argument + } + else + { + seen[ix] = 1; + } + } + bad = bad || std::find( seen.begin(), seen.end(), char( 0 ) ) != seen.end(); // an argument no field prints + } + else + { + bad = use.autoFields != positional; + } + for( const std::string& n : use.names ) + { + bad = bad || std::find( kw.begin(), kw.end(), n ) == kw.end(); + } + for( const std::string& k : kw ) + { + bad = bad || std::find( use.names.begin(), use.names.end(), k ) == use.names.end(); + } + return bad; +} + +// ─── format-arity, C++ ────────────────────────────────────────────────────────────────────────────────── + +// The format family: (namespace, name) → the index the format argument sits at, and whether a leading +// stdout / stderr argument shifts it (the print overloads that take a FILE*). A leading std::locale(...) +// argument shifts every family by one. Nothing else moves the index: a non-literal at that position is +// skipped, never searched past (the literal there may be an ARGUMENT, as in std::format( kFmt, "{}" )). +struct FormatFamily +{ + std::string_view ns; + std::string_view name; + std::uint8_t fmtIndex; + bool streamShift; +}; +inline constexpr std::array kFormatFamilies = { { + { "std", "format", 0, false }, { "std", "format_to", 1, false }, { "std", "format_to_n", 2, false }, + { "std", "print", 0, true }, { "std", "println", 0, true }, { "fmt", "format", 0, false }, + { "fmt", "format_to", 1, false }, { "fmt", "format_to_n", 2, false }, { "fmt", "print", 0, true }, + { "fmt", "println", 0, true }, { "rw", "emitTo", 1, false }, { "rw", "formatTo", 2, false }, +} }; + +inline const FormatFamily* formatFamilyAt( std::string_view src, const std::vector& toks, std::size_t k ) noexcept +{ + if( k < 2 || toks[k].kind != TokKind::Ident || !tokIs( src, toks[k - 1], "::" ) || toks[k - 2].kind != TokKind::Ident ) + { + return nullptr; + } + if( k >= 3 && ( tokIs( src, toks[k - 3], "." ) || tokIs( src, toks[k - 3], "->" ) ) ) + { + return nullptr; + } + const std::string_view ns = tokText( src, toks[k - 2] ); + const std::string_view name = tokText( src, toks[k] ); + for( const FormatFamily& f : kFormatFamilies ) + { + if( f.ns == ns && f.name == name ) + { + return &f; + } + } + return nullptr; +} + +inline bool isAllCapsIdent( std::string_view w ) noexcept +{ + if( w.size() < 2 || !( w[0] >= 'A' && w[0] <= 'Z' ) ) + { + return false; + } + return std::all_of( w.begin(), w.end(), []( char c ) { return ( c >= 'A' && c <= 'Z' ) || isDigit( c ) || c == '_'; } ); +} + +// An argument whose count is unknowable here: a pack expansion, __VA_ARGS__, an all-caps identifier alone (an +// object-like macro may expand to a comma list), or unbalanced angle brackets (a template argument list the +// comma split may have cut). +inline bool argCountUnknowable( std::string_view src, const std::vector& toks, const ArgRange& a ) noexcept +{ + if( a.empty() ) + { + return true; + } + if( tokIs( src, toks[a.last], "..." ) ) + { + return true; + } + if( a.first == a.last && toks[a.first].kind == TokKind::Ident && isAllCapsIdent( tokText( src, toks[a.first] ) ) ) + { + return true; + } + int angle = 0; + for( std::size_t k = a.first; k <= a.last; ++k ) + { + if( tokIs( src, toks[k], "__VA_ARGS__" ) ) + { + return true; + } + angle += tokIs( src, toks[k], "<" ) ? 1 : 0; + angle -= tokIs( src, toks[k], ">" ) ? 1 : 0; + } + return angle != 0; +} + +// The argument's tokens are one or more adjacent string literals → their concatenated content; else empty +// optional-by-flag (a named constant, a macro piece such as PRIu64, a runtime_format wrapper). +inline bool literalContent( std::string_view src, const std::vector& toks, const ArgRange& a, std::string& out ) +{ + out.clear(); + if( a.empty() ) + { + return false; + } + for( std::size_t k = a.first; k <= a.last; ++k ) + { + if( toks[k].kind != TokKind::String ) + { + return false; + } + out.append( tokContent( src, toks[k] ) ); + } + return true; +} + +inline void scanFormatArityCpp( std::string_view src, const std::vector& toks, std::vector& out ) +{ + std::string fmt; + for( std::size_t k = 2; k + 1 < toks.size(); ++k ) + { + const FormatFamily* fam = formatFamilyAt( src, toks, k ); + if( fam == nullptr || !tokIs( src, toks[k + 1], "(" ) ) + { + continue; + } + const std::size_t close = matchClose( src, toks, k + 1 ); + if( close >= toks.size() ) + { + continue; + } + const std::vector args = splitArgs( src, toks, k + 1, close ); + std::size_t fi = fam->fmtIndex; + if( fi < args.size() && fam->streamShift && args[fi].first == args[fi].last + && ( tokIs( src, toks[ args[fi].first ], "stdout" ) || tokIs( src, toks[ args[fi].first ], "stderr" ) ) ) + { + ++fi; + } + if( fi < args.size() && !args[fi].empty() && args[fi].first + 3 <= args[fi].last && tokIs( src, toks[ args[fi].first ], "std" ) + && tokIs( src, toks[ args[fi].first + 1 ], "::" ) && tokIs( src, toks[ args[fi].first + 2 ], "locale" ) ) + { + ++fi; + } + if( fi >= args.size() || !literalContent( src, toks, args[fi], fmt ) ) + { + continue; + } + bool unknowable = false; + for( std::size_t a = fi + 1; a < args.size(); ++a ) + { + unknowable = unknowable || argCountUnknowable( src, toks, args[a] ); + } + const FieldUse use = parseFields( fmt, false ); + if( unknowable || !use.ok ) + { + continue; + } + const std::uint32_t positional = static_cast( args.size() - fi - 1 ); + if( fieldsMismatch( use, positional, {} ) ) + { + out.push_back( { Facet::FormatArity, toks[k - 2].begin, joinTokens( src, toks, k - 2, close ) } ); + } + } +} + +// ─── format-arity, Python "literal".format( ... ) ─────────────────────────────────────────────────────── + +inline bool pyStringPrefixOk( std::string_view src, const Tok& t ) noexcept +{ + for( std::uint32_t p = t.begin; p < t.contentBegin; ++p ) + { + const char c = src[p]; + if( c == 'f' || c == 'F' || c == 'b' || c == 'B' ) + { + return false; // an f-string has no argument list; bytes have no .format + } + } + return true; +} + +// A keyword before '(' makes the parentheses a grouping (return ( "a" "b" ).format(…)), not a call. +inline bool isPythonKeyword( std::string_view w ) noexcept +{ + static constexpr std::array kKw = { "return", "yield", "in", "not", "and", "or", "if", "else", "elif", "while", "assert", + "await", "lambda", "is", "for" }; + return std::find( kKw.begin(), kKw.end(), w ) != kKw.end(); +} + +inline void scanFormatArityPython( std::string_view src, const std::vector& toks, std::vector& out ) +{ + std::string fmt; + for( std::size_t k = 1; k + 2 < toks.size(); ++k ) + { + if( !tokIs( src, toks[k], "." ) || !tokIs( src, toks[k + 1], "format" ) || !tokIs( src, toks[k + 2], "(" ) ) + { + continue; + } + // the receiver: adjacent literals right before the dot, or a parenthesised group holding only literals + std::size_t firstLit = k; + std::size_t lastLit = k - 1; + std::size_t siteFrom = k; + if( toks[k - 1].kind == TokKind::String ) + { + firstLit = k - 1; + while( firstLit > 0 && toks[firstLit - 1].kind == TokKind::String ) + { + --firstLit; + } + siteFrom = firstLit; + } + else if( tokIs( src, toks[k - 1], ")" ) ) + { + std::size_t open = k - 1; + while( open > 0 && toks[open - 1].kind == TokKind::String ) + { + --open; + } + if( open == 0 || !tokIs( src, toks[open - 1], "(" ) || open > k - 2 ) + { + continue; + } + const std::size_t paren = open - 1; + if( paren > 0 && ( ( toks[paren - 1].kind == TokKind::Ident && !isPythonKeyword( tokText( src, toks[paren - 1] ) ) ) + || tokIs( src, toks[paren - 1], ")" ) || tokIs( src, toks[paren - 1], "]" ) ) ) + { + continue; // a call's parentheses, not a grouping + } + firstLit = open; + lastLit = k - 2; + siteFrom = paren; + } + if( firstLit > lastLit ) + { + continue; + } + fmt.clear(); + bool prefixOk = true; + for( std::size_t p = firstLit; p <= lastLit; ++p ) + { + prefixOk = prefixOk && pyStringPrefixOk( src, toks[p] ); + fmt.append( tokContent( src, toks[p] ) ); + } + const std::size_t close = matchClose( src, toks, k + 2 ); + if( !prefixOk || close >= toks.size() ) + { + continue; + } + std::vector args = splitArgs( src, toks, k + 2, close ); + if( !args.empty() && args.back().empty() ) + { + args.pop_back(); // a trailing comma + } + std::uint32_t positional = 0; + std::vector kw; + bool unknowable = false; + for( const ArgRange& a : args ) + { + if( a.empty() || tokIs( src, toks[a.first], "*" ) || tokIs( src, toks[a.first], "**" ) ) + { + unknowable = true; + break; + } + if( a.first < a.last && toks[a.first].kind == TokKind::Ident && tokIs( src, toks[a.first + 1], "=" ) ) + { + kw.emplace_back( tokText( src, toks[a.first] ) ); + } + else + { + ++positional; + } + } + const FieldUse use = parseFields( fmt, true ); + if( unknowable || !use.ok ) + { + continue; + } + if( fieldsMismatch( use, positional, kw ) ) + { + out.push_back( { Facet::FormatArity, toks[siteFrom].begin, joinTokens( src, toks, siteFrom, close ) } ); + } + } +} + +// ─── utf8-cut, C++ ────────────────────────────────────────────────────────────────────────────────────── + +// An ellipsis literal: an ordinary or u8 literal whose content, spaces trimmed, is "..." or U+2026 (written +// raw or as \xE2\x80\xA6). A wide / u16 / u32 literal holds code units or points: a cut cannot split one. +inline bool isEllipsisLiteral( std::string_view src, const Tok& t ) noexcept +{ + if( t.kind != TokKind::String ) + { + return false; + } + const std::string_view pre = src.substr( t.begin, t.contentBegin - t.begin ); + if( pre != "\"" && pre != "u8\"" ) + { + return false; + } + std::string_view c = tokContent( src, t ); + while( !c.empty() && c.front() == ' ' ) + { + c.remove_prefix( 1 ); + } + while( !c.empty() && c.back() == ' ' ) + { + c.remove_suffix( 1 ); + } + return c == "..." || c == "\xE2\x80\xA6" || c == "\\xE2\\x80\\xA6" || c == "\\xe2\\x80\\xa6" || c == "\\u2026"; +} + +// A UTF-8 boundary back-off or a utf8-aware helper anywhere in the function exempts it (a precision choice: +// the guard is not proven to cover the cut): the 0xC0 / 0x80 continuation mask, or a call whose name has a +// utf8 token. +inline bool hasUtf8Guard( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to ) noexcept +{ + bool c0 = false; + bool c80 = false; + for( std::size_t k = from; k < to; ++k ) + { + const std::string_view w = tokText( src, toks[k] ); + if( toks[k].kind == TokKind::Number ) + { + c0 = c0 || w == "0xC0" || w == "0xc0" || w == "0xC0u" || w == "0xc0u" || w == "0xC0U"; + c80 = c80 || w == "0x80" || w == "0x80u" || w == "0x80U"; + } + if( toks[k].kind == TokKind::Ident && ( w.find( "utf8" ) != std::string_view::npos || w.find( "Utf8" ) != std::string_view::npos + || w.find( "UTF8" ) != std::string_view::npos ) ) + { + return true; + } + } + return c0 && c80; +} + +// V . size ( ) / V . length ( ) at k: the receiver name, or empty. +inline std::string_view sizeCallReceiver( std::string_view src, const std::vector& toks, std::size_t k ) noexcept +{ + if( k + 4 < toks.size() && toks[k].kind == TokKind::Ident && tokIs( src, toks[k + 1], "." ) + && ( tokIs( src, toks[k + 2], "size" ) || tokIs( src, toks[k + 2], "length" ) ) && tokIs( src, toks[k + 3], "(" ) && tokIs( src, toks[k + 4], ")" ) ) + { + return tokText( src, toks[k] ); + } + return {}; +} + +inline bool isCompareOp( std::string_view src, const Tok& t ) noexcept +{ + const std::string_view w = tokText( src, t ); + return w == ">" || w == ">=" || w == "<" || w == "<=" || w == "=="; +} + +// Name tokens of an identifier (camelCase and snake_case boundaries), lower-cased. +inline std::vector nameTokens( std::string_view w ) +{ + std::vector out; + std::string cur; + for( std::size_t i = 0; i < w.size(); ++i ) + { + const char c = w[i]; + if( c == '_' || isDigit( c ) ) + { + if( !cur.empty() ) + { + out.push_back( cur ); + cur.clear(); + } + continue; + } + const bool upper = c >= 'A' && c <= 'Z'; + if( upper && !cur.empty() && !( i > 0 && w[i - 1] >= 'A' && w[i - 1] <= 'Z' ) ) + { + out.push_back( cur ); + cur.clear(); + } + cur.push_back( upper ? static_cast( c - 'A' + 'a' ) : c ); + } + if( !cur.empty() ) + { + out.push_back( cur ); + } + return out; +} + +inline bool isCapName( std::string_view w ) +{ + for( const std::string& t : nameTokens( w ) ) + { + if( t == "cap" || t == "max" || t == "limit" || t == "budget" ) + { + return true; + } + } + return false; +} + +// Is the length text `n` (the tokens of a cut's length argument) the bound of a size comparison on `v` +// within [from, to), or a single cap-named identifier? +inline bool lengthIsSizeBound( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to, std::string_view v, + const std::string& n ) +{ + for( std::size_t k = from; k < to; ++k ) + { + if( sizeCallReceiver( src, toks, k ) != v ) + { + continue; + } + // V.size() OP N + if( k + 6 < to && isCompareOp( src, toks[k + 5] ) && tokText( src, toks[k + 6] ) == n ) + { + return true; + } + // N OP V.size() + if( k >= 2 && isCompareOp( src, toks[k - 1] ) && tokText( src, toks[k - 2] ) == n ) + { + return true; + } + } + return n.find( ' ' ) == std::string::npos && isCapName( n ); +} + +// Does `V += ELL` / `V.append( ELL )` occur in [from, to)? Returns the index of the statement's first token, +// or `to`. +inline std::size_t ellipsisAppendOn( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to, std::string_view v ) noexcept +{ + for( std::size_t k = from; k + 2 < to; ++k ) + { + if( toks[k].kind != TokKind::Ident || tokText( src, toks[k] ) != v || ( k > 0 && ( tokIs( src, toks[k - 1], "." ) || tokIs( src, toks[k - 1], "->" ) ) ) ) + { + continue; + } + if( tokIs( src, toks[k + 1], "+=" ) && isEllipsisLiteral( src, toks[k + 2] ) ) + { + return k; + } + if( k + 4 < to && tokIs( src, toks[k + 1], "." ) && tokIs( src, toks[k + 2], "append" ) && tokIs( src, toks[k + 3], "(" ) && isEllipsisLiteral( src, toks[k + 4] ) ) + { + return k; + } + } + return to; +} + +// The single-argument text of the call whose '(' is at `open`, or empty when it has more than one argument. +inline std::string singleArgText( std::string_view src, const std::vector& toks, std::size_t open ) +{ + const std::size_t close = matchClose( src, toks, open ); + if( close >= toks.size() ) + { + return {}; + } + const std::vector args = splitArgs( src, toks, open, close ); + if( args.size() != 1 || args[0].empty() ) + { + return {}; + } + return joinTokens( src, toks, args[0].first, args[0].last ); +} + +// substr( 0 , N ) at the '(' `open`: N's text, or empty. +inline std::string substrZeroLength( std::string_view src, const std::vector& toks, std::size_t open ) +{ + const std::size_t close = matchClose( src, toks, open ); + if( close >= toks.size() ) + { + return {}; + } + const std::vector args = splitArgs( src, toks, open, close ); + if( args.size() != 2 || args[0].empty() || args[1].empty() || args[0].first != args[0].last || !tokIs( src, toks[ args[0].first ], "0" ) ) + { + return {}; + } + return joinTokens( src, toks, args[1].first, args[1].last ); +} + +inline void scanUtf8CutFunction( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to, std::vector& out ) +{ + if( hasUtf8Guard( src, toks, from, to ) ) + { + return; + } + for( std::size_t k = from; k + 3 < to; ++k ) + { + if( toks[k].kind != TokKind::Ident || !tokIs( src, toks[k + 1], "." ) || ( k > 0 && ( tokIs( src, toks[k - 1], "." ) || tokIs( src, toks[k - 1], "->" ) ) ) ) + { + continue; + } + const std::string_view v = tokText( src, toks[k] ); + const std::string_view method = tokText( src, toks[k + 2] ); + if( !tokIs( src, toks[k + 3], "(" ) ) + { + continue; + } + const std::size_t close = matchClose( src, toks, k + 3 ); + if( close >= to ) + { + continue; + } + // (1) V.resize( N ) / V.erase( N ), then V += ELL / V.append( ELL ) later in the function + if( method == "resize" || method == "erase" ) + { + const std::string n = singleArgText( src, toks, k + 3 ); + if( n.empty() || !lengthIsSizeBound( src, toks, from, to, v, n ) ) + { + continue; + } + const std::size_t ell = ellipsisAppendOn( src, toks, close + 1, to, v ); + if( ell < to ) + { + const std::size_t ellEnd = std::min( to - 1, ell + 2 + ( tokIs( src, toks[ell + 1], "." ) ? 3 : 0 ) ); + out.push_back( { Facet::Utf8Cut, toks[ell].begin, joinTokens( src, toks, k, close ) + " ; " + joinTokens( src, toks, ell, ellEnd ) } ); + } + continue; + } + if( method != "substr" ) + { + continue; + } + // (2) V.substr( 0, N ) + ELL, as an expression + const std::string n = substrZeroLength( src, toks, k + 3 ); + if( n.empty() || !lengthIsSizeBound( src, toks, from, to, v, n ) ) + { + continue; + } + if( close + 2 < to && tokIs( src, toks[close + 1], "+" ) && isEllipsisLiteral( src, toks[close + 2] ) ) + { + out.push_back( { Facet::Utf8Cut, toks[k].begin, joinTokens( src, toks, k, close + 2 ) } ); + continue; + } + // (3) W = V.substr( 0, N ) (or a declaration of W), then W += ELL later + if( k >= 2 && tokIs( src, toks[k - 1], "=" ) && toks[k - 2].kind == TokKind::Ident ) + { + const std::string_view w = tokText( src, toks[k - 2] ); + const std::size_t ell = ellipsisAppendOn( src, toks, close + 1, to, w ); + if( ell < to ) + { + const std::size_t ellEnd = std::min( to - 1, ell + 2 + ( tokIs( src, toks[ell + 1], "." ) ? 3 : 0 ) ); + out.push_back( { Facet::Utf8Cut, toks[ell].begin, joinTokens( src, toks, k - 2, close ) + " ; " + joinTokens( src, toks, ell, ellEnd ) } ); + } + } + } + // (4) a single-char push loop: V.push_back( c ) somewhere, and `if( V.size() OP CAP ) { … V += ELL … }` + for( std::size_t k = from; k + 2 < to; ++k ) + { + if( !tokIs( src, toks[k], "if" ) || !tokIs( src, toks[k + 1], "(" ) ) + { + continue; + } + const std::string_view v = sizeCallReceiver( src, toks, k + 2 ); + if( v.empty() || k + 7 >= to || !isCompareOp( src, toks[k + 7] ) ) + { + continue; + } + const std::size_t condClose = matchClose( src, toks, k + 1 ); + if( condClose + 1 >= to || !tokIs( src, toks[condClose + 1], "{" ) ) + { + continue; + } + const std::size_t blockClose = matchClose( src, toks, condClose + 1 ); + const std::size_t ell = ellipsisAppendOn( src, toks, condClose + 2, std::min( blockClose, to ), v ); + if( blockClose >= to || ell >= blockClose ) + { + continue; + } + bool pushes = false; + for( std::size_t p = from; p + 3 < to && !pushes; ++p ) + { + pushes = tokText( src, toks[p] ) == v && tokIs( src, toks[p + 1], "." ) && tokIs( src, toks[p + 2], "push_back" ) && tokIs( src, toks[p + 3], "(" ); + } + if( pushes ) + { + const std::size_t ellEnd = std::min( to - 1, ell + 2 + ( tokIs( src, toks[ell + 1], "." ) ? 3 : 0 ) ); + out.push_back( { Facet::Utf8Cut, toks[ell].begin, joinTokens( src, toks, k, condClose ) + " { " + joinTokens( src, toks, ell, ellEnd ) } ); + } + } +} + +// ─── dedup-first, C++ ─────────────────────────────────────────────────────────────────────────────────── + +inline bool isSeverityName( std::string_view field ) +{ + for( const std::string& t : nameTokens( field ) ) + { + if( t == "sev" || t == "severity" || t == "priority" || t == "prio" ) + { + return true; + } + } + return false; +} + +// The member fields named in `struct T { … }` / `class T { … }` bodies in this file: the severity-like ones. +// A member is an identifier followed by '=', ';', '{' or '[' at the body's top level. +inline std::vector severityFieldsOf( std::string_view src, const std::vector& toks, std::string_view type ) +{ + std::vector out; + for( std::size_t k = 0; k + 2 < toks.size(); ++k ) + { + if( !( tokIs( src, toks[k], "struct" ) || tokIs( src, toks[k], "class" ) ) || tokText( src, toks[k + 1] ) != type ) + { + continue; + } + std::size_t open = k + 2; + while( open < toks.size() && !tokIs( src, toks[open], "{" ) && !tokIs( src, toks[open], ";" ) ) + { + ++open; + } + if( open >= toks.size() || !tokIs( src, toks[open], "{" ) ) + { + continue; + } + const std::size_t close = matchClose( src, toks, open ); + int depth = 0; + for( std::size_t m = open + 1; m + 1 < close && close < toks.size(); ++m ) + { + if( tokIs( src, toks[m], "{" ) || tokIs( src, toks[m], "(" ) ) + { + ++depth; + } + else if( tokIs( src, toks[m], "}" ) || tokIs( src, toks[m], ")" ) ) + { + --depth; + } + else if( depth == 0 && toks[m].kind == TokKind::Ident + && ( tokIs( src, toks[m + 1], "=" ) || tokIs( src, toks[m + 1], ";" ) || tokIs( src, toks[m + 1], "{" ) || tokIs( src, toks[m + 1], "[" ) ) + && isSeverityName( tokText( src, toks[m] ) ) ) + { + out.emplace_back( tokText( src, toks[m] ) ); + } + } + } + return out; +} + +// Does any `. FIELD` / `-> FIELD` for a field in `fields` occur in [from, to]? +inline bool readsAnyField( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to, const std::vector& fields ) +{ + for( std::size_t k = from + 1; k <= to && k < toks.size(); ++k ) + { + if( ( tokIs( src, toks[k - 1], "." ) || tokIs( src, toks[k - 1], "->" ) ) + && std::find( fields.begin(), fields.end(), tokText( src, toks[k] ) ) != fields.end() ) + { + return true; + } + } + return false; +} + +// The element type of a lambda `[ … ]( const T& a, … )` starting at `lb`: T, or empty (auto, a builtin, not a +// lambda). Returns the index of the lambda's closing '}' in `bodyEnd`. +inline std::string_view lambdaElementType( std::string_view src, const std::vector& toks, std::size_t lb, std::size_t& bodyEnd ) +{ + bodyEnd = toks.size(); + if( lb >= toks.size() || !tokIs( src, toks[lb], "[" ) ) + { + return {}; + } + const std::size_t capClose = matchClose( src, toks, lb ); + if( capClose + 1 >= toks.size() || !tokIs( src, toks[capClose + 1], "(" ) ) + { + return {}; + } + const std::size_t parClose = matchClose( src, toks, capClose + 1 ); + std::size_t body = parClose + 1; + while( body < toks.size() && !tokIs( src, toks[body], "{" ) && !tokIs( src, toks[body], ";" ) && !tokIs( src, toks[body], ")" ) ) + { + ++body; + } + if( body >= toks.size() || !tokIs( src, toks[body], "{" ) ) + { + return {}; + } + bodyEnd = matchClose( src, toks, body ); + // the first parameter: the identifier right before '&' / '*' / the parameter name + std::string_view type; + for( std::size_t k = capClose + 2; k < parClose && !tokIs( src, toks[k], "," ); ++k ) + { + if( toks[k].kind != TokKind::Ident ) + { + continue; + } + const std::string_view w = tokText( src, toks[k] ); + if( w == "const" || w == "volatile" || w == "std" || w == "struct" ) + { + continue; + } + if( type.empty() ) + { + type = w; + } + else if( k + 1 < parClose && tokIs( src, toks[k - 1], "::" ) ) + { + type = w; // a qualified type: keep its last segment + } + } + if( type == "auto" || type == "int" || type == "long" || type == "unsigned" || type == "char" || type == "double" || type == "float" || type == "bool" + || type == "size_t" ) + { + return {}; + } + return type; +} + +inline void scanDedupFirstFunction( std::string_view src, const std::vector& toks, std::size_t from, std::size_t to, std::vector& out ) +{ + for( std::size_t k = from + 2; k + 1 < to; ++k ) + { + if( !tokIs( src, toks[k], "unique" ) || !tokIs( src, toks[k - 1], "::" ) || !tokIs( src, toks[k + 1], "(" ) ) + { + continue; + } + const bool ranges = tokIs( src, toks[k - 2], "ranges" ); + if( !ranges && !tokIs( src, toks[k - 2], "std" ) ) + { + continue; + } + const std::size_t close = matchClose( src, toks, k + 1 ); + if( close >= to ) + { + continue; + } + const std::vector args = splitArgs( src, toks, k + 1, close ); + const std::size_t predAt = ranges ? ( args.size() == 2 ? 1 : 2 ) : 2; + if( predAt >= args.size() || args[predAt].empty() ) + { + continue; // no predicate: whole values compare + } + std::size_t lambdaEnd = 0; + const std::string_view type = lambdaElementType( src, toks, args[predAt].first, lambdaEnd ); + if( type.empty() || lambdaEnd > close ) + { + continue; // a named predicate, an auto / builtin element: skipped, never guessed + } + const std::vector sev = severityFieldsOf( src, toks, type ); + if( sev.empty() || readsAnyField( src, toks, args[predAt].first, lambdaEnd, sev ) ) + { + continue; + } + // a sort before the unique, in this function, whose arguments read the severity field (either direction) + bool sorted = false; + for( std::size_t s = from; s < k && !sorted; ++s ) + { + const std::string_view w = tokText( src, toks[s] ); + if( ( w == "sort" || w == "stable_sort" ) && s + 1 < k && tokIs( src, toks[s + 1], "(" ) ) + { + const std::size_t sc = matchClose( src, toks, s + 1 ); + sorted = sc < k && readsAnyField( src, toks, s + 1, sc, sev ); + } + } + if( !sorted ) + { + out.push_back( { Facet::DedupFirst, toks[k - 2].begin, joinTokens( src, toks, k - ( ranges ? 4 : 2 ), close ) } ); + } + } +} + +// ─── vacuous-assert, Bash test scripts ────────────────────────────────────────────────────────────────── + +// One logical line of a shell script: continuation lines joined, comments dropped, heredoc bodies and the +// tails of multi-line quotes removed (they are data, not code). +struct ShLine +{ + std::uint32_t startByte = 0; + std::string text; +}; + +inline std::vector shLogicalLines( std::string_view s ) +{ + std::vector out; + std::vector heredocEnds; // pending terminators, in order + bool heredocStrip = false; + char openQuote = 0; // a quote left open at the end of the previous physical line + std::size_t i = 0; + ShLine cur; + bool haveCur = false; + while( i < s.size() ) + { + std::size_t e = s.find( '\n', i ); + if( e == std::string_view::npos ) + { + e = s.size(); + } + std::string_view phys = s.substr( i, e - i ); + const std::uint32_t physStart = static_cast( i ); + i = e + 1; + if( !heredocEnds.empty() ) + { + std::string_view t = phys; + while( heredocStrip && !t.empty() && t.front() == '\t' ) + { + t.remove_prefix( 1 ); + } + if( t == heredocEnds.front() ) + { + heredocEnds.erase( heredocEnds.begin() ); + } + continue; + } + // code bytes of this physical line: quotes tracked, comments dropped + std::string code; + std::size_t k = 0; + if( openQuote != 0 ) + { + const std::size_t close = phys.find( openQuote ); + if( close == std::string_view::npos ) + { + continue; // the whole line is inside a multi-line literal + } + k = close + 1; + openQuote = 0; + code.append( "\"\"" ); + } + char q = 0; + for( ; k < phys.size(); ++k ) + { + const char c = phys[k]; + if( q == 0 && c == '#' && ( k == 0 || isSpace( phys[k - 1] ) || phys[k - 1] == ';' ) ) + { + break; + } + if( c == '\\' && q != '\'' && k + 1 < phys.size() ) + { + code.push_back( c ); + code.push_back( phys[k + 1] ); + ++k; + continue; + } + if( q == 0 && ( c == '\'' || c == '"' ) ) + { + q = c; + } + else if( q == c ) + { + q = 0; + } + code.push_back( c ); + } + if( q != 0 ) + { + openQuote = q; // a literal that runs on: the rest of it is data + } + // heredoc operators on this line (not here-strings) + for( std::size_t h = code.find( "<<" ); h != std::string::npos; h = code.find( "<<", h + 2 ) ) + { + if( h + 2 < code.size() && code[h + 2] == '<' ) + { + h += 1; + continue; + } + std::size_t p = h + 2; + const bool strip = p < code.size() && code[p] == '-'; + p += strip ? 1 : 0; + while( p < code.size() && code[p] == ' ' ) + { + ++p; + } + std::string word; + while( p < code.size() && !isSpace( code[p] ) && code[p] != ';' && code[p] != ')' && code[p] != '|' && code[p] != '&' && code[p] != '>' ) + { + if( code[p] != '\'' && code[p] != '"' && code[p] != '\\' ) + { + word.push_back( code[p] ); + } + ++p; + } + // a terminator is a word ( << 2 inside $(( )) is a shift, not a heredoc ) + if( !word.empty() && isIdentStart( word[0] ) && std::all_of( word.begin(), word.end(), []( char ch ) { return isIdentChar( ch ); } ) ) + { + heredocEnds.push_back( word ); + heredocStrip = strip; + } + } + const bool continues = !code.empty() && code.back() == '\\' && openQuote == 0; + if( continues ) + { + code.pop_back(); + } + if( !haveCur ) + { + cur = ShLine{ physStart, {} }; + haveCur = true; + } + cur.text.append( code ); + if( continues && heredocEnds.empty() ) + { + cur.text.push_back( ' ' ); + continue; + } + out.push_back( std::move( cur ) ); + cur = ShLine{}; + haveCur = false; + } + if( haveCur ) + { + out.push_back( std::move( cur ) ); + } + return out; +} + +// A shell word or operator, quote- and $( )-aware. +struct ShTok +{ + std::string text; + bool op = false; +}; + +inline std::vector shTokens( std::string_view l ) +{ + std::vector out; + std::size_t i = 0; + while( i < l.size() ) + { + const char c = l[i]; + if( isSpace( c ) ) + { + ++i; + continue; + } + if( l.substr( i, 2 ) == "&&" || l.substr( i, 2 ) == "||" || l.substr( i, 2 ) == ";;" ) + { + out.push_back( { std::string( l.substr( i, 2 ) ), true } ); + i += 2; + continue; + } + if( c == '|' || c == ';' || c == '&' || ( c == '!' && ( i + 1 >= l.size() || isSpace( l[i + 1] ) ) ) ) + { + out.push_back( { std::string( 1, c ), true } ); + ++i; + continue; + } + std::string w; + int paren = 0; + char q = 0; + while( i < l.size() ) + { + const char d = l[i]; + if( q == 0 && paren == 0 && ( isSpace( d ) || d == '|' || d == ';' || d == '&' ) ) + { + break; + } + if( d == '\\' && q != '\'' && i + 1 < l.size() ) + { + w.push_back( d ); + w.push_back( l[i + 1] ); + i += 2; + continue; + } + // A "$( … )" capture's inner quotes are balanced, so plain toggling keeps the word whole; an UNQUOTED + // $( … ) with spaces inside is held together by the paren depth. + if( q == 0 && d == '$' && i + 1 < l.size() && l[i + 1] == '(' ) + { + ++paren; + w.append( "$(" ); + i += 2; + continue; + } + if( q == 0 && paren > 0 && d == '(' ) + { + ++paren; + } + else if( q == 0 && paren > 0 && d == ')' ) + { + --paren; + } + else if( q == 0 && ( d == '\'' || d == '"' ) ) + { + q = d; + } + else if( q != 0 && d == q ) + { + q = 0; + } + w.push_back( d ); + ++i; + } + out.push_back( { std::move( w ), false } ); + } + return out; +} + +using ShLineToks = std::vector>; // every logical line's tokens, computed once per script + +inline std::string_view unquoted( std::string_view w ) noexcept +{ + if( w.size() >= 2 && ( w.front() == '"' || w.front() == '\'' ) && w.back() == w.front() ) + { + return w.substr( 1, w.size() - 2 ); + } + return w; +} + +// "$VAR" / "${VAR}" / $VAR → VAR, else empty. +inline std::string_view varRef( std::string_view w ) noexcept +{ + w = unquoted( w ); + if( w.size() < 2 || w[0] != '$' ) + { + return {}; + } + w.remove_prefix( 1 ); + if( !w.empty() && w.front() == '{' && w.back() == '}' ) + { + w = w.substr( 1, w.size() - 2 ); + } + if( w.empty() || !isIdentStart( w[0] ) ) + { + return {}; + } + for( char c : w ) + { + if( !isIdentChar( c ) ) + { + return {}; + } + } + return w; +} + +enum class Reporter : std::uint8_t +{ + None, + Pass, + Fail, +}; + +// A reporter is named by its tokens (ok / pass vs no / fail …), or is an echo / printf of PASS / FAIL text, +// an exit / return with a nonzero status, or a fail-flag assignment. +inline Reporter reporterOf( const std::vector& t, std::size_t k, const std::vector>& defined ) +{ + while( k < t.size() && !t[k].op && ( t[k].text == "{" || t[k].text == "then" || t[k].text == "else" ) ) + { + ++k; + } + if( k >= t.size() || t[k].op ) + { + return Reporter::None; + } + const std::string& w = t[k].text; + for( const auto& [ name, r ] : defined ) + { + if( name == w && r != Reporter::None ) + { + return r; + } + } + if( w == "echo" || w == "printf" ) + { + for( std::size_t a = k + 1; a < t.size() && !t[a].op; ++a ) + { + std::string_view u = unquoted( t[a].text ); + while( !u.empty() && ( u.front() == ' ' || u.front() == '[' ) ) + { + u.remove_prefix( 1 ); + } + if( u.starts_with( "FAIL" ) ) + { + return Reporter::Fail; + } + if( u.starts_with( "PASS" ) || u.starts_with( "ok " ) ) + { + return Reporter::Pass; + } + } + return Reporter::None; + } + if( ( w == "exit" || w == "return" ) && k + 1 < t.size() && !t[k + 1].op && t[k + 1].text != "0" ) + { + return Reporter::Fail; + } + if( w.find( '=' ) != std::string::npos ) + { + const std::string lhs = w.substr( 0, w.find( '=' ) ); + const std::string rhs = w.substr( w.find( '=' ) + 1 ); + for( const std::string& tk : nameTokens( lhs ) ) + { + if( ( tk == "fail" || tk == "failed" || tk == "failures" || tk == "status" ) && rhs != "0" ) + { + return Reporter::Fail; + } + } + return Reporter::None; + } + for( const std::string& tk : nameTokens( w ) ) + { + if( tk == "ok" || tk == "pass" || tk == "passed" || tk == "good" ) + { + return Reporter::Pass; + } + if( tk == "no" || tk == "fail" || tk == "failed" || tk == "bad" || tk == "die" ) + { + return Reporter::Fail; + } + } + return Reporter::None; +} + +// The grep -q assertion of one pipeline [b, e) of tokens: its producer and polarity. +struct GrepPipe +{ + bool ok = false; + bool negated = false; // a leading `!` + bool direct = false; // the producer is a command (not a variable's text) + std::string var; // the variable whose text is grepped, when not direct +}; + +inline bool isQuietGrep( const std::vector& t, std::size_t b, std::size_t e, std::size_t& operands ) +{ + if( b >= e || t[b].op || t[b].text != "grep" ) + { + return false; + } + bool quiet = false; + operands = 0; + bool afterDashE = false; + for( std::size_t k = b + 1; k < e; ++k ) + { + const std::string& w = t[k].text; + if( afterDashE ) + { + ++operands; + afterDashE = false; + continue; + } + if( w == "--quiet" || w == "--silent" ) + { + quiet = true; + } + else if( w == "-e" ) + { + afterDashE = true; + } + else if( w.size() > 1 && w[0] == '-' && w[1] != '-' ) + { + quiet = quiet || w.find( 'q' ) != std::string::npos; + } + else if( w == "--" ) + { + continue; + } + else if( w.rfind( "<<<", 0 ) == 0 || w.rfind( "2>", 0 ) == 0 || w.rfind( ">", 0 ) == 0 ) + { + continue; + } + else + { + ++operands; + } + } + return quiet; +} + +inline GrepPipe grepPipe( const std::vector& t, std::size_t b, std::size_t e ) +{ + GrepPipe g; + if( b < e && t[b].op && t[b].text == "!" ) + { + g.negated = true; + ++b; + } + // stage boundaries + std::vector stages{ b }; + for( std::size_t k = b; k < e; ++k ) + { + if( t[k].op && t[k].text == "|" ) + { + stages.push_back( k + 1 ); + } + } + const std::size_t last = stages.back(); + std::size_t operands = 0; + if( !isQuietGrep( t, last, e, operands ) ) + { + return g; + } + if( stages.size() == 1 ) + { + // grep -q PAT <<< "$V" reads a variable; grep -q PAT FILE reads a file (not this shape) + for( std::size_t k = last + 1; k < e; ++k ) + { + if( t[k].text == "<<<" && k + 1 < e ) + { + g.var = std::string( varRef( t[k + 1].text ) ); + } + else if( t[k].text.rfind( "<<<", 0 ) == 0 ) + { + g.var = std::string( varRef( std::string_view( t[k].text ).substr( 3 ) ) ); + } + } + g.ok = !g.var.empty(); + return g; + } + if( operands > 1 ) + { + return g; // the last grep reads files, not the pipe + } + const std::size_t p0 = stages.front(); + const std::size_t p0e = stages[1] - 1; + if( p0 >= p0e ) + { + return g; + } + const std::string& cmd = t[p0].text; + if( cmd == "printf" || cmd == "echo" ) + { + // printf '%s' "$V" / echo "$V" — the text of one variable; anything else printed is not a run's output + std::string_view v; + std::size_t vars = 0; + for( std::size_t k = p0 + 1; k < p0e; ++k ) + { + const std::string_view r = varRef( t[k].text ); + if( !r.empty() ) + { + v = r; + ++vars; + } + else if( t[k].text.find( "$(" ) != std::string::npos ) + { + g.direct = true; + } + } + if( g.direct ) + { + g.ok = true; + return g; + } + if( vars != 1 ) + { + return g; + } + g.var = std::string( v ); + g.ok = true; + return g; + } + if( cmd == "cat" || cmd == "true" || cmd == "false" || cmd == ":" ) + { + return g; // a file's content or a constant: not a run under test + } + g.direct = true; + g.ok = true; + return g; +} + +// The shape of one list: `PIPE && R1 || R2`. Returns the grep pipe and the two reporters. +struct AndOr +{ + GrepPipe pipe; + Reporter onTrue = Reporter::None; + Reporter onFalse = Reporter::None; +}; + +inline bool andOrShape( const std::vector& t, const std::vector>& defined, AndOr& out ) +{ + std::size_t andAt = t.size(); + std::size_t orAt = t.size(); + for( std::size_t k = 0; k < t.size(); ++k ) + { + if( t[k].op && t[k].text == "&&" && andAt == t.size() ) + { + andAt = k; + } + else if( t[k].op && t[k].text == "||" && andAt != t.size() && orAt == t.size() ) + { + orAt = k; + } + else if( t[k].op && ( t[k].text == ";" || t[k].text == ";;" ) && andAt == t.size() ) + { + return false; + } + } + if( andAt == t.size() || orAt == t.size() ) + { + return false; + } + out.pipe = grepPipe( t, 0, andAt ); + out.onTrue = reporterOf( t, andAt + 1, defined ); + out.onFalse = reporterOf( t, orAt + 1, defined ); + return out.pipe.ok; +} + +// Is a list an ABSENCE assertion (the grep matching is the failure branch)? Or a REQUIREMENT on the same +// text (a match is the pass branch, or no match is the failure branch)? +enum class Polarity : std::uint8_t +{ + None, + Absence, + Requirement, +}; + +inline Polarity polarityOf( bool negated, Reporter onMatch, Reporter onNoMatch ) noexcept +{ + if( negated ) + { + std::swap( onMatch, onNoMatch ); + } + if( onMatch == Reporter::Fail && onNoMatch == Reporter::Pass ) + { + return Polarity::Absence; + } + if( onMatch == Reporter::Pass || onNoMatch == Reporter::Fail ) + { + return Polarity::Requirement; + } + return Polarity::None; +} + +// Every grep assertion in a script: the line, its pipe, its polarity. +struct ShAssert +{ + std::size_t line = 0; + GrepPipe pipe; + Polarity pol = Polarity::None; +}; + +inline std::vector shAssertions( const ShLineToks& lt, const std::vector>& defined ) +{ + std::vector out; + for( std::size_t li = 0; li < lt.size(); ++li ) + { + const std::vector& t = lt[li]; + if( t.empty() ) + { + continue; + } + // case arm: `pattern) LIST ;;` — read the list after the arm's ')' + std::size_t from = 0; + if( !t[0].op && t[0].text.size() > 1 && t[0].text.back() == ')' && t[0].text.find( "$(" ) == std::string::npos ) + { + from = 1; + } + if( !t[from].op && ( t[from].text == "if" || t[from].text == "elif" || t[from].text == "while" ) ) + { + std::size_t thenAt = t.size(); + for( std::size_t k = from + 1; k < t.size(); ++k ) + { + if( !t[k].op && t[k].text == "then" ) + { + thenAt = k; + break; + } + } + std::size_t condEnd = from + 1; + while( condEnd < t.size() && !( t[condEnd].op && ( t[condEnd].text == ";" || t[condEnd].text == "&&" || t[condEnd].text == "||" ) ) ) + { + ++condEnd; + } + const GrepPipe g = grepPipe( t, from + 1, condEnd ); + if( !g.ok || ( condEnd < t.size() && t[condEnd].op && ( t[condEnd].text == "&&" || t[condEnd].text == "||" ) ) ) + { + continue; + } + // the then-branch's first command: on this line after `then`, or the next logical line + Reporter onThen = Reporter::None; + if( thenAt + 1 < t.size() ) + { + onThen = reporterOf( t, thenAt + 1, defined ); + } + else if( li + 1 < lt.size() ) + { + const std::vector& n = lt[li + 1]; + onThen = reporterOf( n, ( !n.empty() && n[0].text == "then" ) ? 1 : 0, defined ); + if( onThen == Reporter::None && !n.empty() && n[0].text == "then" && n.size() == 1 && li + 2 < lt.size() ) + { + onThen = reporterOf( lt[li + 2], 0, defined ); + } + } + // an if whose then-branch reports failure is an absence when the else-branch (or nothing) passes; + // whose then-branch does anything else, it REQUIRES the text (a guard for an absence nested in it). + Polarity pol = Polarity::Requirement; + if( onThen == Reporter::Fail && !g.negated ) + { + pol = Polarity::Absence; + } + else if( onThen == Reporter::Pass && g.negated ) + { + pol = Polarity::Absence; + } + else if( onThen == Reporter::Fail && g.negated ) + { + pol = Polarity::Requirement; + } + out.push_back( { li, g, pol } ); + continue; + } + std::vector list( t.begin() + static_cast( from ), t.end() ); + AndOr ao; + if( andOrShape( list, defined, ao ) ) + { + out.push_back( { li, ao.pipe, polarityOf( ao.pipe.negated, ao.onTrue, ao.onFalse ) } ); + continue; + } + // `PIPE || R` / `PIPE && R` alone + for( std::size_t k = 0; k < list.size(); ++k ) + { + if( list[k].op && ( list[k].text == "||" || list[k].text == "&&" ) ) + { + const GrepPipe g = grepPipe( list, 0, k ); + const Reporter r = reporterOf( list, k + 1, defined ); + if( g.ok && r != Reporter::None ) + { + const bool onMatch = list[k].text == "&&"; + out.push_back( { li, g, polarityOf( g.negated, onMatch ? r : Reporter::None, onMatch ? Reporter::None : r ) } ); + } + break; + } + } + } + return out; +} + +// The innermost span (function) holding `byte`, or spans.size() for top-level code. +inline std::size_t scopeOf( const std::vector& spans, std::uint32_t byte ) noexcept +{ + std::size_t best = spans.size(); + std::uint32_t bestLen = UINT32_MAX; + for( std::size_t k = 0; k < spans.size(); ++k ) + { + if( byte >= spans[k].begin && byte < spans[k].end && spans[k].end - spans[k].begin < bestLen ) + { + best = k; + bestLen = spans[k].end - spans[k].begin; + } + } + return best; +} + +// The assignment of `v` that reaches line `li` within its scope: the last such line before it, or npos. +inline std::size_t assignmentOf( const ShLineToks& lt, const std::vector& scope, std::size_t li, std::string_view v ) +{ + const std::string eq = std::string( v ) + "="; + for( std::size_t k = li; k-- > 0; ) + { + if( scope[k] != scope[li] ) + { + continue; + } + const std::vector& t = lt[k]; + for( std::size_t w = 0; w < t.size(); ++w ) + { + if( !t[w].op && t[w].text.rfind( eq, 0 ) == 0 ) + { + return k; + } + } + } + return std::string::npos; +} + +// Is the variable's capture guarded — its failure checked or its content required somewhere in scope? +inline bool captureGuarded( const std::vector& lines, const ShLineToks& lt, const std::vector& scope, + const std::vector& asserts, std::size_t absenceLine, std::size_t assignLine, std::string_view v, bool errexit ) +{ + const std::string eq = std::string( v ) + "="; + const std::vector& t = lt[assignLine]; + bool plainAssign = false; + std::size_t at = t.size(); + for( std::size_t w = 0; w < t.size(); ++w ) + { + if( !t[w].op && t[w].text.rfind( eq, 0 ) == 0 ) + { + at = w; + plainAssign = w == 0 || ( t[w - 1].op ) || t[w - 1].text == "if" || t[w - 1].text == "elif" || t[w - 1].text == "while"; + break; + } + } + // if ! V="$( … )"; then / while V=… — the rc decides a branch + if( at > 0 && at < t.size() && ( t[at - 1].text == "if" || t[at - 1].text == "elif" || t[at - 1].text == "while" || ( t[at - 1].op && t[at - 1].text == "!" ) ) ) + { + return true; + } + // V="$( … )" || … / && … on the capture, or an rc read right after it on the same line + for( std::size_t w = at + 1; w < t.size(); ++w ) + { + if( t[w].op && ( t[w].text == "||" || t[w].text == "&&" ) ) + { + return true; + } + if( t[w].text.find( "$?" ) != std::string::npos || t[w].text.find( "PIPESTATUS" ) != std::string::npos ) + { + return true; + } + } + // the next line in scope reads $? / PIPESTATUS + for( std::size_t k = assignLine + 1; k < lines.size(); ++k ) + { + if( scope[k] != scope[assignLine] ) + { + continue; + } + if( lines[k].text.find( "$?" ) != std::string::npos || lines[k].text.find( "PIPESTATUS" ) != std::string::npos ) + { + return true; + } + break; + } + // errexit aborts the script on a plain capture's failure (local / declare / export mask the rc) + if( errexit && plainAssign ) + { + return true; + } + // a presence test, a case on the text, or a requirement on the text anywhere in scope + const std::string q1 = "\"$" + std::string( v ) + "\""; + const std::string q2 = "\"${" + std::string( v ) + "}\""; + const std::string q3 = "$" + std::string( v ); + const std::string q4 = "${#" + std::string( v ) + "}"; + for( std::size_t k = 0; k < lines.size(); ++k ) + { + if( scope[k] != scope[absenceLine] || k == absenceLine ) + { + continue; + } + const std::string& l = lines[k].text; + for( const char* test : { "-n ", "-z " } ) + { + for( std::size_t p = l.find( test ); p != std::string::npos; p = l.find( test, p + 1 ) ) + { + const std::string_view rest = std::string_view( l ).substr( p + 3 ); + if( rest.starts_with( q1 ) || rest.starts_with( q2 ) || ( rest.starts_with( q3 ) && ( rest.size() == q3.size() || !isIdentChar( rest[q3.size()] ) ) ) ) + { + return true; + } + } + } + if( l.find( q4 ) != std::string::npos ) + { + return true; + } + const std::vector& ct = lt[k]; + if( ct.size() >= 3 && ct[0].text == "case" && varRef( ct[1].text ) == v ) + { + return true; + } + } + for( const ShAssert& a : asserts ) + { + if( a.line != absenceLine && scope[a.line] == scope[absenceLine] && a.pol == Polarity::Requirement && !a.pipe.direct && a.pipe.var == v ) + { + return true; + } + } + return false; +} + +// Does the absence's own line read an rc ($?, or a variable named like rc / status) — `if [ "$RC" = 0 ] && ! …`? +inline bool lineReadsRc( std::string_view l ) +{ + if( l.find( "$?" ) != std::string_view::npos || l.find( "PIPESTATUS" ) != std::string_view::npos ) + { + return true; + } + for( std::size_t p = l.find( '$' ); p != std::string_view::npos; p = l.find( '$', p + 1 ) ) + { + std::size_t q = p + 1; + if( q < l.size() && l[q] == '{' ) + { + ++q; + } + std::size_t e = q; + while( e < l.size() && isIdentChar( l[e] ) ) + { + ++e; + } + for( const std::string& tk : nameTokens( l.substr( q, e - q ) ) ) + { + if( tk == "rc" || tk == "status" || tk == "ret" || tk == "exit" ) + { + return true; + } + } + } + return false; +} + +// Script-defined reporter functions, classified by name tokens, else by body signal (prints FAIL / sets a +// failure flag → fail; prints PASS → pass). +inline std::vector> shDefinedReporters( std::string_view src, const std::vector& lines, const ShLineToks& lt, + const std::vector& spans ) +{ + std::vector> out; + for( std::size_t li = 0; li < lines.size(); ++li ) + { + const ShLine& l = lines[li]; + const std::vector& t = lt[li]; + if( t.empty() || t[0].op ) + { + continue; + } + std::string name; + if( t[0].text == "function" && t.size() > 1 ) + { + name = t[1].text; + } + else if( t[0].text.size() > 2 && t[0].text.find( "()" ) != std::string::npos && t[0].text.find( '$' ) == std::string::npos ) + { + name = t[0].text.substr( 0, t[0].text.find( "()" ) ); + } + else if( t.size() > 1 && t[1].text.rfind( "()", 0 ) == 0 ) + { + name = t[0].text; + } + while( !name.empty() && name.back() == '(' ) + { + name.pop_back(); + } + if( name.empty() || !std::all_of( name.begin(), name.end(), []( char c ) { return isIdentChar( c ) || c == '-'; } ) ) + { + continue; + } + std::vector probe{ { name, false } }; + Reporter r = reporterOf( probe, 0, {} ); + if( r == Reporter::None ) + { + const std::size_t s = scopeOf( spans, l.startByte ); + const std::string_view body = s < spans.size() ? src.substr( spans[s].begin, spans[s].end - spans[s].begin ) : std::string_view( l.text ); + if( body.find( "FAIL" ) != std::string_view::npos || body.find( "fail=1" ) != std::string_view::npos ) + { + r = Reporter::Fail; + } + else if( body.find( "PASS" ) != std::string_view::npos ) + { + r = Reporter::Pass; + } + } + out.emplace_back( name, r ); + } + return out; +} + +// errexit state at each logical line: a top-level `set -e…` / `set -o errexit` turns it on, `set +e` off. +inline std::vector shErrexit( const std::vector& lines, const ShLineToks& lt ) +{ + std::vector out( lines.size(), 0 ); + bool on = false; + if( !lines.empty() && lines[0].text.rfind( "#!", 0 ) == 0 ) + { + on = lines[0].text.find( " -e" ) != std::string::npos; + } + for( std::size_t k = 0; k < lines.size(); ++k ) + { + const std::vector& t = lt[k]; + for( std::size_t w = 0; w + 1 < t.size(); ++w ) + { + if( t[w].op || t[w].text != "set" ) + { + continue; + } + for( std::size_t a = w + 1; a < t.size() && !t[a].op; ++a ) + { + const std::string& f = t[a].text; + if( f.size() > 1 && f[0] == '-' && f[1] != 'o' && f.find( 'e' ) != std::string::npos ) + { + on = true; + } + else if( f.size() > 1 && f[0] == '+' && f.find( 'e' ) != std::string::npos ) + { + on = false; + } + else if( f == "-o" && a + 1 < t.size() && t[a + 1].text == "errexit" ) + { + on = true; + } + } + } + out[k] = on ? 1 : 0; + } + return out; +} + +inline std::string collapseSpaces( std::string_view s ) +{ + std::string out; + bool space = false; + for( char c : s ) + { + if( isSpace( c ) ) + { + space = !out.empty(); + continue; + } + if( space ) + { + out.push_back( ' ' ); + space = false; + } + out.push_back( c ); + } + return out; +} + +inline void scanVacuousAssertBash( std::string_view src, const std::vector& spans, std::vector& out ) +{ + const std::vector lines = shLogicalLines( src ); + if( lines.empty() ) + { + return; + } + std::vector scope( lines.size() ); + ShLineToks lt( lines.size() ); + for( std::size_t k = 0; k < lines.size(); ++k ) + { + scope[k] = scopeOf( spans, lines[k].startByte ); + lt[k] = shTokens( lines[k].text ); + } + const std::vector> defined = shDefinedReporters( src, lines, lt, spans ); + const std::vector asserts = shAssertions( lt, defined ); + const std::vector errexit = shErrexit( lines, lt ); + for( const ShAssert& a : asserts ) + { + if( a.pol != Polarity::Absence || lineReadsRc( lines[a.line].text ) ) + { + continue; + } + bool vacuous = a.pipe.direct; + if( !vacuous ) + { + // follow a derived capture (V="$( printf '%s' "$W" | … )") back to its source, up to three steps + std::string var = a.pipe.var; + bool guard = false; + bool found = false; + for( int step = 0; step < 3 && !guard; ++step ) + { + const std::size_t as = assignmentOf( lt, scope, a.line, var ); + if( as == std::string::npos ) + { + break; + } + const std::string& al = lines[as].text; + const std::size_t eq = al.find( var + "=" ); + const std::string_view rhs = std::string_view( al ).substr( eq + var.size() + 1 ); + if( rhs.find( "$(" ) == std::string_view::npos && rhs.find( '`' ) == std::string_view::npos ) + { + break; // a literal or a copy: not a run under test + } + guard = captureGuarded( lines, lt, scope, asserts, a.line, as, var, errexit[as] != 0 ); + found = true; + // derived from another variable's text? + const std::vector inner = shTokens( rhs.substr( rhs.find( "$(" ) + 2 ) ); + std::string next; + if( !inner.empty() && ( inner[0].text == "printf" || inner[0].text == "echo" ) ) + { + for( std::size_t w = 1; w < inner.size() && !inner[w].op; ++w ) + { + const std::string_view r = varRef( inner[w].text ); + if( !r.empty() ) + { + next = std::string( r ); + } + } + } + if( next.empty() ) + { + break; + } + var = next; + } + vacuous = found && !guard; + } + if( vacuous ) + { + out.push_back( { Facet::VacuousAssert, lines[a.line].startByte, collapseSpaces( lines[a.line].text ) } ); + } + } +} + +} // namespace detail + +// ─── the per-language entry points ────────────────────────────────────────────────────────────────────── + +// C++: format-arity over the whole file; utf8-cut and dedup-first inside each function span. A site found +// in nested spans is reported once (the scan of each span is independent; the caller anchors by byte). +inline std::vector scanCpp( std::string_view src, const std::vector& fnSpans ) +{ + std::vector out; + const std::vector toks = detail::lexCpp( src ); + detail::scanFormatArityCpp( src, toks, out ); + for( const Span& sp : fnSpans ) + { + const auto first = std::lower_bound( toks.begin(), toks.end(), sp.begin, []( const detail::Tok& t, std::uint32_t b ) { return t.begin < b; } ); + const auto last = std::lower_bound( toks.begin(), toks.end(), sp.end, []( const detail::Tok& t, std::uint32_t b ) { return t.begin < b; } ); + const std::size_t from = static_cast( first - toks.begin() ); + const std::size_t to = static_cast( last - toks.begin() ); + if( from >= to ) + { + continue; + } + detail::scanUtf8CutFunction( src, toks, from, to, out ); + detail::scanDedupFirstFunction( src, toks, from, to, out ); + } + std::sort( out.begin(), out.end(), []( const Site& a, const Site& b ) { return a.startByte != b.startByte ? a.startByte < b.startByte : a.text < b.text; } ); + out.erase( std::unique( out.begin(), out.end(), []( const Site& a, const Site& b ) { return a.facet == b.facet && a.startByte == b.startByte && a.text == b.text; } ), + out.end() ); + return out; +} + +inline std::vector scanPython( std::string_view src ) +{ + std::vector out; + detail::scanFormatArityPython( src, detail::lexPython( src ), out ); + return out; +} + +// Bash, for a test-script path only (the caller decides the path): `fnSpans` are the script's function +// definitions, the scopes a capture and its guards are matched in. +inline std::vector scanBashTestScript( std::string_view src, const std::vector& fnSpans ) +{ + std::vector out; + detail::scanVacuousAssertBash( src, fnSpans, out ); + return out; +} + +// A cheap byte prefilter: can this C++ file hold any of the three C++ shapes at all? +inline bool cppMayHoldShapes( std::string_view src ) noexcept +{ + for( std::string_view needle : { std::string_view( "format" ), std::string_view( "print" ), std::string_view( "emitTo" ), std::string_view( "formatTo" ), + std::string_view( "unique" ), std::string_view( "..." ), std::string_view( "\xE2\x80\xA6" ), std::string_view( "xE2" ), + std::string_view( "u2026" ) } ) + { + if( src.find( needle ) != std::string_view::npos ) + { + return true; + } + } + return false; +} + +} // namespace rw::defectshape From c84b042f1cb2156e3199c30b81122f03f99cbdfd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:18:03 -0400 Subject: [PATCH 133/229] =?UTF-8?q?quality:=20the=20defect-shape=20kind=20?= =?UTF-8?q?=E2=80=94=20sites=20in=20the=20snapshot,=20a=20row=20per=20new?= =?UTF-8?q?=20site's=20anchor,=20one=20exit=20predicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Snapshot.defectSites: sorted (anchor, site) records per offending site plus each scanned file's presence (or unreadable) record; in the qsnap blob (scheme 17 -> 18) and the text sidecar ("defect" lines); healed across renames like the per-symbol maps. - computeDelta: a site is new when its identity (facet + normalized text) occurs more often in the current tree than in the baseline, repo-wide, so an untouched, moved or renamed defect is not new and a swap is; the excess is credited to anchors in key order, one row per anchor and facet, was/now = the anchor's sites of that facet. Rows anchor on the innermost definition or, for top-level code, the file (sym= is then the path). Ack key = defectRowKey( anchor, facet ), so one facet's ack never suppresses another. - rowGates(): THE exit predicate, asked by every surface that states it (gating= and the per-row gating attribute, --json / MCP "gating", the exit code, --ack-only=gating, scoped-out would-gate, the allow-dirty absorbed count, the stderr naming line). New-symbol rows never gate, except defect-shape format-arity: a placeholder/argument mismatch is a defect, not debt. utf8-cut, dedup-first and vacuous-assert rows are always sev="minor". - The stderr gating line keeps its bytes unless a new-code format-arity row gates. - The full legend says TWELVE KINDS and states the exception; a defect-shape sentence (printed only with such a row) names the facets, the anchors, the report-only rule and what is not checked. Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 8 +- src/quality.h | 396 +++++++++++++++++++++++++++++++++++++++++++- src/verbs_quality.h | 78 ++++++--- 3 files changed, 452 insertions(+), 30 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 7cc680277..b9d741281 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3747,7 +3747,7 @@ inline std::string connectText( const std::string& root, const std::vector qualityDeltaJson( const std::string& { ++newSymbolCount; } - else if( !r.isMinor ) + if( rw::quality::rowGates( r ) ) { ++gatingCount; } @@ -4041,9 +4041,9 @@ inline std::pair qualityDeltaJson( const std::string& { out += ",\"p\":\"" + mcpdetail::jsonEscape( r.path ) + ":" + std::to_string( r.line ) + "\""; // P2.5 locator } - if( !r.isNewSymbol && !r.isMinor ) + if( rw::quality::rowGates( r ) ) { - out += ",\"gating\":true"; // the exit predicate, per row + out += ",\"gating\":true"; // the exit predicate, per row (rowGates) } if( r.isMinor ) { diff --git a/src/quality.h b/src/quality.h index 3de7c2fae..ee963a5c0 100644 --- a/src/quality.h +++ b/src/quality.h @@ -26,6 +26,7 @@ #include "clones.h" #include "cloneidiom.h" // idiom-class demotion — the closed 3-idiom shape classifier that turns an idiom-COLLISION clone group into a minor row instead of a gating one #include "lintrules.h" // findQualityConstructs — the built-in error-masking rule table (GitClear +47% kind) + the placeholder shapes +#include "defectshape.h" // the defect-shape kind's four scanners (format-arity, utf8-cut, dedup-first, vacuous-assert) #include "arch.h" // fnv1a64 #include "pathguard.h" // CWE-59/367: rw::pathguard::openNoFollowTruncate — writeBaseline truncates, so its open must refuse a link atomically #include "gitmine.h" // shSingleQuote + gitFileCommitCountsInDayWindow — short-horizon-churn window mining @@ -249,6 +250,21 @@ inline std::string baselineCanonId( const IngestResult& ing, NodeId i, std::stri return canonicalIdRelTo( ing, ing.symbols[i], root ); } +// The defect-shape kind's per-site record. `anchor` is the definition the site sits in (its qualityKey) or, for +// top-level code, defectFileAnchor( path ). `site` is fnv1a64 of the facet name and the site's NORMALIZED text, +// with the facet in its low two bits; two values are reserved: 0 is the FILE-PRESENCE record (the file was +// scanned at all — the origin oracle of a file anchor), 1 is "the file did not read" (its sites are unknown, so +// no current site in that file may be judged new against it). +struct DefectSiteKey +{ + std::uint64_t anchor = 0; + std::uint64_t site = 0; + friend bool operator==( const DefectSiteKey&, const DefectSiteKey& ) = default; + friend auto operator<=>( const DefectSiteKey&, const DefectSiteKey& ) = default; +}; +inline constexpr std::uint64_t kDefectFilePresent = 0; +inline constexpr std::uint64_t kDefectFileUnreadable = 1; + // A deterministic snapshot of the structural-quality state. Each per-symbol metric map is keyed by // hash(baselineCanonId) and stores the MAX over the overload set sharing that id (see computeSnapshot) — so a // low-metric overload written last can never manufacture a phantom regression on the next delta (the trap that @@ -267,6 +283,7 @@ struct Snapshot std::vector cloneGroups; // sorted hash(sorted member canonIds) std::vector dead; // sorted hash(canonId) of dead-candidate symbols std::vector publicApi; // Q1 contract drift — sorted hash(canonId) of PUBLIC/exported symbols (see isPublicApi) + std::vector defectSites; // the defect-shape kind — sorted (anchor, site) records, one per offending site (a multiset), plus each scanned file's presence record }; // Q1 api-surface — the PUBLIC/exported contract surface. DEFINITION (deterministic, documented): @@ -984,6 +1001,52 @@ inline std::uint64_t qualityKey( const IngestResult& ing, NodeId i, std::string_ return pathQualifiedKey( relForHash( ing.files[ s.fileId ], root ), s ); // the Symbol overload: the one keying rule per language } +// ─── the defect-shape kind (the twelfth) ───────────────────────────────────────────────────────────────── +// The anchor of top-level code: a key no definition can carry (no symbol has an empty name). +inline std::uint64_t defectFileAnchor( std::string_view relPath ) +{ + return pathQualifiedKey( relPath, "", "" ); +} + +inline defectshape::Facet defectFacetOf( std::uint64_t site ) noexcept +{ + return static_cast( site & 3u ); +} + +// A site's identity: the facet and its normalized text. Never one of the two reserved file records (0, 1). +inline std::uint64_t defectSiteValue( defectshape::Facet f, std::string_view text ) +{ + std::string id( defectshape::facetName( f ) ); + id.push_back( '\0' ); + id.append( text ); + std::uint64_t h = fnv1a64( id ) & ~std::uint64_t( 3 ); + if( h == 0 ) + { + h = 4; + } + const std::uint64_t v = h | static_cast( f ); + ENSURES( v != kDefectFilePresent && v != kDefectFileUnreadable && defectFacetOf( v ) == f, "a site value is never a reserved file record and carries its facet" ); + return v; +} + +// A defect-shape ROW's identity — the ack key: the anchor AND the facet, so a format-arity ack and a utf8-cut +// ack on one definition are two acks (checklist 16: a facet is part of the finding, not a label on it). +inline std::uint64_t defectRowKey( std::uint64_t anchor, defectshape::Facet f ) noexcept +{ + return anchor ^ ( 0x9E3779B97F4A7C15ull * ( static_cast( f ) + 1 ) ); +} + +// One current-side site with what the delta needs to print it: the anchoring definition (kNoNode for a file +// anchor), its file and 1-based line. +struct DefectSiteAt +{ + DefectSiteKey key; + NodeId owner = kNoNode; + std::uint32_t fileId = 0; + std::uint32_t line = 0; +}; + + // §D#4 short-horizon-churn — a per-identity hash of the symbols' RAW body bytes, for CHANGE detection that // metrics miss. `hot(){ return 2; }` → `hot(){ return 3; }` moves NO metric (same ccx/loc/nest/params), and // clones.h normalization erases the literal (`$N`), so neither can tell the body changed — only a raw-byte @@ -3393,7 +3456,11 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // argument holds as a VALUE (valuerefindex.h ValueRefIndex::isValueReferenced, checked after every other exemption, // counted as value-ref-excluded= like --dead-code): the dead SET moved, as in v9/v12/v15. kParserVer moved with // the extraction (the Value/Through rows) and its mirror moved with it. Bumped 16 -> 17. -constexpr std::uint32_t kQSnapCacheScheme = 17; +// v18 (lane/cr-qd-kinds-068, the defect-shape kind) — the blob gained the defect-shape (anchor, site) records +// after publicApi: a BLOB SHAPE change. A v17 blob has none, so served here it would read every format-arity +// site in the tree as newly added. Bumped by the v4/v5 rule (the producer identity already keeps this build's +// blobs apart; the bump keeps the history above complete). +constexpr std::uint32_t kQSnapCacheScheme = 18; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) @@ -3488,8 +3555,9 @@ inline bool qsnapCountFits( const char* p, const char* end, std::uint32_t count, } // Serialize a Snapshot to a self-validating blob: [magic][scheme][cacheVer][parserVer][producer][fnv(headSha)] then each -// of the 9 fields as a uint32 count followed by its flat records (btree maps in sorted key order, vectors -// as-is), then an fnv1a64 checksum over all preceding bytes. Byte-stable for a fixed Snapshot. +// field as a uint32 count followed by its flat records (btree maps in sorted key order, vectors as-is; the +// defect-shape records last, as anchor+site pairs), then an fnv1a64 checksum over all preceding bytes. +// Byte-stable for a fixed Snapshot. // P0.2 (r27): cacheVer/parserVer are the EXTRACTION IDENTITY every field below is a function of — see the note // at kIngestCacheVersionMirror. They are in the filename key too; carrying them here as well means a blob // reached by any other route (hand-copied, collided) is REJECTED rather than believed. @@ -3523,6 +3591,12 @@ inline std::string serializeSnapshot( const Snapshot& s, const std::string& head putVec( s.cloneGroups ); putVec( s.dead ); putVec( s.publicApi ); + qsnapPut( buf, std::uint32_t( s.defectSites.size() ) ); // v18: the defect-shape (anchor, site) records, in sorted order + for( const DefectSiteKey& d : s.defectSites ) + { + qsnapPut( buf, d.anchor ); + qsnapPut( buf, d.site ); + } const std::uint64_t sum = fnv1a64( std::string_view( buf.data(), buf.size() ) ); qsnapPut( buf, sum ); @@ -3646,7 +3720,28 @@ inline bool deserializeSnapshot( const std::string& blob, const std::string& hea return true; }; - if( !getValMap( s.ccxBySym ) || !getValMap( s.locBySym ) || !getValMap( s.nestBySym ) || !getValMap( s.paramsBySym ) || !getValMap( s.defsBySym ) || !getValMap( s.maskBySym ) || !getValMap( s.maskReportBySym ) || !getValMap( s.placeholderBySym ) || !getHashMap( s.bodyHashBySym ) || !getVec( s.cloneGroups ) || !getVec( s.dead ) || !getVec( s.publicApi ) ) + const auto getDefectSites = [ & ]( std::vector& v ) -> bool + { + std::uint32_t n = 0; + if( !qsnapGet( p, end, n ) || !qsnapCountFits( p, end, n, 2 * sizeof( std::uint64_t ) ) ) + { + return false; // a corrupt blob: the caller's "cache corrupt — recomputing" path + } + v.reserve( n ); + for( std::uint32_t i = 0; i < n; ++i ) + { + DefectSiteKey d; + if( !qsnapGet( p, end, d.anchor ) || !qsnapGet( p, end, d.site ) ) + { + return false; + } + v.push_back( d ); + } + return true; + }; + + if( !getValMap( s.ccxBySym ) || !getValMap( s.locBySym ) || !getValMap( s.nestBySym ) || !getValMap( s.paramsBySym ) || !getValMap( s.defsBySym ) || !getValMap( s.maskBySym ) || !getValMap( s.maskReportBySym ) || !getValMap( s.placeholderBySym ) || !getHashMap( s.bodyHashBySym ) || !getVec( s.cloneGroups ) || !getVec( s.dead ) || !getVec( s.publicApi ) + || !getDefectSites( s.defectSites ) ) { return false; } @@ -3654,6 +3749,7 @@ inline bool deserializeSnapshot( const std::string& blob, const std::string& hea std::sort( s.cloneGroups.begin(), s.cloneGroups.end() ); // computeDelta binary_searches these — enforce order std::sort( s.dead.begin(), s.dead.end() ); std::sort( s.publicApi.begin(), s.publicApi.end() ); + std::sort( s.defectSites.begin(), s.defectSites.end() ); // computeDelta merges these in key order out = std::move( s ); return true; } @@ -4553,6 +4649,86 @@ inline std::vector baselineCloneGroupHashes( const IngestResult& return out; } +// Every defect-shape site in the tree, anchored on the innermost enclosing definition (byte-span containment, +// the same rule error-masking counts by) or on the file. Languages: C++ (format-arity, utf8-cut, +// dedup-first), Python (format-arity), Bash under a test-script path (vacuous-assert). Each scanned file also +// leaves its presence record; a file that does not read leaves the unreadable record instead and contributes +// no site (a partial read is evidence of nothing, forEachSymbolBody's rule) — the delta then judges no current +// site in that file against it. Sorted by key; deterministic (files in id order, scanners pure). +inline std::vector defectSitesOf( const IngestResult& ing, std::string_view root, const std::vector* fileInBaseline ) +{ + std::vector out; + const SymbolsByFile byFile = symbolsByFileInIdOrder( ing, []( const Symbol& s ) { return s.endByte > s.sigStartByte && s.kind != SymKind::ModuleScope; } ); + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + if( !fileIsInBaseline( fileInBaseline, f ) ) + { + continue; + } + const std::string_view rel = relForHash( ing.files[f], root ); + const Lang lang = langOfPath( rel ); + if( lang != Lang::Cpp && lang != Lang::Python && !( lang == Lang::Bash && isTestScriptPath( rel ) ) ) + { + continue; + } + const std::uint64_t fileAnchor = defectFileAnchor( rel ); + std::optional bytes = docparse::detail::readWholeFile( diskPath( ing, f ) ); + if( !bytes ) + { + out.push_back( { { fileAnchor, kDefectFileUnreadable }, kNoNode, f, 0 } ); + continue; + } + out.push_back( { { fileAnchor, kDefectFilePresent }, kNoNode, f, 0 } ); + const std::string_view src = *bytes; + std::vector fnSpans; + if( f < byFile.size() ) + { + for( NodeId i : byFile[f] ) + { + const Symbol& s = ing.symbols[i]; + if( ( s.kind == SymKind::Function || s.kind == SymKind::Method ) && s.endByte <= src.size() ) + { + fnSpans.push_back( { s.sigStartByte, s.endByte } ); + } + } + } + std::vector sites; + if( lang == Lang::Cpp && defectshape::cppMayHoldShapes( src ) ) + { + sites = defectshape::scanCpp( src, fnSpans ); + } + else if( lang == Lang::Python && src.find( ".format" ) != std::string_view::npos ) + { + sites = defectshape::scanPython( src ); + } + else if( lang == Lang::Bash && src.find( "grep" ) != std::string_view::npos ) + { + sites = defectshape::scanBashTestScript( src, fnSpans ); + } + for( const defectshape::Site& st : sites ) + { + ASSUME( st.startByte < src.size(), "a scanner reports a site at a byte of the file it scanned" ); + const NodeId owner = enclosingDefOf( ing, byFile, f, st.startByte ); + const std::uint64_t anchor = owner == kNoNode ? fileAnchor : qualityKey( ing, owner, root ); + const std::uint32_t line = static_cast( 1 + std::count( src.begin(), src.begin() + st.startByte, '\n' ) ); + out.push_back( { { anchor, defectSiteValue( st.facet, st.text ) }, owner, f, line } ); + } + } + std::stable_sort( out.begin(), out.end(), []( const DefectSiteAt& a, const DefectSiteAt& b ) { return a.key < b.key; } ); + return out; +} + +inline std::vector defectSiteKeys( const std::vector& sites ) +{ + std::vector out; + out.reserve( sites.size() ); + for( const DefectSiteAt& s : sites ) + { + out.push_back( s.key ); + } + return out; +} + inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, const std::vector* fileInBaseline ) { @@ -4609,6 +4785,10 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s snap.placeholderBySym = std::move( counts.placeholder ); } + // the defect-shape baseline: every offending site (a sorted multiset of anchor + site identity) and each + // scanned file's presence record. Filtered to the baseline files inside defectSitesOf (IDENTITY BASIS). + snap.defectSites = defectSiteKeys( defectSitesOf( ing, root, fileInBaseline ) ); + // §D#4 short-horizon-churn baseline: per-canonId RAW-body hash so the delta detects a rewrite that moved no // metric (a literal-only edit). Compared, never bar-checked — presence-or-difference IS the rewrite signal. snap.bodyHashBySym = bodyHashesBySym( ing, root ); @@ -5018,6 +5198,10 @@ inline bool writeBaseline( const Snapshot& s, const std::string& path, std::stri { f << "api " << std::hex << h << std::dec << '\n'; } + for( const DefectSiteKey& d : s.defectSites ) + { + f << "defect " << std::hex << d.anchor << ' ' << d.site << std::dec << '\n'; // the defect-shape kind: anchor + site identity (both hex) + } // Was an unconditional `return true`: a stream that failed to flush still reported a written baseline. // The descriptor answers for the bytes, so a full disk is now a failure the caller can report. return rw::pathguard::writeAllAndClose( fd, f.str() ); @@ -5160,7 +5344,7 @@ inline bool readBaseline( const std::string& path, Snapshot& out, BaselineReadSt if( is.fail() ) { DISCLOSE( stats, BaselineReadStats::DisclosureWhy::MalformedLine ); return; } m[h] = v; }; if( kind == "ccx" || kind == "loc" || kind == "nest" || kind == "params" || kind == "mask" || kind == "maskr" || kind == "stub" || kind == "body" || kind == "clone" || kind == "dead" || kind == "api" || kind == "head" || kind == "defs" - || kind == "producer" ) + || kind == "producer" || kind == "defect" ) { ++recognizedLineCount; // structure seen — this file IS a baseline } @@ -5213,6 +5397,19 @@ inline bool readBaseline( const std::string& path, Snapshot& out, BaselineReadSt { readSet( out.publicApi ); } + else if( kind == "defect" ) + { + DefectSiteKey d; + is >> std::hex >> d.anchor >> d.site >> std::dec; + if( is.fail() ) + { + DISCLOSE( stats, BaselineReadStats::DisclosureWhy::MalformedLine ); + } + else + { + out.defectSites.push_back( d ); + } + } else if( kind == "producer" ) { readProducerRecord( is, stats ); @@ -5240,6 +5437,7 @@ inline bool readBaseline( const std::string& path, Snapshot& out, BaselineReadSt std::sort( out.cloneGroups.begin(), out.cloneGroups.end() ); std::sort( out.dead.begin(), out.dead.end() ); std::sort( out.publicApi.begin(), out.publicApi.end() ); + std::sort( out.defectSites.begin(), out.defectSites.end() ); return true; } @@ -5761,7 +5959,7 @@ inline std::uint32_t churnEditWindowCommitCount( DiffHunkMemo& memo, const std:: struct Regression { std::string kind; // "complexity" | "duplication" | "dead-code" | "verbosity" | "nesting" | "params" | "api-surface" - // | "error-masking" | "short-horizon-churn" | "new-clone-of-reused-helper" (§D#4) | "placeholder" + // | "error-masking" | "short-horizon-churn" | "new-clone-of-reused-helper" (§D#4) | "placeholder" | "defect-shape" std::string sym; // canonical id (or, for duplication, the space-joined member ids) std::uint32_t was = 0; std::uint32_t now = 0; @@ -5769,7 +5967,8 @@ struct Regression // short-horizon-churn rows carry pathQualifiedKey instead (W1-S2): a bare canonId folds scope-less same-named symbols across files, so one ack would suppress — and one finding would name — the WRONG file's symbol bool isMinor = false;// materiality tier: true = below the kind's minor-delta bar → reported sev="minor", does not gate exit 2 std::string facet; // B10.2 — optional classification facet (attribute NAME chosen by the kind in main.cpp): - // short-horizon-churn: "self" | "ambient"; api-surface: "new-symbol" | "contract-change". Empty = no facet. + // short-horizon-churn: "self" | "ambient"; api-surface: "new-symbol" | "contract-change"; + // defect-shape: "format-arity" | "utf8-cut" | "dedup-first" | "vacuous-assert". Empty = no facet. bool isNewSymbol = false;// r26 ORIGIN axis: true = the finding exists ONLY because the code is new (emitted origin="new-symbol", // counted in new-symbol=, never gates); false = preexisting-worse. See the ORIGIN block in computeDelta. // P2.5 (r27) — the LOCATOR. `sym` is a canonical id whose display tail is often a bare, one-letter local @@ -5795,6 +5994,16 @@ struct Regression bool isZeroMagnitude() const noexcept { return was == 0 && now == 0; } }; +// THE EXIT PREDICATE, in one place: every surface that says what gates — the XML gating= count and per-row +// gating attribute, the --json / MCP "gating", the exit code, --ack-only=gating, the scoped-out would-gate +// count, the allow-dirty absorbed count and the stderr naming line — asks this. A row gates when it is major +// (not sev="minor") AND either preexisting-worse or a defect-shape format-arity row: new-symbol rows never +// gate, except defect-shape format-arity, because a placeholder/argument mismatch is a defect, not debt. +inline bool rowGates( const Regression& r ) noexcept +{ + return !r.isMinor && ( !r.isNewSymbol || ( r.kind == "defect-shape" && r.facet == defectshape::facetName( defectshape::Facet::FormatArity ) ) ); +} + // B10.2 — kind → the ATTRIBUTE NAME its facet value is emitted under. `Regression::facet` carries only the // VALUE, so exactly one place decides what to call it. That place used to be THREE places (the XML // quality-delta emitter, its --json twin, and the MCP quality_delta emitter each held the same conditional @@ -5813,6 +6022,7 @@ inline constexpr FacetAttr kFacetAttrs[] = { { "short-horizon-churn", "churn" }, // self / ambient { "api-surface", "surface" }, // new-symbol / contract-change { "duplication", "idiom" }, // the recognized clone-body shape (cloneidiom.h) + { "defect-shape", "defect" }, // format-arity / utf8-cut / dedup-first / vacuous-assert { "new-clone-of-reused-helper", "idiom" }, // T13/fix2 gave this kind duplication's idiom demotion (Regression::facet // is populated the same way, from the same CloneIdiomVerdict); this row was // missing, so a demoted row's idiom name never reached the reader even @@ -7224,6 +7434,30 @@ inline std::size_t remapSnapshotIdentity( Snapshot& base, const IdentityAliases& healSet( base.dead ); healSet( base.publicApi ); // base.cloneGroups is deliberately untouched — see the WHAT IS NOT REMAPPED note above. + + // the defect-shape records: a renamed definition's sites are re-filed under its current anchor, same + // add-never-overwrite rule (an anchor the baseline already holds keeps its own records). Whether a site is + // NEW never depends on this — that is decided by its text, repo-wide — only the row's was= count does. + std::vector addSites; + for( const auto& [ from, to ] : al.toCurrent ) + { + const auto lo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ from, 0 } ); + const auto toLo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ to, 0 } ); + if( toLo != base.defectSites.end() && toLo->anchor == to ) + { + continue; + } + for( auto it = lo; it != base.defectSites.end() && it->anchor == from; ++it ) + { + addSites.push_back( { to, it->site } ); + ++moved; + } + } + if( !addSites.empty() ) + { + base.defectSites.insert( base.defectSites.end(), addSites.begin(), addSites.end() ); + std::sort( base.defectSites.begin(), base.defectSites.end() ); + } return moved; } @@ -7637,6 +7871,22 @@ inline std::optional staleForChurn( std::uint64_t key, const Snapsh return ( snap.bodyHashBySym.find( key ) == snap.bodyHashBySym.end() ) ? std::optional( StaleAckWhy::TargetGone ) : std::nullopt; } +// defect-shape: the ack key is defectRowKey( anchor, facet ), so the facet is part of the finding's identity +// (one ack never suppresses a different shape on the same definition). A key no current site produces is +// finding-gone; the anchor cannot be recovered from the mixed key, so target-gone is never claimed (the +// clone kinds' rule). +inline std::optional staleForDefectShape( std::uint64_t key, const Snapshot& snap ) +{ + for( const DefectSiteKey& d : snap.defectSites ) + { + if( d.site != kDefectFilePresent && d.site != kDefectFileUnreadable && defectRowKey( d.anchor, defectFacetOf( d.site ) ) == key ) + { + return std::nullopt; + } + } + return StaleAckWhy::FindingGone; +} + inline std::vector computeStaleAcks( const gtl::btree_map& acks, const Snapshot& snap ) { std::vector out; @@ -7679,6 +7929,10 @@ inline std::vector computeStaleAcks( const gtl::btree_map computeDelta( const IngestResult& ing, const Grap } } + // ── defect-shape (the twelfth kind) ─────────────────────────────────────────────────────────────────── + // A site is NEW when its identity (facet + normalized text) occurs more often in the current tree than in + // the baseline, REPO-WIDE: an untouched, moved or renamed defect is not new, an edited one is, and a swap + // (one site fixed and another added in the same definition) still is. The excess of each identity is + // credited to the anchors that hold more copies of it now than at the baseline, in anchor-key order + // (deterministic). An anchor credited with a new site of a facet gets ONE row for that facet; was= and now= + // count the anchor's sites of that facet on each side. format-arity gates on ANY origin — a placeholder / + // argument mismatch is a defect, not debt; utf8-cut, dedup-first and vacuous-assert are report-only: + // sev="minor" by facet, not by size. A file the baseline could not read judges none of its sites. + { + const std::vector nowSites = defectSitesOf( ing, root, nullptr ); + const auto isRealSite = []( std::uint64_t v ) { return v != kDefectFilePresent && v != kDefectFileUnreadable; }; + std::vector baseVals, nowVals, baseUnreadable; + for( const DefectSiteKey& d : base.defectSites ) + { + if( isRealSite( d.site ) ) + { + baseVals.push_back( d.site ); + } + else if( d.site == kDefectFileUnreadable ) + { + baseUnreadable.push_back( d.anchor ); + } + } + for( const DefectSiteAt& d : nowSites ) + { + if( isRealSite( d.key.site ) ) + { + nowVals.push_back( d.key.site ); + } + } + std::sort( baseVals.begin(), baseVals.end() ); + std::sort( nowVals.begin(), nowVals.end() ); + std::sort( baseUnreadable.begin(), baseUnreadable.end() ); + const auto countOf = []( const std::vector& v, std::uint64_t x ) + { + const auto r = std::equal_range( v.begin(), v.end(), x ); + return static_cast( r.second - r.first ); + }; + // the repo-wide excess still to credit, per identity + std::vector> excess; + for( std::size_t k = 0; k < nowVals.size(); ) + { + const std::uint64_t v = nowVals[k]; + const std::uint32_t nN = countOf( nowVals, v ); + const std::uint32_t nB = countOf( baseVals, v ); + if( nN > nB ) + { + excess.push_back( { v, nN - nB } ); + } + k += nN; + } + const auto baseAnchorCount = [ & ]( std::uint64_t anchor, std::uint64_t site ) + { + const auto r = std::equal_range( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ anchor, site } ); + return static_cast( r.second - r.first ); + }; + const auto facetCount = []( const auto& sites, std::uint64_t anchor, defectshape::Facet f, auto keyOf ) + { + std::uint32_t n = 0; + for( const auto& s : sites ) + { + const DefectSiteKey& k = keyOf( s ); + n += ( k.anchor == anchor && k.site != kDefectFilePresent && k.site != kDefectFileUnreadable && defectFacetOf( k.site ) == f ) ? 1u : 0u; + } + return n; + }; + struct Credited + { + std::uint64_t anchor = 0; + defectshape::Facet facet = defectshape::Facet::FormatArity; + const DefectSiteAt* first = nullptr; + }; + std::vector credited; + for( std::size_t k = 0; k < nowSites.size(); ) + { + const DefectSiteKey key = nowSites[k].key; + std::size_t e = k; + while( e < nowSites.size() && nowSites[e].key == key ) + { + ++e; + } + const std::uint32_t headA = static_cast( e - k ); + const DefectSiteAt& at = nowSites[k]; + k = e; + if( !isRealSite( key.site ) || at.fileId >= ing.files.size() + || std::binary_search( baseUnreadable.begin(), baseUnreadable.end(), defectFileAnchor( relForHash( ing.files[ at.fileId ], root ) ) ) ) + { + continue; + } + const std::uint32_t baseA = baseAnchorCount( key.anchor, key.site ); + const auto ex = std::lower_bound( excess.begin(), excess.end(), std::pair{ key.site, 0 } ); + if( headA <= baseA || ex == excess.end() || ex->first != key.site || ex->second == 0 ) + { + continue; + } + ex->second -= std::min( ex->second, headA - baseA ); + const defectshape::Facet f = defectFacetOf( key.site ); + const bool seen = std::any_of( credited.begin(), credited.end(), [ & ]( const Credited& c ) { return c.anchor == key.anchor && c.facet == f; } ); + if( !seen ) + { + credited.push_back( { key.anchor, f, &at } ); + } + } + for( const Credited& c : credited ) + { + const std::uint32_t was = facetCount( base.defectSites, c.anchor, c.facet, []( const DefectSiteKey& d ) -> const DefectSiteKey& { return d; } ); + const std::uint32_t now = facetCount( nowSites, c.anchor, c.facet, []( const DefectSiteAt& d ) -> const DefectSiteKey& { return d.key; } ); + const NodeId owner = c.first->owner; + const std::string rel( relForHash( ing.files[ c.first->fileId ], root ) ); + const bool isNew = ( owner != kNoNode ) + ? !existedAtBaseline( c.anchor ) + : !std::binary_search( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ c.anchor, kDefectFilePresent } ); + const std::string sym = ( owner != kNoNode && owner < g.canonId.size() && !g.canonId[owner].empty() ) ? g.canonId[owner] : rel; + regs.push_back( { "defect-shape", sym, was, now, defectRowKey( c.anchor, c.facet ), !defectshape::facetGatesOnAnyOrigin( c.facet ), + std::string( defectshape::facetName( c.facet ) ), isNew } ); + if( owner != kNoNode ) + { + stampLoc( owner ); + } + else + { + regs.back().path = rel; + regs.back().line = c.first->line; + } + } + } + // ── §D#4-2 short-horizon churn (GitClear +15% "new code rewritten within two weeks") ─────────────────── // A symbol flags iff THREE independent gates all hold (signal-to-noise round, 2026-07-13): // 1. its FILE had ≥ kShortHorizonMinCommits commits in the last kShortHorizonDays (git COMMIT TIMESTAMPS diff --git a/src/verbs_quality.h b/src/verbs_quality.h index fd47911bd..446ca5553 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -443,7 +443,7 @@ std::size_t partitionByScope( const rw::quality::Scope& scope, std::vector refuseForeignAckSelection( const rw::Config& cfg, const rw::q // this prose either: several gates grep the header counters (regressions=, gating=, stale=) and a // quoted example here would be matched ahead of the real one. -// Always. The verb, the eleven kinds, the three axes, the exit predicate, and the two counters that are +// Always. The verb, the twelve kinds, the three axes, the exit predicate, and the two counters that are // printed even at zero. Every row in the document — finding rows and stale-ack rows alike — carries // kind=, so it is defined here rather than in either conditional row dictionary. // P8 (L7): the bar= literals in emitRow mirror quality.h's constants — pinned here so a moved bar cannot drift the row @@ -530,17 +530,18 @@ static_assert( rw::quality::kCcxBar == 15 && rw::quality::kLocBar == 60 && rw::q inline constexpr const char* kQdLegendCore = " - - + + ``` diff --git a/docs/LIMITS.md b/docs/LIMITS.md index fbeb13a73..9263afb70 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -17,10 +17,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 236 | 94 | 135 | **101** | +| 237 | 94 | 135 | **102** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 236 + 7 is the 243 constants this generator parses out of `src/`. +are not counted as caps, and 237 + 7 is the 244 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -38,8 +38,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. **130 of 236 caps are classified -(45 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 106 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **130 of 237 caps are classified +(45 INDEXING, 41 OUTPUT, 44 BOUNDARY); the remaining 107 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 @@ -89,7 +89,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 94 files that declare a cap — the 236 caps counted above, and no parameter. +One table for each of the 94 files that declare a cap — the 237 caps counted above, and no parameter. ### `src/abicheck.h` @@ -753,6 +753,7 @@ Discloses: **none** | `kMaxCacheBlobAgeDays` | `30.0` | BOUNDARY | — | | `kMaxCacheBlobCount` | `4096` | BOUNDARY | bound every future hygiene scan | | `kMaxEditLockAgeDays` | `1.0` | BOUNDARY | — | +| `kMaxFilesPerCommit` | `30` | — | — | | `kRenameMaxChain` | `8` | INDEXING | a→b→c… chain depth followed from one current path (disclosed) | | `kRenameMaxPairs` | `4000` | INDEXING | hard cap on recorded pairs (disclosed when hit) | diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..74806c976 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -3921,33 +3921,33 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4 # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:5565` +## `./build/ripwire . --at=src/graph.h:5573` *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* ````` - - + + ````` -## `./build/ripwire . --callers=@src/graph.h:5565` +## `./build/ripwire . --callers=@src/graph.h:5573` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - - + + - - - + + + ````` From 2f8ee9ef81fde13e2d37e14f06e78d87cdbbe216 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:29:43 -0400 Subject: [PATCH 136/229] whereis: the default listing is the shorter of the defs page and the whole list The default --whereis listing (definitions listed, references counted in ) pays a fixed overhead: listing=, the element and, under the compact legend, their readings. On a symbol with one to three references that overhead is larger than the rows it elides, so the lean page was longer than --whereis-listing=all while listing fewer rows (24 of 58 sampled symbols). The default now renders both pages (unpaged, over the same row cap) and serves the defs page only when it is strictly shorter both as written and as the compact dialect delivers it; a tie serves the whole list. The choice does not depend on --limit/--offset or on the legend posture, so every page of one answer and both legend dialects list the same rows. An explicit --whereis-listing=defs (MCP listing:"defs") is served as asked. CLI and MCP share the code; --json is not served for --whereis. The legends, --help, the MCP listing description and the change-check skill say so, and also that kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is among the counted references. Gates: crossrefcheck LEAN L11-L14 (1-ref symbol, 73-ref symbol, the exact tie and one byte either side, MCP twin); compactlegendcheck whereis pin 900->1040 (+136 B, the two reading clauses) with a new --whereis-listing=all twin pin at 700; mcpmanifestcheck 46,828->46,846 B, ceiling unmoved; printf parity help and help_all re-pinned (the whereis help lines only); COMMANDS.md regenerated. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 ++++--- docs/COMMANDS.md | 6 +-- skills/ripwire-change-check/SKILL.md | 3 +- src/cli.h | 18 ++++--- src/compactlegend.h | 2 +- src/crossref.h | 81 +++++++++++++++++++++++----- src/mcp.h | 2 +- src/mcprefusal.h | 2 +- src/mcpverbs.h | 4 +- test/compactlegendcheck.sh | 20 ++++++- test/crossrefcheck.sh | 68 +++++++++++++++++++++++ test/mcpmanifestcheck.sh | 4 ++ test/printf_parity.manifest | 4 +- 13 files changed, 194 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db44026d8..8b5aefd7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,11 +22,17 @@ build: 631,732 → 611,785 B for the lossless changes alone (−3.2%), 567,512 B Every gold item the base answers supplied is still supplied (502 of 502), and decoding the lossless spellings gives the base answer back element for element on all 90. -- **`--whereis=SYM` lists the definitions and counts the references.** Every `kind="def"` row is listed; the - `kind="ref"` rows are counted in one `` element whose - `next=` lists exactly those rows. `--whereis-listing=defs|refs|all` (MCP `listing`) picks the rows; `all` is the - whole hit list. An answer with no reference row, or no definition row (the mentions are then the answer), lists - every hit. Median where-defined answer 10,455 → 1,424 B. `shown=`/`capped=`/`--limit`/`--offset` and `` +- **`--whereis=SYM` lists the definitions and counts the references, when that answer is shorter.** Every + `kind="def"` row is listed; the `kind="ref"` rows are counted in one + `` element whose `next=` lists exactly those rows. The + default serves this page only when it is strictly shorter, in bytes, than the whole hit list (compared as written and + in the compact legend); otherwise it lists every hit. On a symbol with few references (one to three on a 60-symbol + sample) the count and its legend reading cost more than the rows they replace, and the default never serves a page + that lists fewer rows in more bytes (an explicit `--whereis-listing=defs` is served as asked). + `--whereis-listing=defs|refs|all` (MCP `listing`) picks the rows; `all` is the whole hit list. An answer with no + reference row, or no definition row (the mentions are then the answer), lists every hit. `kind="def"` is the + parser's label: a definition it does not model (a Ruby `define_method`, a `setattr`, a name bound by assignment) + is among the counted references, and the legend says so. Median where-defined answer 10,455 → 1,424 B. `shown=`/`capped=`/`--limit`/`--offset` and `` window the listed rows; `hits=` counts every row; `complete=` reads the listing. - **Lossless:** a `--whereis` row on HEAD's commit omits `tip=`/`date=` (they read `at=` and the new root `head_date=`); a `--for` lens row omits `cx=`/`ccx=`/`in=` when 0 (the legend says an absent one is 0; the rows @@ -37,7 +43,7 @@ the base answer back element for element on all 90. posture (rows first, each definition sent once per session, the core carried by the first ref answer). `--mcp-legend=inline` keeps the previous posture; `legend:"compact"`/`"full"` still keep one answer inline; the CLI and the HTTP transport are unchanged. A ten-call session on one repo: 45,463 → 32,622 B (inline) → 29,679 B. -- **Manifest.** The `tools/list` manifest grows 46,732 → 46,828 B: whereis declares `listing`. +- **Manifest.** The `tools/list` manifest grows 46,732 → 46,846 B: whereis declares `listing`. Gates: `crossrefcheck` (LEAN L1–L10 and listing=all twins), `completecheck`, `legendrefcheck` (J), `mcptwinclaimscheck` (A-default), `compactlegendcheck` (re-pinned with the measured bytes), `mcpmanifestcheck`. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index d222366af..a359ba2b6 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -3546,7 +3546,7 @@ $ ./build/ripwire . --stray-content=main --abi **Answers:** find which branch's tree defines or mentions SYM, HEAD first which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch. -Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed; the kind="ref" rows are counted in one element (see --whereis-listing). +Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page is strictly shorter than listing every hit; otherwise every hit is listed (see --whereis-listing). **Try it** @@ -3574,9 +3574,9 @@ $ ./build/ripwire . --whereis=rankGraphTeleport ### `--whereis-listing=WHICH` -**Answers:** with --whereis: which rows to list, defs (the default), refs or all defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). +**Answers:** with --whereis: which rows to list, defs, refs or all (default: the shorter page). -With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. +The default lists defs when that page is strictly shorter in bytes than all, else all (a tie lists all). defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. **Shaped by:** `--whereis` diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index 789867b71..fd79e3ffd 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -148,7 +148,8 @@ emits a flat `` top-K — identity + score + signatu distinct blob is read once (git is content-addressed), so 30 branches cost about one tree. `kind="def"` on a branch row is a lexical heuristic — branch blobs are raw text, never ingested; for HEAD's parsed answer use `--expand`/`--callers`. By default only the `kind="def"` rows are listed and the mentions are - counted in one `` row (its `next=` lists them); `--whereis-listing=all` lists every row. + counted in one `` row (its `next=` lists them) when that page is the shorter one; + otherwise, and with `--whereis-listing=all`, every row is listed. A tree scan only finds what some ref *still carries*, so `hits="0"` cannot by itself tell a name this repo never had from one it deleted. Add `--with-history` and a `` row says which: `v="never"`, or `v="removed" commit=… date=… p=…` naming the commit that took it out. One `git log` pass, memoized per diff --git a/src/cli.h b/src/cli.h index 8d5e18f9f..611d47dad 100644 --- a/src/cli.h +++ b/src/cli.h @@ -385,7 +385,7 @@ struct Config std::string_view laneBrief; // --brief=FILE: one non-blank line per lane, each ranked on its own bool whereisFlag = false; // --whereis was given at all (a bare/empty value still routes to the // handler and refuses loudly rather than falling through to the map) - std::string_view whereisListing; // --whereis-listing=defs|refs|all (default defs; crossref.h WhereisListing) + std::string_view whereisListing; // --whereis-listing=defs|refs|all (default: the shorter of defs and all; crossref.h WhereisListing) std::string_view whereis; // --whereis=SYM: every ref whose TREE contains SYM, // HEAD first, with on-head= saying whether the live line has it at all. // Scans each ref's FULL tree; each distinct blob is read once (content- @@ -2094,12 +2094,16 @@ inline constexpr char kHelpTail[] = " is invisible. Add --with-history: a row then says v=\"never\" or v=\"removed\"\n" " with the commit, date and file that removed it. Remote-tracking refs are excluded\n" " (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR.\n" - " LISTING: by default only the kind=\"def\" rows are listed; the kind=\"ref\" rows are\n" - " counted in one element (see --whereis-listing).\n" - " --whereis-listing=WHICH with --whereis: which rows to list, defs (the default), refs or all\n" - " defs lists every kind=\"def\" row and COUNTS the kind=\"ref\" rows in one\n" - " element whose next= lists exactly them (refs). With no ref\n" - " row, or no def row (the mentions are then the answer), every hit is listed and the\n" + " LISTING: by default only the kind=\"def\" rows are listed and the kind=\"ref\" rows\n" + " counted in one element, when that page is strictly shorter\n" + " than listing every hit; otherwise every hit is listed (see --whereis-listing).\n" + " --whereis-listing=WHICH with --whereis: which rows to list, defs, refs or all (default: the shorter page).\n" + " The default lists defs when that page is strictly shorter in bytes than all, else\n" + " all (a tie lists all). defs lists every kind=\"def\" row and COUNTS the kind=\"ref\"\n" + " rows in one element whose next= lists exactly them\n" + " (refs). kind=\"def\" is the parser's label: a definition it does not\n" + " model (define_method, setattr, a name bound by assignment) is a counted ref. With no\n" + " ref row, or no def row (the mentions are then the answer), every hit is listed and the\n" " root carries no listing=. all lists every row, the whole hit list. shown=/capped=\n" " and --limit/--offset window the LISTED rows; hits= counts every row. Refused\n" " without --whereis, and on an unknown value.\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index 09e79f54e..371cfa4cd 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -930,7 +930,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "worktree", "worktree=read|partial|unlisted: the checkout differs from HEAD (at= +dirty); each changed path is read from disk as ref=\"worktree\" rows replacing HEAD's, and on-head=/hits=/head_labels= count them; on-head= then reads the checkout, not HEAD's commit; partial = some changed path unreadable or a directory (nested repo, submodule), its HEAD rows may be stale; unlisted = git could not list the changes; either withholds complete=", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "hits", "more hits=N: rows after this page; page on with offset=next_offset", true, "more", MapHeaderRead::No, {}, "whereis" }, // lean-answers lane (crossref.h WhereisListing / the tip-date hoist): each rides only the answer that carries it. - { "listing", "listing=defs|refs: only those kind= rows listed; under defs counts the kind=ref rows and next= lists them; the window counts listed rows", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "listing", "listing=defs|refs: only those kind= rows listed; under defs counts the kind=ref rows and next= lists them; the window counts listed rows; default: defs only if strictly shorter than all; a def the parser does not model (define_method, setattr, assignment) is a counted ref", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "head_date", "head_date=: a hit without tip= date= has tip= at=, date= this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, // the GREY ZONE of the same sweep: attributes the compact prose named in passing ("in/out, cx/ccx", " groups") but never // DEFINED as name= — legendcoveragecheck's default rows hold the definitional predicate, so each gets its reading here. diff --git a/src/crossref.h b/src/crossref.h index de1df4ce1..dac24ad35 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -98,6 +98,7 @@ #include "serialize.h" // escapeXml #include "pageview.h" // §P8: pageWindow / pageDisclosure — the shared --limit/--offset contract #include "nextverb.h" // nextAttrXml / nextFlag — the follow-up of the default listing +#include "compactlegend.h" // compactDeliveredBytes — the default listing is chosen on the bytes the compact dialect delivers #include "workspace.h" // wsdetail::segmentsOf #include "filter.h" // §P11.5: rw::pathTierOf — the shared source/test/doc ORDERING tier #include "infra/Diagnostics.h" // ASSUME / DISCLOSE @@ -151,7 +152,12 @@ constexpr std::size_t kWhereisHits = 60; // list holds no ref row (nothing to elide) or no def row (the mentions ARE the answer then). // Refs the kind="ref" rows only, in the hit list's order: the page. // All every row. -enum class WhereisListing : std::uint8_t { Defs, Refs, All }; +// ShorterOfDefsAll the DEFAULT (no flag, no MCP `listing`): the Defs page when it is STRICTLY shorter in bytes than +// the All page, else the All page (review B1: on a symbol with one or two refs the Defs page pays more for its +// element, listing= and their legend readings than the ref rows it elides, and a lean answer that is +// longer while listing less is strictly worse). Measured, never guessed: whereisServedListing renders both. +// An explicit defs is served as asked. +enum class WhereisListing : std::uint8_t { Defs, Refs, All, ShorterOfDefsAll }; // ── blob facts (the per-sha memo payload) ──────────────────────────────────────────────────────────────── @@ -2846,6 +2852,7 @@ struct ListedHits inline ListedHits listedHits( const WhereResult& res, WhereisListing listing ) { + EXPECTS( listing != WhereisListing::ShorterOfDefsAll, "the default is resolved to Defs or All (whereisServedListing) before the rows are picked" ); const std::size_t defs = std::size_t( std::count_if( res.hits.begin(), res.hits.end(), []( const WhereHit& h ) { return h.isDef; } ) ); const std::size_t refs = res.hits.size() - defs; ListedHits out; @@ -2892,11 +2899,12 @@ inline bool whereisRowOnHeadCommit( const WhereHit& h, std::string_view headSha, return !head.empty() && !headDate.empty() && std::string_view( h.tip ).substr( 0, 9 ) == head && h.date == headDate; } -// The listing a validated --whereis-listing= / MCP `listing` value names; "" (absent) is the default, defs. Both surfaces -// refuse any other value before they get here. +// The listing a validated --whereis-listing= / MCP `listing` value names; "" (absent) is the default, the shorter of +// defs and all (WhereisListing::ShorterOfDefsAll). Both surfaces refuse any other value before they get here. inline WhereisListing whereisListingOf( std::string_view v ) noexcept { - return v == "refs" ? WhereisListing::Refs : v == "all" ? WhereisListing::All : WhereisListing::Defs; + return v == "refs" ? WhereisListing::Refs : v == "all" ? WhereisListing::All + : v == "defs" ? WhereisListing::Defs : WhereisListing::ShorterOfDefsAll; } // The : the same symbol, listing=refs — exactly the kind="ref" rows the default listing counted. @@ -3020,16 +3028,12 @@ inline void writeWhereisRows( std::FILE* out, const WhereResult& res, const List // // Paging lives in its own entry point rather than as two defaulted parameters on writeWhereis() so the // un-paginated contract — the one the MCP `whereis` verb calls — keeps its exact three-argument shape. -inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset, - WhereisListing listing ) +inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset, + const ListedHits& listed ) { std::vector esc; const XmlEscaper ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; - // The LISTED rows (WhereisListing): indices into res.hits, in the hit list's own order, so a listing is a - // subsequence of the whole list and its rows print byte-identically to the same rows of listing=all. - const ListedHits listed = listedHits( res, listing ); - // The emitted window. An explicit --limit overrides maxHits (the caller's 60-hit display default, or // SIZE_MAX under --detail); --offset skips whole rows and clamps at the end, so offset-past-the-end is // an empty page rather than an out-of-range read. maxHits can be SIZE_MAX, which pageWindow's int limit @@ -3087,12 +3091,18 @@ inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_ "the other end (what this page did not print). Page with limit= and offset=; the more element is " "absent exactly when this page reached the end of the hit list. " // lean-answers lane — the default LISTING and the tip/date hoist, defined where they appear. - "LISTING: by default only the kind=\"def\" rows are listed (listing=\"defs\" on the root) and the " + "LISTING: by default only the kind=\"def\" rows are listed (listing=\"defs\" on the root) when that page is " + "STRICTLY shorter in bytes than the page listing every hit (compared unpaged, as written and in the compact " + "dialect); otherwise the default lists every hit. Under listing=\"defs\" the " "kind=\"ref\" rows are COUNTED by the trailing refs element: count= is exactly the number of kind=\"ref\" " "rows in the hit list, none of them printed here, and its next= lists exactly those rows (listing=\"refs\", " "the same rows byte for byte that listing=all prints). listing= is absent when every hit is listed: the hit " - "list holds no kind=\"ref\" row, or no kind=\"def\" row (then the mentions are the answer), or the whole list " - "was asked for (the whereis-listing flag, value all). shown=, capped=, the paging attributes and the more " + "list holds no kind=\"ref\" row, or no kind=\"def\" row (then the mentions are the answer), or the defs page " + "would not be shorter, or the whole list was asked for (the whereis-listing flag, value all; its value defs " + "lists the definitions whatever the size). kind=\"def\" on a HEAD row is the parser's label, not a proof: a " + "definition the parser does not model (a Ruby define_method, a setattr, a name bound by assignment such as an " + "alias in a class body) is a kind=\"ref\" row, so under listing=\"defs\" it is among the counted refs. " + "shown=, capped=, the paging attributes and the more " "element window the LISTED rows; hits= still counts every row. " "TIP AND DATE: a row whose commit is HEAD's omits tip= and date=: its tip is at= without +dirty and its " "date is head_date= on the root. Every other row carries both. " @@ -3128,6 +3138,51 @@ inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_ rw::emitRaw( out, "" ); } +// The bytes one whereis page DELIVERS on the posture that prices it: the compact dialect (the CLI and MCP default), +// or the page as written when that dialect has nothing to rewrite. +inline std::size_t whereisDeliveredBytes( std::string_view page ) +{ + const std::size_t compact = compactDeliveredBytes( page, "whereis" ); + return compact != 0 ? compact : page.size(); +} + +// THE DEFAULT, MEASURED (review B1). The Defs page pays a fixed overhead — listing=, the element +// and, under the compact legend, their readings — that eliding one or two short ref rows does not repay: on 24 of 58 +// sampled symbols the Defs page was LONGER than the All page while listing fewer rows. So both pages are rendered and +// Defs is served only when it is STRICTLY shorter both as written (the full legend, whose prose is the same text on +// both pages, so this compares the payload) and as the compact dialect delivers it (its per-answer readings ride only +// the page that carries their attribute, so a reading the All page's ref rows trigger can tip either way). A tie serves All: equal +// bytes, more rows. Both pages are rendered UNPAGED over the same row cap, so the choice is a fact of the answer, not +// of --limit/--offset: every page of one answer lists the same rows. A render that fails serves All (the whole answer). +inline ListedHits whereisServedListing( const WhereResult& res, std::size_t maxHits, WhereisListing listing ) +{ + if( listing != WhereisListing::ShorterOfDefsAll ) + { + return listedHits( res, listing ); + } + ListedHits defs = listedHits( res, WhereisListing::Defs ); + if( defs.attr.empty() ) + { + return defs; // Defs already degraded to every hit (no ref row, or no def row): nothing to compare + } + ListedHits all = listedHits( res, WhereisListing::All ); + const Rendered defsPage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, 0, 0, defs ); } ); + const Rendered allPage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, 0, 0, all ); } ); + const bool defsShorter = defsPage.ok && allPage.ok && defsPage.text.size() < allPage.text.size() + && whereisDeliveredBytes( defsPage.text ) < whereisDeliveredBytes( allPage.text ); + return defsShorter ? std::move( defs ) : std::move( all ); +} + +// One --whereis answer page: the listing it serves (whereisServedListing), then that page. +inline void writeWhereisPage( std::FILE* out, const WhereResult& res, std::size_t maxHits, int pageLimit, int pageOffset, + WhereisListing listing ) +{ + // The LISTED rows (WhereisListing): indices into res.hits, in the hit list's own order, so a listing is a + // subsequence of the whole list and its rows print byte-identically to the same rows of listing=all. + const ListedHits listed = whereisServedListing( res, maxHits, listing ); + writeWhereisListedPage( out, res, maxHits, pageLimit, pageOffset, listed ); +} + // The un-paginated form — unchanged contract, for callers that want the whole (capped) listing. inline void writeWhereis( std::FILE* out, const WhereResult& res, std::size_t maxHits ) { diff --git a/src/mcp.h b/src/mcp.h index 2aec52dc0..9b2d44184 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -2313,7 +2313,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // — the shared triple (flag + problem + the per-fault clause selectorrefuse.h speaks for // the CLI), not a hits="0" answer about the literal string. `seedFault` distinguishes it // from the non-git degrade, which is the other way whereisText returns "". - crossref::WhereisListing listing = crossref::WhereisListing::Defs; + crossref::WhereisListing listing = crossref::WhereisListing::ShorterOfDefsAll; const std::string listingRefusal = whereisListingFromArg( strArg( "listing" ), mcpdetail::findRawValue( args, "listing" ).isPresent, listing ); resp = !listingRefusal.empty() ? errResultMsg( -32602, listingRefusal ) : pagedResult( [ & ]( McpPageArgs pg ) diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 786a7223b..0684d6313 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -338,7 +338,7 @@ inline constexpr McpValueSpec kMcpValueFields[] = { { "var", "a STRING variable name inside the resolved definition (omit it to list the sliceable locals)", "var=\"out\"" }, { "flow", "a STRING flow direction: back, fwd or both (omit it for the flat per-line rows)", "flow=\"back\"" }, // lean-answers lane: whereis's CLOSED listing set, the CLI --whereis-listing= values verbatim. - { "listing", "a STRING: defs (default; refs counted), refs or all", "listing=\"all\"" }, + { "listing", "a STRING: defs (refs counted), refs or all; default: the shorter page", "listing=\"all\"" }, // lane/t10-mcp-coverage: rank_by's own CLOSED value set, the CLI --rank-by= spelling verbatim (cli.h's // parse arm). churn/churn-decay are valid CLI values that this row still names — the closed-set check // must accept them so the refusal can then say WHY they are unsupported HERE (a named gap, not an diff --git a/src/mcpverbs.h b/src/mcpverbs.h index d58654d86..565f9a5a0 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -556,7 +556,7 @@ inline std::string rankByText( const std::string& root, std::string_view mode, i // than answering a question the caller did not ask. inline std::string whereisText( const std::string& root, const std::string& symbol, const std::string& filter, std::size_t maxHits, McpPageArgs page = {}, bool* seedFault = nullptr, - crossref::WhereisListing listing = crossref::WhereisListing::Defs ) + crossref::WhereisListing listing = crossref::WhereisListing::ShorterOfDefsAll ) { std::string sel = symbol; std::string seedSpec; @@ -1054,7 +1054,7 @@ inline std::string grepAuxJson( const std::vector& hits, const PageW // was false: mcprefusal.h already registers the field, and the batch surface refuses loudly. // · `in` reaches the batch arm at all. It previously took the defaulted GrepIn::Code with no hatch. // Absent reads as the default, as an OPTIONAL field must; only a PRESENT unknown spelling refuses. -// `listing` on whereis — the CLI --whereis-listing= twin, the same closed set (defs|refs|all; absent = defs), refused on +// `listing` on whereis — the CLI --whereis-listing= twin, the same closed set (defs|refs|all; absent = the shorter of defs and all), refused on // any other value through the shared sentence so a typo never reads as the default listing. inline std::string whereisListingFromArg( std::string_view typed, bool isPresent, crossref::WhereisListing& out ) { diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 72af595d8..a39956f19 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -520,6 +520,12 @@ probeFor() # ripwire.whereis/v1 611->887: the default listing's readings — listing=, the element (and the # generic next= reading it pulls in), head_date= (the tip/date hoist) and complete='s "(of its listing=)" (+276 B). The # answer they ride is the lean one: its rows shrink from every reference to the definitions alone. +# RE-PINNED 2026-10-07 (lane/lean-answers-068 fix round 1, review B1 + its non-blocking item 2), measured on this gate's +# fixture: ripwire.whereis/v1 887->1023: the listing= reading gains "; default: defs only if strictly shorter than all" +# (+49 B: the default now serves the defs page only when it is strictly shorter than the all page) and "; a def the +# parser does not model (define_method, setattr, assignment) is a counted ref" (+87 B) — +136 B, nothing else moved. +# Both ride only a page that carries listing=. The OLD path keeps its own pin: the whereis-listing=all twin after the +# universe loop holds the whole-list page's legend at 700 B (measured 692), so it cannot grow inside this headroom. PIN_TABLE=' ripwire.edit-check/v1 610 595 ripwire.map/v1 910 892 @@ -581,7 +587,7 @@ ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 ripwire.merge-scout/v1 570 558 -ripwire.whereis/v1 900 887 +ripwire.whereis/v1 1040 1023 ripwire.community/v1 730 719 ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 @@ -720,6 +726,18 @@ else fi [ "$nRefuse" -ge 60 ] && ok "(U) $nRefuse non-XML flags refuse --legend=compact (empty stdout, non-zero exit); $nSkip write/serve/exec flags not probed" \ || no "(U) only $nRefuse non-XML flags refused compact (want ≥ 60)" +# (U-TWIN) the whole-list whereis page (--whereis-listing=all, the pre-listing answer) keeps its OWN pin: the default's pin +# rose to 1040 B for the listing= readings, and without this twin the old path could grow inside that headroom unseen. +# 700 = measured 692 (the base's 611 plus the head_date= reading) + 10, rounded up to 10. +( cd "$REPO" && "$BIN" . --whereis=distance --whereis-listing=all >"$TMP/wall.c" 2>/dev/null "$TMP/wall.f" 2>/dev/null ]*>' "$TMP/wall.c" | grep -q ' listing=' \ + && [ -n "$wallLb" ] && [ "$wallLb" -gt 0 ] 2>/dev/null && [ "$wallLb" -le 700 ]; then + ok "(U-TWIN) --whereis=distance --whereis-listing=all compact PROSE legend is $wallLb B (<= its 700 B pin; the default's pin is 1040)" +else + no "(U-TWIN) --whereis-listing=all compact PROSE legend is '${wallLb:-none}' B (pin 700; exit $rcWall): $( leg legend "$TMP/wall.c" | head -c 200 )" +fi echo echo "=== (F) the DEFAULT is the default posture; --legend=full restores MORE legend over the SAME payload (L1) ===" diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index 966a43da0..32c335e43 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -502,6 +502,9 @@ printf '#include "lean.h"\nint zqLean( int x )\n{\n return x + 1;\n}\nint zqD printf '#include "lean.h"\nint useA( void ) { return zqLean( 1 ); }\nint useB( void ) { return zqLean( 2 ); }\n' >"$LR/use.c" printf '# notes\n\nzqLean is the entry point; zqMentionOnly is a word no code defines.\n' >"$LR/NOTES.md" { printf '#include "lean.h"\n'; i=0; while [ $i -lt 70 ]; do printf 'int many%d( void ) { return zqLean( %d ); }\n' $i $i; i=$((i+1)); done; } >"$LR/many.c" +# fix round 1 (review B1): a symbol with ONE short ref row, where the defs page pays more than it elides +printf 'int zqOne( void ) { return 1; }\n' >"$LR/one.c" +printf 'int oneUse( void ) { return zqOne(); }\n' >"$LR/one_use.c" lg add -A; lg commit -qm base LHEAD="$( git -C "$LR" rev-parse --short=9 HEAD )"; LFEAT="$( git -C "$LR" rev-parse --short=9 feat-lean )" lw(){ "$BIN" "$LR" --whereis="$1" --no-cache "${@:2}" 2>/dev/null; } @@ -582,6 +585,71 @@ else printf ' SKIP LEAN (L10) MCP twin (no python3)\n' fi +# ── THE DEFAULT IS THE SHORTER PAGE (fix round 1, review B1; crossref.h whereisServedListing) ──────────────────── +# The default serves the defs page only when it is STRICTLY shorter, in bytes, than the listing=all page (as written +# AND in the compact dialect); otherwise it serves the all page. An explicit --whereis-listing=defs is served as asked, +# so each arm reads the two explicit pages of the same binary and checks which one the default is, byte for byte. +nbytes(){ printf '%s' "$1" | wc -c | tr -d ' '; } +# (L11) a 1-ref symbol: the defs page is LONGER than the all page, so the default IS the all page (no listing=, no ) +O_D="$( lw zqOne )"; O_A="$( lw zqOne --whereis-listing=all )"; O_F="$( lw zqOne --whereis-listing=defs )" +{ [ -n "$( printf '%s' "$O_A" | hits_of ref )" ] && printf '%s' "$O_F" | grep -q ' all page $( nbytes "$O_A" ) B, so the default is byte-identical to listing=all" \ + || { no "LEAN (L11): the 1-ref default is not the (shorter) all page: default $( nbytes "$O_D" ) B, all $( nbytes "$O_A" ) B, defs $( nbytes "$O_F" ) B"; printf '%s\n' "$O_D" | lroot; } +# (L12) a many-ref symbol (73 refs) stays lean: the default IS the explicit defs page, strictly shorter than all +M_F="$( lw zqLean --whereis-listing=defs )"; M_A="$( lw zqLean --whereis-listing=all )" +{ [ "$LDEF" = "$M_F" ] && printf '%s' "$LDEF" | lroot | grep -q ' listing="defs"' && [ "$( nbytes "$M_F" )" -lt "$( nbytes "$M_A" )" ]; } \ + && ok "LEAN (L12): 73-ref symbol — the default is the defs page ($( nbytes "$M_F" ) B < all $( nbytes "$M_A" ) B)" \ + || no "LEAN (L12): the many-ref default is not the defs page (default $( nbytes "$LDEF" ) B, defs $( nbytes "$M_F" ) B, all $( nbytes "$M_A" ) B)" +# (L13) the TIE: three repos identical but for the length of the one ref row's text. The probe repo measures how much +# longer the defs page is (D); padding the ref row by D more bytes makes the two pages EQUAL. A tie serves all (equal +# bytes, more rows); one byte more and defs is strictly shorter, one byte less and all is. +mk_tie(){ local d="$TMP/tie$1"; mkdir -p "$d"; git -C "$d" init -q -b main >/dev/null 2>&1; git -C "$d" config commit.gpgsign false + printf 'int zqTie( int x ) { return x; }\n' >"$d/tie.c" + printf 'int useT( void ) { return zqTie( 1 ); } // %s\n' "$( head -c "$1" "$d/use.c" + git -C "$d" add -A >/dev/null 2>&1; git -C "$d" commit -qm tie >/dev/null 2>&1; printf '%s' "$d"; } +tw(){ "$BIN" "$1" --whereis=zqTie --no-cache "${@:2}" 2>/dev/null; } +TP="$( mk_tie 1 )"; TD=$(( $( nbytes "$( tw "$TP" --whereis-listing=defs )" ) - $( nbytes "$( tw "$TP" --whereis-listing=all )" ) )) +if [ "$TD" -gt 1 ] 2>/dev/null; then + for k in -1 0 1; do + TR="$( mk_tie $(( 1 + TD + k )) )" + T_D="$( tw "$TR" )"; T_F="$( tw "$TR" --whereis-listing=defs )"; T_A="$( tw "$TR" --whereis-listing=all )" + TF="$( nbytes "$T_F" )"; TA="$( nbytes "$T_A" )" + case $k in + -1) { [ $(( TF - TA )) -eq 1 ] && [ "$T_D" = "$T_A" ]; } \ + && ok "LEAN (L13a): defs page 1 B LONGER ($TF vs $TA) — the default is the all page" \ + || no "LEAN (L13a): defs $TF / all $TA, default $( nbytes "$T_D" ) B — want the all page" ;; + 0) { [ "$TF" -eq "$TA" ] && [ "$T_D" = "$T_A" ] && [ "$T_D" != "$T_F" ]; } \ + && ok "LEAN (L13b): defs page and all page EQUAL ($TF B) — the tie serves the all page (more rows)" \ + || no "LEAN (L13b): tie defs $TF / all $TA, default $( nbytes "$T_D" ) B — want the all page on a tie" ;; + 1) { [ $(( TA - TF )) -eq 1 ] && [ "$T_D" = "$T_F" ]; } \ + && ok "LEAN (L13c): defs page 1 B SHORTER ($TF vs $TA) — the default is the defs page" \ + || no "LEAN (L13c): defs $TF / all $TA, default $( nbytes "$T_D" ) B — want the defs page" ;; + esac + done +else + no "LEAN (L13): the tie probe measured no defs-page overhead (D='$TD'): the probe did not run" +fi +# (L14) MCP twin: the same served page over MCP — the 1-ref default is the all page, the tie serves all, the +# many-ref default is the defs page; each byte-identical to the CLI. +mcpq(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whereis","arguments":{"path":"%s","symbol":"%s"%s}}}\n' "$1" "$2" "$3" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line); c=d.get("result",{}).get("content") + if c: print(c[0].get("text",""),end="") + elif "error" in d: print("ERROR",d["error"].get("code"),end="")'; } +if command -v python3 >/dev/null 2>&1; then + TT="$TMP/tie$(( 1 + TD ))" + { [ "$( mcpq "$LR" zqOne '' )" = "$O_A" ] && [ "$( mcpq "$LR" zqOne ',"listing":"defs"' )" = "$O_F" ] \ + && [ "$( mcpq "$LR" zqLean '' )" = "$M_F" ] && [ -d "$TT" ] && [ "$( mcpq "$TT" zqTie '' )" = "$( tw "$TT" --whereis-listing=all )" ]; } \ + && ok "LEAN (L14): MCP whereis serves the same page as the CLI default — all on the 1-ref symbol and the tie, defs on the 73-ref symbol" \ + || no "LEAN (L14): the MCP default serves a different page than the CLI" +else + printf ' SKIP LEAN (L14) MCP twin (no python3)\n' +fi + if command -v xmllint >/dev/null 2>&1; then if printf '%s' "$SW" | xmllint --noout - 2>/dev/null; then ok "capped stray-content still G4 clean"; else no "capped stray-content XML malformed"; fi if printf '%s' "$W1" | xmllint --noout - 2>/dev/null; then ok "paged whereis still G4 clean"; else no "paged whereis XML malformed"; fi diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index c22ab9ca7..c84d12834 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -288,6 +288,10 @@ tools = json.loads( line )[ "result" ][ "tools" ] # ARGUMENT, no prose: whereis takes `listing` (defs|refs|all, the CLI --whereis-listing= twin), whose default (defs) lists the # definitions and counts the references. Attributed: whereis schema 714 -> 810 B (+96 B, the field and its obliged # description), whereis description unchanged at 701 B, every other tool unchanged. Headroom after this line: 22 B. +# RE-MEASURED 2026-10-07 (lane/lean-answers-068 fix round 1), CEILING UNMOVED at 46,850: 46,828 -> 46,846 B. The listing +# field's description now says what its default does ("defs (refs counted), refs or all; default: the shorter page": +# absent `listing` serves the defs page only when it is strictly shorter than the all page), +18 B in whereis's schema; +# every other tool unchanged. Headroom after this line: 4 B. CEILING = 46850 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 008d6a8a6..724e94e0f 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 c4d340f8ad76e35bf282d4dcd0c82abe15324a6d0fc1559ba1107df538760214 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 6d8f62342517d63043bc48883f9b420a17db647f10831195c4118fe6e7667870 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 62d2d7dc1b44d708e44cecdf4a351833f07cd5d34abfc2666073cdc0db65da51 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 83f5195cee26479c5d751b6cab18a4d1fc37051b105d20eefc7c70d5603954e9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 1c08714cfd64f810ceb81055ce8f96cb28a5348e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 19:47:01 -0400 Subject: [PATCH 137/229] showcase capture: blocks for --whereis-listing=all and --mcp-legend=inline The capture covered neither new flag (showcasecapturecheck (D)): one block shows the whole hit list the default whereis page is measured against, one an MCP stdio session under --mcp-legend=inline. Co-Authored-By: Claude Opus 5.5 --- test/showcase_capture.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/showcase_capture.py b/test/showcase_capture.py index ee03fbab3..1dca8419e 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -489,6 +489,7 @@ def _strayScope( sub, kind ): add(S4, f"{BIN} . --stray-content=zzzz-no-such-ref --plan", "A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.") add(S4, f"{BIN} . --stray-content={shlex.quote( _famB )} --abi", f"Cross-branch ABI-break gate over {_strayScope( _famB, _famBKind )}: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.", timeout=600) add(S4, f"{BIN} . --whereis=rankGraphTeleport", "Which ref's tree defines or mentions SYM — HEAD first, then every local branch.", timeout=600) +add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=all", "The whole hit list, references included: the page the default is measured against. The default lists only the definitions (listing=\"defs\", the references counted in ) when that page is strictly shorter in bytes, and this page otherwise.", timeout=600) add(S4, f"{BIN} . --whereis=computeOnePairOverlap --with-history", "Same, plus a git-history row (never / removed-by-commit) for names no tree carries.", timeout=600) add(S4, f"{BIN} . --flags", "The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.") add(S4, f"{BIN} . --flags --flip=RIPWIRE_ASAN", "Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.") @@ -748,6 +749,7 @@ def chooseInWindow( lo=2, hi=14 ): post=mcp(MCP_INIT, '{"jsonrpc":"2.0","id":2,"method":"tools/list"}') + f" | tail -1 | python3 {manifest_py}", post_label="The manifest, summarised (name / description bytes / required args) — what the host pays in context every session:") add(S9, mcp(MCP_INIT, mcp_call("for", task="pagerank power iteration")), "MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.") +add(S9, mcp(MCP_INIT, mcp_call("whereis", symbol="rankGraphTeleport"), mcp_call("whereis", symbol="computeOnePairOverlap").replace('"id":2', '"id":3', 1)) + " --mcp-legend=inline", "--mcp-legend=inline: every answer of the stdio session carries its own legend (the default, session, sends each definition once per session after the first answer).") add(S9, mcp(MCP_INIT, mcp_call("explore", task="add a new output format flag to the CLI", budget_tokens=2000)), "MCP `explore` = --pack-task under a token budget, one call.") add(S9, mcp(MCP_INIT, mcp_call("fetch_body", handle="rankGraphTeleport")), "MCP `fetch_body`: the lazy-body handle posture — bodies only after ranked retrieval, by bare name here.") add(S9, mcp(MCP_INIT, mcp_call("grep", pattern="DISCLOSE", limit=3)), "MCP `grep` with paging args.") From 3ef120eec81d7511ac4142d27b466786b14b0954 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 20:22:32 -0400 Subject: [PATCH 138/229] defect-shape: precision rounds measured on this tree as all-new (vacuous-assert 178 -> 62 sites) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vacuous-assert judges only a RUN UNDER TEST: a pipe or capture whose command (past cd DIR &&, env assignments and env/nice/timeout/command/exec/time/stdbuf) is spelled through a variable or a path. A text utility over a file and a script-defined wrapper are not judged (stated floors), also inside printf "$( … )". - A derived capture (TAG="$( printf '%s' "$OUT" | … )") is followed back to its run; the absence is vacuous only when the chain ends in a run and no step is guarded. A presence test or requirement on a variable derived FROM the capture also guards it; a requirement inside a { …; } group counts. - The if form needs its else branch: then-FAIL with else-PASS is an absence; an elif chain is not judged. - utf8-cut: a cut length that is the compared bound less the ellipsis width (resize( 117 ) under size() > 120, the skillscan excerpt) is a cap. - Gate arms for each: capminus (positive), t_derived (positive), n_filepipe, n_wrapper, n_derivedguard, n_bracegroup, n_elif (negatives). Co-Authored-By: Claude Opus 5.5 --- src/defectshape.h | 297 ++++++++++++++++++++++++++++----------- test/defectshapecheck.sh | 61 +++++++- 2 files changed, 272 insertions(+), 86 deletions(-) diff --git a/src/defectshape.h b/src/defectshape.h index 889bd2ae6..21c0d8379 100644 --- a/src/defectshape.h +++ b/src/defectshape.h @@ -1046,13 +1046,29 @@ inline bool lengthIsSizeBound( std::string_view src, const std::vector& tok { continue; } - // V.size() OP N - if( k + 6 < to && isCompareOp( src, toks[k + 5] ) && tokText( src, toks[k + 6] ) == n ) + // V.size() OP M / M OP V.size(), where the cut length N is M, or M less the ellipsis width (1 to 3 bytes: + // resize( 117 ) under size() > 120 leaves room for "...") + const auto boundMatches = [ & ]( const Tok& m ) + { + const std::string_view mt = tokText( src, m ); + if( mt == n || n == std::string( mt ) + " - 1" || n == std::string( mt ) + " - 2" || n == std::string( mt ) + " - 3" ) + { + return true; + } + if( m.kind != TokKind::Number || n.empty() || !std::all_of( n.begin(), n.end(), isDigit ) || n.size() > 9 + || !std::all_of( mt.begin(), mt.end(), isDigit ) || mt.size() > 9 ) + { + return false; + } + const long nv = std::stol( n ); + const long mv = std::stol( std::string( mt ) ); + return mv >= nv && mv - nv <= 3; + }; + if( k + 6 < to && isCompareOp( src, toks[k + 5] ) && boundMatches( toks[k + 6] ) ) { return true; } - // N OP V.size() - if( k >= 2 && isCompareOp( src, toks[k - 1] ) && tokText( src, toks[k - 2] ) == n ) + if( k >= 2 && isCompareOp( src, toks[k - 1] ) && boundMatches( toks[k - 2] ) ) { return true; } @@ -1668,9 +1684,9 @@ enum class Reporter : std::uint8_t // an exit / return with a nonzero status, or a fail-flag assignment. inline Reporter reporterOf( const std::vector& t, std::size_t k, const std::vector>& defined ) { - while( k < t.size() && !t[k].op && ( t[k].text == "{" || t[k].text == "then" || t[k].text == "else" ) ) + while( k < t.size() && ( ( !t[k].op && ( t[k].text == "{" || t[k].text == "then" || t[k].text == "else" ) ) || ( t[k].op && t[k].text == ";" ) ) ) { - ++k; + ++k; // the branch's opening words and the line breaks (joined as ';') before its first command } if( k >= t.size() || t[k].op ) { @@ -1790,9 +1806,49 @@ inline bool isQuietGrep( const std::vector& t, std::size_t b, std::size_t return quiet; } +// Is the command at [b, e) a RUN UNDER TEST? Past `cd DIR &&`, environment assignments and the wrappers env / +// nice / timeout / command / exec / time / stdbuf, the command must be spelled through a variable ("$BIN") +// or a path. Not judged (stated floors): a text utility or builtin over a file — its input's provenance is +// not on this line — and a function the script defines, which may wrap a run or only read a variable. +inline bool isRunUnderTest( const std::vector& t, std::size_t b, std::size_t e ) +{ + std::size_t k = b; + while( k < e ) + { + const std::string& w = t[k].text; + if( !t[k].op && w == "cd" ) + { + while( k < e && !( t[k].op && t[k].text == "&&" ) ) + { + ++k; + } + ++k; + continue; + } + if( !t[k].op && ( w == "env" || w == "nice" || w == "timeout" || w == "command" || w == "exec" || w == "time" || w == "stdbuf" + || ( !w.empty() && w[0] == '-' ) || ( !w.empty() && isDigit( w[0] ) ) + || ( w.find( '=' ) != std::string::npos && w[0] != '$' && w[0] != '"' && w[0] != '\'' ) ) ) + { + ++k; + continue; + } + break; + } + if( k >= e || t[k].op ) + { + return false; + } + const std::string_view head = unquoted( t[k].text ); + return !head.empty() && ( head.front() == '$' || head.find( '/' ) != std::string_view::npos ); +} + inline GrepPipe grepPipe( const std::vector& t, std::size_t b, std::size_t e ) { GrepPipe g; + while( b < e && !t[b].op && ( t[b].text == "{" || t[b].text == "(" ) ) + { + ++b; // a brace group or subshell around the pipe: { echo "$V" | grep -q P && … ; } + } if( b < e && t[b].op && t[b].text == "!" ) { g.negated = true; @@ -1856,6 +1912,16 @@ inline GrepPipe grepPipe( const std::vector& t, std::size_t b, std::size_ } else if( t[k].text.find( "$(" ) != std::string::npos ) { + const std::vector inner = shTokens( std::string_view( t[k].text ).substr( t[k].text.find( "$(" ) + 2 ) ); + std::size_t innerEnd = 0; + while( innerEnd < inner.size() && !( inner[innerEnd].op && inner[innerEnd].text == "|" ) ) + { + ++innerEnd; + } + if( !isRunUnderTest( inner, 0, innerEnd ) ) + { + return g; // printf "$( wrapper … )": not a run under test + } g.direct = true; } } @@ -1872,9 +1938,9 @@ inline GrepPipe grepPipe( const std::vector& t, std::size_t b, std::size_ g.ok = true; return g; } - if( cmd == "cat" || cmd == "true" || cmd == "false" || cmd == ":" ) + if( !isRunUnderTest( t, p0, p0e ) ) { - return g; // a file's content or a constant: not a run under test + return g; // a direct pipe judges only a run under test } g.direct = true; g.ok = true; @@ -1968,19 +2034,15 @@ inline std::vector shAssertions( const ShLineToks& lt, const std::vect { from = 1; } - if( !t[from].op && ( t[from].text == "if" || t[from].text == "elif" || t[from].text == "while" ) ) + if( !t[from].op && ( t[from].text == "elif" || t[from].text == "while" ) ) + { + continue; // an elif / while condition is not judged (a stated floor) + } + if( !t[from].op && t[from].text == "if" ) { - std::size_t thenAt = t.size(); - for( std::size_t k = from + 1; k < t.size(); ++k ) - { - if( !t[k].op && t[k].text == "then" ) - { - thenAt = k; - break; - } - } std::size_t condEnd = from + 1; - while( condEnd < t.size() && !( t[condEnd].op && ( t[condEnd].text == ";" || t[condEnd].text == "&&" || t[condEnd].text == "||" ) ) ) + while( condEnd < t.size() && !( t[condEnd].op && ( t[condEnd].text == ";" || t[condEnd].text == "&&" || t[condEnd].text == "||" ) ) + && !( !t[condEnd].op && t[condEnd].text == "then" ) ) { ++condEnd; } @@ -1989,37 +2051,54 @@ inline std::vector shAssertions( const ShLineToks& lt, const std::vect { continue; } - // the then-branch's first command: on this line after `then`, or the next logical line - Reporter onThen = Reporter::None; - if( thenAt + 1 < t.size() ) + // the whole if … fi, lines joined with ';': its top-level then / elif / else, nested ifs skipped + std::vector st; + for( std::size_t j = li; j < lt.size() && j < li + 80; ++j ) { - onThen = reporterOf( t, thenAt + 1, defined ); + st.insert( st.end(), lt[j].begin(), lt[j].end() ); + st.push_back( { ";", true } ); } - else if( li + 1 < lt.size() ) + std::size_t thenAt = st.size(), elseAt = st.size(); + bool hasElif = false; + int depth = 0; + for( std::size_t k = 0; k < st.size(); ++k ) { - const std::vector& n = lt[li + 1]; - onThen = reporterOf( n, ( !n.empty() && n[0].text == "then" ) ? 1 : 0, defined ); - if( onThen == Reporter::None && !n.empty() && n[0].text == "then" && n.size() == 1 && li + 2 < lt.size() ) + if( st[k].op ) { - onThen = reporterOf( lt[li + 2], 0, defined ); + continue; + } + const std::string& w = st[k].text; + if( w == "if" ) + { + ++depth; + } + else if( w == "fi" ) + { + if( --depth == 0 ) + { + break; + } + } + else if( depth == 1 && w == "then" && thenAt == st.size() ) + { + thenAt = k; + } + else if( depth == 1 && w == "elif" ) + { + hasElif = true; + } + else if( depth == 1 && w == "else" ) + { + elseAt = k; } } - // an if whose then-branch reports failure is an absence when the else-branch (or nothing) passes; - // whose then-branch does anything else, it REQUIRES the text (a guard for an absence nested in it). - Polarity pol = Polarity::Requirement; - if( onThen == Reporter::Fail && !g.negated ) - { - pol = Polarity::Absence; - } - else if( onThen == Reporter::Pass && g.negated ) - { - pol = Polarity::Absence; - } - else if( onThen == Reporter::Fail && g.negated ) + if( thenAt == st.size() || hasElif ) { - pol = Polarity::Requirement; + continue; // no then found, or an elif chain (its branches are not judged) } - out.push_back( { li, g, pol } ); + const Reporter onThen = reporterOf( st, thenAt + 1, defined ); + const Reporter onElse = elseAt < st.size() ? reporterOf( st, elseAt + 1, defined ) : Reporter::None; + out.push_back( { li, g, polarityOf( g.negated, onThen, onElse ) } ); continue; } std::vector list( t.begin() + static_cast( from ), t.end() ); @@ -2086,6 +2165,53 @@ inline std::size_t assignmentOf( const ShLineToks& lt, const std::vector& lines, const ShLineToks& lt, const std::vector& scope, + const std::vector& asserts, std::size_t absenceLine, std::string_view v ) +{ + const std::string q1 = "\"$" + std::string( v ) + "\""; + const std::string q2 = "\"${" + std::string( v ) + "}\""; + const std::string q3 = "$" + std::string( v ); + const std::string q4 = "${#" + std::string( v ) + "}"; + for( std::size_t k = 0; k < lines.size(); ++k ) + { + if( scope[k] != scope[absenceLine] || k == absenceLine ) + { + continue; + } + const std::string& l = lines[k].text; + for( const char* test : { "-n ", "-z " } ) + { + for( std::size_t p = l.find( test ); p != std::string::npos; p = l.find( test, p + 1 ) ) + { + const std::string_view rest = std::string_view( l ).substr( p + 3 ); + if( rest.starts_with( q1 ) || rest.starts_with( q2 ) || ( rest.starts_with( q3 ) && ( rest.size() == q3.size() || !isIdentChar( rest[q3.size()] ) ) ) ) + { + return true; + } + } + } + if( l.find( q4 ) != std::string::npos ) + { + return true; + } + const std::vector& ct = lt[k]; + if( ct.size() >= 3 && ct[0].text == "case" && varRef( ct[1].text ) == v ) + { + return true; + } + } + for( const ShAssert& a : asserts ) + { + if( a.line != absenceLine && scope[a.line] == scope[absenceLine] && a.pol == Polarity::Requirement && !a.pipe.direct && a.pipe.var == v ) + { + return true; + } + } + return false; +} + // Is the variable's capture guarded — its failure checked or its content required somewhere in scope? inline bool captureGuarded( const std::vector& lines, const ShLineToks& lt, const std::vector& scope, const std::vector& asserts, std::size_t absenceLine, std::size_t assignLine, std::string_view v, bool errexit ) @@ -2138,44 +2264,41 @@ inline bool captureGuarded( const std::vector& lines, const ShLineToks& { return true; } - // a presence test, a case on the text, or a requirement on the text anywhere in scope - const std::string q1 = "\"$" + std::string( v ) + "\""; - const std::string q2 = "\"${" + std::string( v ) + "}\""; - const std::string q3 = "$" + std::string( v ); - const std::string q4 = "${#" + std::string( v ) + "}"; + // a presence test, a case on the text, or a requirement on the text anywhere in scope — on the variable + // itself or on one DERIVED from it (W="$( printf '%s' "$V" | … )" then a check on W): a check that W is + // non-empty or carries a pattern is a check that V had content. + if( contentRequired( lines, lt, scope, asserts, absenceLine, v ) ) + { + return true; + } + const std::string ref1 = "\"$" + std::string( v ) + "\""; for( std::size_t k = 0; k < lines.size(); ++k ) { - if( scope[k] != scope[absenceLine] || k == absenceLine ) + if( scope[k] != scope[absenceLine] || k == absenceLine || lt[k].empty() || lt[k][0].op ) { continue; } - const std::string& l = lines[k].text; - for( const char* test : { "-n ", "-z " } ) + const std::string& w0 = lt[k][0].text; + const std::size_t eqAt = w0.find( '=' ); + if( eqAt == std::string::npos || eqAt == 0 || w0.find( "$(" ) == std::string::npos ) { - for( std::size_t p = l.find( test ); p != std::string::npos; p = l.find( test, p + 1 ) ) - { - const std::string_view rest = std::string_view( l ).substr( p + 3 ); - if( rest.starts_with( q1 ) || rest.starts_with( q2 ) || ( rest.starts_with( q3 ) && ( rest.size() == q3.size() || !isIdentChar( rest[q3.size()] ) ) ) ) - { - return true; - } - } + continue; } - if( l.find( q4 ) != std::string::npos ) + const std::string_view rhs = std::string_view( w0 ).substr( eqAt + 1 ); + const std::size_t cap = rhs.find( "$(" ); + const std::string_view in = rhs.substr( cap + 2 ); + std::size_t b = 0; + while( b < in.size() && isSpace( in[b] ) ) { - return true; + ++b; } - const std::vector& ct = lt[k]; - if( ct.size() >= 3 && ct[0].text == "case" && varRef( ct[1].text ) == v ) + if( ( in.substr( b ).starts_with( "printf" ) || in.substr( b ).starts_with( "echo" ) ) && in.find( ref1 ) != std::string_view::npos ) { - return true; - } - } - for( const ShAssert& a : asserts ) - { - if( a.line != absenceLine && scope[a.line] == scope[absenceLine] && a.pol == Polarity::Requirement && !a.pipe.direct && a.pipe.var == v ) - { - return true; + const std::string w = w0.substr( 0, eqAt ); + if( w != v && contentRequired( lines, lt, scope, asserts, absenceLine, w ) ) + { + return true; + } } } return false; @@ -2353,28 +2476,38 @@ inline void scanVacuousAssertBash( std::string_view src, const std::vector bool vacuous = a.pipe.direct; if( !vacuous ) { - // follow a derived capture (V="$( printf '%s' "$W" | … )") back to its source, up to three steps - std::string var = a.pipe.var; + // follow a derived capture (V="$( printf '%s' "$W" | … )") back to its source, up to three steps: the + // absence is vacuous only when the chain ends in a capture of a RUN UNDER TEST and no step of it is + // guarded (its failure checked, or its content required). + std::string var = a.pipe.var; bool guard = false; - bool found = false; - for( int step = 0; step < 3 && !guard; ++step ) + bool runSource = false; + for( int step = 0; step < 3 && !guard && !runSource; ++step ) { const std::size_t as = assignmentOf( lt, scope, a.line, var ); if( as == std::string::npos ) { break; } - const std::string& al = lines[as].text; - const std::size_t eq = al.find( var + "=" ); + const std::string& al = lines[as].text; + const std::size_t eq = al.find( var + "=" ); const std::string_view rhs = std::string_view( al ).substr( eq + var.size() + 1 ); - if( rhs.find( "$(" ) == std::string_view::npos && rhs.find( '`' ) == std::string_view::npos ) + if( rhs.find( "$(" ) == std::string_view::npos ) { break; // a literal or a copy: not a run under test } - guard = captureGuarded( lines, lt, scope, asserts, a.line, as, var, errexit[as] != 0 ); - found = true; - // derived from another variable's text? const std::vector inner = shTokens( rhs.substr( rhs.find( "$(" ) + 2 ) ); + std::size_t innerEnd = 0; + while( innerEnd < inner.size() && !( inner[innerEnd].op && inner[innerEnd].text == "|" ) ) + { + ++innerEnd; + } + guard = captureGuarded( lines, lt, scope, asserts, a.line, as, var, errexit[as] != 0 ); + if( isRunUnderTest( inner, 0, innerEnd ) ) + { + runSource = true; + break; + } std::string next; if( !inner.empty() && ( inner[0].text == "printf" || inner[0].text == "echo" ) ) { @@ -2389,11 +2522,11 @@ inline void scanVacuousAssertBash( std::string_view src, const std::vector } if( next.empty() ) { - break; + break; // a text utility over a file, or a wrapper function: not judged } var = next; } - vacuous = found && !guard; + vacuous = runSource && !guard; } if( vacuous ) { diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh index 24cada5d4..11d1508ab 100644 --- a/test/defectshapecheck.sh +++ b/test/defectshapecheck.sh @@ -669,6 +669,7 @@ std::string othervar( std::string buf, std::string label, std::size_t n ) { retu std::string replaced( const std::string& s, std::size_t n ) { return s; } std::string packname( const std::string& text, bool pack ) { return text; } std::string wordloop( const std::vector& words ) { std::string out; return out; } +std::string capminus( std::string s ) { return s; } std::u32string wide( std::u32string s, std::size_t n ) { return s; } std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } int drive() { return 0; } @@ -771,6 +772,15 @@ std::u32string wide( std::u32string s, std::size_t n ) } return s; } +std::string capminus( std::string s ) +{ + if( s.size() > 120 ) + { + s.resize( 117 ); + s += "..."; + } + return s; +} std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } std::string freshcut( std::string s ) { if( s.size() > 60 ) { s = s.substr( 0, 60 ) + "..."; } return s; } int drive() { return 1; } @@ -782,6 +792,7 @@ ds_has "$L" utf8-cut excerpt minor ds_has "$L" utf8-cut clip minor ds_has "$L" utf8-cut build minor ds_has "$L" utf8-cut erasecut minor +ds_has "$L" utf8-cut capminus minor ds_has "$L" utf8-cut freshcut new-symbol ds_wasnow "$L" utf8-cut excerpt 0 1 ds_none "$L" guarded "the cut backs off continuation bytes (0xC0/0x80)" @@ -949,8 +960,10 @@ gating_split "$L" 0 # checked, `|| no` on the capture, a presence check, a positive assertion on the same output before OR after # the absence, an enclosing if/case that requires content of the same capture, a plain capture under set -e # (errexit aborts on its failure), the match-is-pass polarity, a literal producer, a commented-out shape, a -# pre-existing vacuous line, a sound if, a new function with a handled capture, and the same shape in a -# script outside a test path. +# pre-existing vacuous line, a sound if, a new function with a handled capture, the same shape in a script +# outside a test path, a text utility over a file, a script-defined wrapper (stated floor), a presence test +# on a variable derived from the capture, a requirement inside a { …; } group, and an elif chain. A capture +# derived from another capture (TAG="$( printf '%s' "$OUT" | grep -o … )") is followed back to its run. fx_init vac mkdir -p "$WORK/vac/test" "$WORK/vac/scripts" VHDR="$( cat <<'EOF' @@ -965,7 +978,7 @@ EOF )" VHDR="$VHDR " -for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn t_setelocal t_setepipe n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok n_posafter n_nestif n_case n_sete n_seteuo; do +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn t_setelocal t_setepipe t_derived n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok n_posafter n_nestif n_case n_sete n_seteuo n_filepipe n_wrapper n_derivedguard n_bracegroup n_elif; do printf '%s' "$VHDR" >"$WORK/vac/test/$f.sh" done for f in t_setepipe n_sete; do printf 'set -e\n' >>"$WORK/vac/test/$f.sh"; done @@ -1138,9 +1151,44 @@ OUT="$( "$BIN" --list 2>/dev/null )" printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" EOF done +cat >>"$WORK/vac/test/t_derived.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +TAG="$( printf '%s' "$OUT" | grep -o ']*>' )" +printf '%s' "$TAG" | grep -q 'bad=' && no "bad= present" || ok "bad= absent" +EOF +cat >>"$WORK/vac/test/n_filepipe.sh" <<'EOF' +"$BIN" --list >"$TMPDIR/out.txt" 2>/dev/null +grep -v '^#' "$TMPDIR/out.txt" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_wrapper.sh" <<'EOF' +runit(){ "$BIN" "$@" 2>/dev/null; } +W="$( runit --list )" +printf '%s' "$W" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_derivedguard.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +ROWS="$( printf '%s' "$OUT" | grep -o ']*>' )" +[ -n "$ROWS" ] || no "the list has no rows" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_bracegroup.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +{ printf '%s' "$OUT" | grep -q ''; } && ok "the list is whole" || no "the list is cut" +printf '%s' "$OUT" | grep -q 'bad' && no "bad present" || ok "bad absent" +EOF +cat >>"$WORK/vac/test/n_elif.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +if printf '%s' "$OUT" | grep -q 'bad'; then + no "bad present" +elif printf '%s' "$OUT" | grep -q ' Date: Wed, 7 Oct 2026 20:25:31 -0400 Subject: [PATCH 139/229] resolve: a Go module alias proves a package by its exact import path only A local `replace github.com/up/lib => ./third/lib` beside an in-repo lib/ drew lib.Mark() to lib/l.go, because the proof fell back to "the candidate's directory ends the import path" whenever the exact path missed. collectGoModules now maps every local replace to the files under its directory (the replaced module path plus the directory below it), so each Go file carries its nearest-go.mod path and its replaced path. A module-alias call proves a candidate only when the import path equals one of them. The directory-tail rule is left for a candidate with no path when no in-tree file carries the import path either (a tree with no go.mod). receiverevidencecheck (G2): 15 arms over ten one-tree fixtures (fork2, tail, fork, rep, rep2, nest, pfx, vend, dot, mixed); 8 red on the previous head (the fork2 false row, and exact paths it declined), all green now. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 126 +++++++++++++++--- src/receiverevidence.h | 44 +++++- test/receiverevidencecheck.sh | 25 ++++ test/receiverevidencefix/gomod/README.md | 12 ++ test/receiverevidencefix/gomod/dot/app/app.go | 5 + test/receiverevidencefix/gomod/dot/decoy/d.go | 3 + test/receiverevidencefix/gomod/dot/go.mod | 3 + test/receiverevidencefix/gomod/dot/lib/l.go | 3 + .../receiverevidencefix/gomod/fork/app/app.go | 5 + test/receiverevidencefix/gomod/fork/go.mod | 5 + test/receiverevidencefix/gomod/fork/lib/l.go | 3 + .../gomod/fork2/app/app.go | 5 + test/receiverevidencefix/gomod/fork2/go.mod | 5 + test/receiverevidencefix/gomod/fork2/lib/l.go | 3 + .../gomod/fork2/third/lib/l.go | 3 + test/receiverevidencefix/gomod/mixed/lib/l.go | 4 + .../gomod/mixed/m/app/app.go | 5 + test/receiverevidencefix/gomod/mixed/m/go.mod | 3 + .../gomod/mixed/m/lib/l.go | 3 + .../receiverevidencefix/gomod/nest/app/app.go | 5 + test/receiverevidencefix/gomod/nest/go.mod | 3 + .../gomod/nest/other/go.mod | 3 + .../gomod/nest/other/util/u.go | 3 + test/receiverevidencefix/gomod/nest/util/u.go | 3 + test/receiverevidencefix/gomod/pfx/ab/go.mod | 3 + .../receiverevidencefix/gomod/pfx/ab/lib/l.go | 3 + test/receiverevidencefix/gomod/pfx/app/app.go | 5 + .../receiverevidencefix/gomod/pfx/app/near.go | 5 + test/receiverevidencefix/gomod/pfx/go.mod | 3 + test/receiverevidencefix/gomod/pfx/lib/l.go | 3 + test/receiverevidencefix/gomod/rep/app/app.go | 5 + .../receiverevidencefix/gomod/rep/app/near.go | 5 + test/receiverevidencefix/gomod/rep/go.mod | 5 + .../gomod/rep/legacy/go.mod | 3 + .../gomod/rep/legacy/lib/l.go | 3 + test/receiverevidencefix/gomod/rep/lib/l.go | 3 + .../receiverevidencefix/gomod/rep2/app/app.go | 5 + .../gomod/rep2/app/near.go | 5 + test/receiverevidencefix/gomod/rep2/go.mod | 5 + .../gomod/rep2/legacy/lib/l.go | 3 + test/receiverevidencefix/gomod/rep2/lib/l.go | 3 + .../receiverevidencefix/gomod/tail/app/app.go | 5 + .../gomod/tail/app/near.go | 5 + test/receiverevidencefix/gomod/tail/go.mod | 3 + test/receiverevidencefix/gomod/tail/lib/l.go | 3 + .../gomod/tail/pkg/lib/l.go | 3 + .../receiverevidencefix/gomod/vend/app/app.go | 5 + test/receiverevidencefix/gomod/vend/go.mod | 5 + test/receiverevidencefix/gomod/vend/lib/l.go | 3 + .../gomod/vend/vendor/github.com/x/lib/l.go | 3 + .../gomod/vend/vendor/modules.txt | 3 + 51 files changed, 367 insertions(+), 20 deletions(-) create mode 100644 test/receiverevidencefix/gomod/README.md create mode 100644 test/receiverevidencefix/gomod/dot/app/app.go create mode 100644 test/receiverevidencefix/gomod/dot/decoy/d.go create mode 100644 test/receiverevidencefix/gomod/dot/go.mod create mode 100644 test/receiverevidencefix/gomod/dot/lib/l.go create mode 100644 test/receiverevidencefix/gomod/fork/app/app.go create mode 100644 test/receiverevidencefix/gomod/fork/go.mod create mode 100644 test/receiverevidencefix/gomod/fork/lib/l.go create mode 100644 test/receiverevidencefix/gomod/fork2/app/app.go create mode 100644 test/receiverevidencefix/gomod/fork2/go.mod create mode 100644 test/receiverevidencefix/gomod/fork2/lib/l.go create mode 100644 test/receiverevidencefix/gomod/fork2/third/lib/l.go create mode 100644 test/receiverevidencefix/gomod/mixed/lib/l.go create mode 100644 test/receiverevidencefix/gomod/mixed/m/app/app.go create mode 100644 test/receiverevidencefix/gomod/mixed/m/go.mod create mode 100644 test/receiverevidencefix/gomod/mixed/m/lib/l.go create mode 100644 test/receiverevidencefix/gomod/nest/app/app.go create mode 100644 test/receiverevidencefix/gomod/nest/go.mod create mode 100644 test/receiverevidencefix/gomod/nest/other/go.mod create mode 100644 test/receiverevidencefix/gomod/nest/other/util/u.go create mode 100644 test/receiverevidencefix/gomod/nest/util/u.go create mode 100644 test/receiverevidencefix/gomod/pfx/ab/go.mod create mode 100644 test/receiverevidencefix/gomod/pfx/ab/lib/l.go create mode 100644 test/receiverevidencefix/gomod/pfx/app/app.go create mode 100644 test/receiverevidencefix/gomod/pfx/app/near.go create mode 100644 test/receiverevidencefix/gomod/pfx/go.mod create mode 100644 test/receiverevidencefix/gomod/pfx/lib/l.go create mode 100644 test/receiverevidencefix/gomod/rep/app/app.go create mode 100644 test/receiverevidencefix/gomod/rep/app/near.go create mode 100644 test/receiverevidencefix/gomod/rep/go.mod create mode 100644 test/receiverevidencefix/gomod/rep/legacy/go.mod create mode 100644 test/receiverevidencefix/gomod/rep/legacy/lib/l.go create mode 100644 test/receiverevidencefix/gomod/rep/lib/l.go create mode 100644 test/receiverevidencefix/gomod/rep2/app/app.go create mode 100644 test/receiverevidencefix/gomod/rep2/app/near.go create mode 100644 test/receiverevidencefix/gomod/rep2/go.mod create mode 100644 test/receiverevidencefix/gomod/rep2/legacy/lib/l.go create mode 100644 test/receiverevidencefix/gomod/rep2/lib/l.go create mode 100644 test/receiverevidencefix/gomod/tail/app/app.go create mode 100644 test/receiverevidencefix/gomod/tail/app/near.go create mode 100644 test/receiverevidencefix/gomod/tail/go.mod create mode 100644 test/receiverevidencefix/gomod/tail/lib/l.go create mode 100644 test/receiverevidencefix/gomod/tail/pkg/lib/l.go create mode 100644 test/receiverevidencefix/gomod/vend/app/app.go create mode 100644 test/receiverevidencefix/gomod/vend/go.mod create mode 100644 test/receiverevidencefix/gomod/vend/lib/l.go create mode 100644 test/receiverevidencefix/gomod/vend/vendor/github.com/x/lib/l.go create mode 100644 test/receiverevidencefix/gomod/vend/vendor/modules.txt diff --git a/src/graph.h b/src/graph.h index c95d5ff47..bf0cba3a9 100644 --- a/src/graph.h +++ b/src/graph.h @@ -4762,8 +4762,7 @@ struct FalseEdgeRules std::vector goDotImport; // fileId → the file has `import . "…"` std::vector goModules; // every in-tree go.mod's module path std::vector goUnderModule; // fileId → a go.mod sits at or above the file, inside the root - std::vector goPackagePath; // fileId → its package's import path: the nearest go.mod's module - // path + the directory below it ("" when no module line is found) + GoPackagePaths goPackages; // fileId → its package's import paths (receiverevidence.h) HashMap jsVocabulary; // jsModuleVocabulary, built only when a JS alias exists HashMap rustOutsideUse; // "#name": a Rust `use` of a path outside the crate names it std::vector> functionsByFile; // fileId → its functions/methods sorted by sigStartByte (enclosingFunction) @@ -5149,15 +5148,12 @@ inline std::string goModulePathOf( std::string_view text ) return {}; } -// The module paths a go.mod puts in the tree: its `module` line, and every `replace X => ./local` (or ../, /) left side — -// a module replaced by a LOCAL directory is this tree's code under another path (a multi-root workspace's sibling root). -inline std::vector goModuleTreePaths( std::string_view text ) +// A go.mod's LOCAL replaces, as ( module path, directory as written ): every `replace X => ./local` (or ../, /) — a +// module replaced by a local directory is this tree's code under another path (a multi-root workspace's sibling root). +// Both the one-line form and the lines of a `replace ( … )` block; a version on either side is dropped. +inline std::vector> goLocalReplaces( std::string_view text ) { - std::vector paths; - if( std::string m = goModulePathOf( text ); !m.empty() ) - { - paths.push_back( std::move( m ) ); - } + std::vector> out; for( std::size_t at = 0; at < text.size(); ) { std::size_t end = text.find( '\n', at ); @@ -5178,12 +5174,66 @@ inline std::vector goModuleTreePaths( std::string_view text ) const std::string_view target = trimWs( line.substr( arrow + 2 ) ); if( !left.empty() && !target.empty() && ( target.front() == '.' || target.front() == '/' ) ) { - paths.emplace_back( left.substr( 0, left.find_first_of( " \t" ) ) ); // drop a version: `X v1.2.3 => ./x` + out.emplace_back( left.substr( 0, left.find_first_of( " \t" ) ), // drop a version: `X v1.2.3 => ./x` + target.substr( 0, target.find_first_of( " \t" ) ) ); } } + return out; +} + +// The module paths a go.mod puts in the tree: its `module` line, and the left side of every local replace. +inline std::vector goModuleTreePaths( std::string_view text ) +{ + std::vector paths; + if( std::string m = goModulePathOf( text ); !m.empty() ) + { + paths.push_back( std::move( m ) ); + } + for( auto& [ module, dir ] : goLocalReplaces( text ) ) + { + paths.push_back( std::move( module ) ); + } return paths; } +// FE-B: root-relative directory `base` joined with a go.mod replace target `rel` (`./third/lib`, `../svc`); nullopt +// when the target is absolute or climbs above the root (no root-relative file can sit under it). +inline std::optional goJoinRelDir( std::string_view base, std::string_view rel ) +{ + if( rel.starts_with( '/' ) ) + { + return std::nullopt; + } + std::vector parts; + for( std::string_view p : { base, rel } ) + { + while( !p.empty() ) + { + const std::size_t slash = p.find( '/' ); + const std::string_view seg = p.substr( 0, slash ); + p = slash == std::string_view::npos ? std::string_view{} : p.substr( slash + 1 ); + if( seg == ".." ) + { + if( parts.empty() ) + { + return std::nullopt; + } + parts.pop_back(); + } + else if( !seg.empty() && seg != "." ) + { + parts.push_back( seg ); + } + } + } + std::string joined; + for( std::string_view p : parts ) + { + joined.append( joined.empty() ? 0 : 1, '/' ).append( p ); + } + return joined; +} + // FE-B: a Go package's import path — its go.mod's module path, then the package directory below the go.mod's directory // (`example.com/qm` + `quoted/lib` under `quoted` → `example.com/qm/lib`); "" when the go.mod has no module line. inline std::string goPackageImportPath( std::string_view modulePath, std::string_view modDir, std::string_view pkgDir ) @@ -5203,14 +5253,53 @@ inline std::string goPackageImportPath( std::string_view modulePath, std::string } // FE-B: record `fileId`'s package import path, its go.mod found in disk directory `disk` — root-relative, the walk's -// `walkRel` cut at `walkCut` (npos: the root itself) +// `walkRel` cut at `walkCut` (npos: the root itself) — and that go.mod's local replaces as ( root-relative dir, path ) inline void noteGoPackagePath( const IngestResult& ing, FalseEdgeRules& rules, const HashMap& modLineOfDir, const std::string& disk, std::uint32_t fileId, std::string_view walkRel, std::size_t walkCut ) { if( const auto ml = modLineOfDir.find( disk ); ml != modLineOfDir.end() ) { const std::string_view modDir = walkCut == std::string_view::npos ? std::string_view{} : walkRel.substr( 0, walkCut ); - rules.goPackagePath[ fileId ] = goPackageImportPath( ml->second, modDir, includerDir( rootRelPath( ing, fileId ) ) ); + rules.goPackages.note( rules.goPackages.nearest, fileId, goPackageImportPath( ml->second, modDir, includerDir( rootRelPath( ing, fileId ) ) ) ); + } +} + +// FE-B: a go.mod in root-relative directory `modDir` maps each local replace's directory to its module path +inline void noteGoLocalReplaces( std::string_view text, std::string_view modDir, std::vector>& replacedDirs ) +{ + for( auto& [ module, dir ] : goLocalReplaces( text ) ) + { + if( std::optional at = goJoinRelDir( modDir, dir ) ) + { + replacedDirs.emplace_back( std::move( *at ), std::move( module ) ); + } + } +} + +// FE-B: every Go file under a locally replaced directory (the deepest one when replaces nest) gets the replaced module's +// path plus the directory below it — `replace github.com/up/lib => ./third/lib` makes third/lib/x.go `github.com/up/lib` +inline void noteGoReplacedPaths( const IngestResult& ing, FalseEdgeRules& rules, const std::vector& isGoFile, + const std::vector>& replacedDirs ) +{ + for( std::uint32_t f = 0; f < isGoFile.size() && !replacedDirs.empty(); ++f ) + { + if( isGoFile[ f ] == 0 ) + { + continue; + } + const std::string_view pkgDir = includerDir( rootRelPath( ing, f ) ); + const std::pair* best = nullptr; + for( const auto& rd : replacedDirs ) + { + if( ( rd.first.empty() || pathIsUnder( pkgDir, rd.first ) ) && ( best == nullptr || rd.first.size() > best->first.size() ) ) + { + best = &rd; + } + } + if( best != nullptr ) + { + rules.goPackages.note( rules.goPackages.replaced, f, goPackageImportPath( best->second, best->first, pkgDir ) ); + } } } @@ -5219,16 +5308,20 @@ inline void noteGoPackagePath( const IngestResult& ing, FalseEdgeRules& rules, c inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) { rules.goUnderModule.assign( ing.files.size(), 0 ); - rules.goPackagePath.assign( ing.files.size(), std::string{} ); + rules.goPackages.nearest.assign( ing.files.size(), std::string{} ); + rules.goPackages.replaced.assign( ing.files.size(), std::string{} ); HashMap> modOfDir; // disk directory → its go.mod's tree paths (empty: no go.mod) HashMap modLineOfDir; // disk directory → its go.mod's own `module` path HashMap seenModule; + std::vector isGoFile( ing.files.size(), 0 ); + std::vector> replacedDirs; // ( root-relative replaced directory, module path ) for( const Symbol& s : ing.symbols ) { if( s.lang != Lang::Go || s.fileId >= ing.files.size() || rules.goUnderModule[ s.fileId ] != 0 ) { continue; } + isGoFile[ s.fileId ] = 1; std::string_view rel = rootRelPath( ing, s.fileId ); std::string disk = diskPath( ing, s.fileId ); // walk up as many directories as the root-relative path has, never above the root @@ -5248,6 +5341,7 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) { it->second = goModuleTreePaths( *text ); modLineOfDir.try_emplace( disk, goModulePathOf( *text ) ); + noteGoLocalReplaces( *text, relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ), replacedDirs ); } } if( !it->second.empty() ) @@ -5271,6 +5365,8 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) } } std::sort( rules.goModules.begin(), rules.goModules.end() ); // determinism: discovery order follows symbol order + std::sort( replacedDirs.begin(), replacedDirs.end() ); // … and so would a tie between two replaces of one dir + noteGoReplacedPaths( ing, rules, isGoFile, replacedDirs ); } // FE-A: the binding facts — every ModuleAlias (Go dot imports apart), every name a JS/TS import or alias binds, and every @@ -5839,7 +5935,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { PROFILE_SCOPE_DESCRIBE( "buildGraph/2i: receiver-evidence tables" ); recvEv.localNames = &fieldNarrow.localNameSet; - recvEv.goPackagePath = &falseEdges.goPackagePath; + recvEv.goPackages = &falseEdges.goPackages; recvEv.build(); } HashMap nameOnlyEdges; // (from<<32|to) of an edge some call bound by NAME ALONE (no receiver evidence) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 16e765585..0f383c368 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -46,6 +46,37 @@ inline bool receiverHedgeLang( Lang l ) noexcept } } +// FE-B: each Go file's package import paths, built by graph.h collectGoModules — the nearest go.mod's module path plus the +// directory below it, and the path a LOCAL `replace L => ./D` gives a file under D (L plus the directory below D). A +// module-alias call proves a candidate by one of these paths EXACTLY. The directory-tail rule is left only for a candidate +// with no path when no in-tree file carries the import path either (a tree with no module facts for it at all). +struct GoPackagePaths +{ + std::vector nearest; // fileId → "" when no go.mod with a module line sits at or above the file + std::vector replaced; // fileId → "" when no local replace maps the file's directory + HashMap known; // every non-empty path in either table + + bool hasPath( std::uint32_t f ) const noexcept + { + return ( f < nearest.size() && !nearest[ f ].empty() ) || ( f < replaced.size() && !replaced[ f ].empty() ); + } + // does `path` name file `f`'s package exactly + bool names( std::uint32_t f, std::string_view path ) const noexcept + { + return ( f < nearest.size() && !nearest[ f ].empty() && nearest[ f ] == path ) + || ( f < replaced.size() && !replaced[ f ].empty() && replaced[ f ] == path ); + } + void note( std::vector& table, std::uint32_t f, std::string path ) + { + EXPECTS( f < table.size(), "collectGoModules sizes both tables to ing.files" ); + if( !path.empty() ) + { + known.try_emplace( path, '\0' ); + } + table[ f ] = std::move( path ); + } +}; + // The languages where a bare `m()` inside a method is an implicit `this.m()` (or reaches a free/top-level function). inline bool implicitReceiverLang( Lang l ) noexcept { @@ -134,7 +165,7 @@ struct ReceiverEvidence mutable rw::SmallVec found; bool active = false; const HashMap* localNames = nullptr; // graph.h FieldNarrowTables::localNameSet - const std::vector* goPackagePath = nullptr; // graph.h FalseEdgeRules::goPackagePath + const GoPackagePaths* goPackages = nullptr; // graph.h FalseEdgeRules::goPackages ReceiverEvidence( const IngestResult& i, const HashMap>& up, const HashMap& classes ) : ing( i ), chaUp( up ), classNames( classes ) {} @@ -696,13 +727,16 @@ struct ReceiverEvidence if( r.lang == Lang::Go ) { // an import path names a package: the candidate's own package import path (its go.mod's module path and the - // directory below it — a nested module's `quoted/lib` is `example.com/qm/lib`) proves it exactly + // directory below it — a nested module's `quoted/lib` is `example.com/qm/lib` — or the path a local replace + // maps its directory to) proves it EXACTLY. A candidate with a path, or an import path some in-tree file + // carries, is proven by nothing else: a directory that merely ends the path is another package const std::uint32_t cf = ing.symbols[ c ].fileId; - if( goPackagePath != nullptr && cf < goPackagePath->size() && !( *goPackagePath )[ cf ].empty() && source == ( *goPackagePath )[ cf ] ) + if( goPackages != nullptr && ( goPackages->hasPath( cf ) || goPackages->known.contains( key.assign( source ) ) ) ) { - return true; + return goPackages->names( cf, source ); } - // else the candidate's directory must end the path, segment-aligned (a replace-only tree, no go.mod above) + // no module facts name this path or the candidate (a tree with no go.mod): its directory must end the path, + // segment-aligned const std::string_view dir = includerDir( target ); return !dir.empty() && source.size() >= dir.size() && source.ends_with( dir ) && ( source.size() == dir.size() || source[ source.size() - dir.size() - 1 ] == '/' ); diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index d327da22a..38f85bdd6 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -80,6 +80,13 @@ # Merger.Get/Length; Bd an outside screen's Size, an outside value's Runes, a field or local named `log` # of an outside type beside the in-repo log.Errorf. Near misses: a pointer-literal local, the method # receiver itself, a real import of the in-repo log package. +# (G2) Go module aliases (gomod/, one tree per directory): an import path proves a package EXACTLY — the nearest +# go.mod's module path plus the directory below it, or the path a local `replace L => ./D` gives D. Bm: +# `replace github.com/up/lib => ./third/lib` beside an in-repo lib/ drew lib.Mark() to lib/ (a directory +# that merely ends the path); `example.com/t/pkg/lib` proved both pkg/lib and lib/ (declined). Near misses: +# a replace onto lib/ itself, a replaced legacy/ with and without its own go.mod, a nested module, +# example.com/a vs example.com/ab, a vendored copy (never the in-repo lib/), a dot import (never the decoy), +# and a path-less root lib/ beside a nested module that carries the path. # (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE # the tree (or that imports the name from outside) never proves an unrelated class's method. Near # misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), @@ -463,6 +470,24 @@ exactproven go callees src/merger.go:First "method Get src/merger.go" exactproven go callees src/tui/tcell.go:Lines "method Size src/tui/tcell.go" exactproven go callees src/cmd/run.go:Run "fn Errorf src/internal/log/log.go" # guard +echo "=== (G2) Go module aliases: an import path proves a package exactly ===" +mkdir -p "$TMP/gomod" +exactproven gomod/fork2 callees app/app.go:Use "fn Mark third/lib/l.go" # Bm: lib/ merely ends github.com/up/lib +notproven gomod/fork2 callees app/app.go:Use "Mark lib/l.go" +exactproven gomod/tail callees app/app.go:Use "fn Mark pkg/lib/l.go" # Bm: lib/ ends example.com/t/pkg/lib too +exactproven gomod/tail callees app/near.go:Near "fn Mark lib/l.go" +exactproven gomod/fork callees app/app.go:Use "fn Mark lib/l.go" # near miss: the replace names lib/ itself +exactproven gomod/rep callees app/app.go:Use "fn Mark legacy/lib/l.go" # near miss: replaced, with its own go.mod +exactproven gomod/rep callees app/near.go:Near "fn Mark lib/l.go" +exactproven gomod/rep2 callees app/app.go:Use "fn Mark legacy/lib/l.go" # near miss: replaced, under the root go.mod only +exactproven gomod/rep2 callees app/near.go:Near "fn Mark lib/l.go" +exactproven gomod/nest callees app/app.go:Use "fn Mark other/util/u.go" # near miss: a nested module +exactproven gomod/pfx callees app/app.go:Use "fn Mark ab/lib/l.go" # near miss: example.com/a vs example.com/ab +exactproven gomod/pfx callees app/near.go:Near "fn Mark lib/l.go" +notproven gomod/vend callees app/app.go:Use "Mark lib/l.go" # near miss: the vendored copy is the package +notproven gomod/dot callees app/app.go:Use "Mark decoy/d.go" # near miss: a dot import (lib/ unresolved: a floor) +exactproven gomod/mixed callees m/app/app.go:Use "fn Mark m/lib/l.go" # near miss: a path-less lib/ never takes a carried path + echo "=== (I) implicit receiver: a bare call never proves an unrelated class's method ===" notproven java callees src/main/java/app/Logger.java:line "render src/main/java/app/Exporter.java" notproven java callees src/main/java/app/Logger.java:close "flush src/main/java/app/Buffer.java" "flush src/main/java/app/Exporter.java" diff --git a/test/receiverevidencefix/gomod/README.md b/test/receiverevidencefix/gomod/README.md new file mode 100644 index 000000000..e5057ce42 --- /dev/null +++ b/test/receiverevidencefix/gomod/README.md @@ -0,0 +1,12 @@ +# gomod — Go module-alias proof (receiverevidencecheck.sh section (G2)) + +Each directory is its own tree (its own `go.mod`), indexed alone. A module-alias call `lib.Mark()` is proven onto a +candidate only when the import path EXACTLY names the candidate's package: the nearest go.mod's module path plus the +directory below it, or the path a local `replace L => ./D` gives a file under D. Never built. + +- fork2: `replace github.com/up/lib => ./third/lib` beside an in-repo `lib/` (another package): only `third/lib` is proven. +- tail: `example.com/t/pkg/lib` — `pkg/lib` is the package; `lib/` merely ends the path. +- fork: `replace github.com/up/lib => ./lib`. rep / rep2: a replaced `legacy/` with and without its own go.mod. +- nest: a nested module `other/`. pfx: `example.com/a` vs `example.com/ab`. vend: a vendored copy. dot: a dot import. +- mixed: no go.mod at the root; the nested module `m/` carries `example.com/x/lib`, and the root's `lib/` (no import + path at all) merely ends it — the directory-tail rule never applies to a path an in-tree module carries. diff --git a/test/receiverevidencefix/gomod/dot/app/app.go b/test/receiverevidencefix/gomod/dot/app/app.go new file mode 100644 index 000000000..ece5831ba --- /dev/null +++ b/test/receiverevidencefix/gomod/dot/app/app.go @@ -0,0 +1,5 @@ +package app + +import . "example.com/d/lib" + +func Use(n int) int { return Mark(n) } diff --git a/test/receiverevidencefix/gomod/dot/decoy/d.go b/test/receiverevidencefix/gomod/dot/decoy/d.go new file mode 100644 index 000000000..5721f1295 --- /dev/null +++ b/test/receiverevidencefix/gomod/dot/decoy/d.go @@ -0,0 +1,3 @@ +package decoy + +func Mark(n int) int { return n + 9 } diff --git a/test/receiverevidencefix/gomod/dot/go.mod b/test/receiverevidencefix/gomod/dot/go.mod new file mode 100644 index 000000000..9edd4200f --- /dev/null +++ b/test/receiverevidencefix/gomod/dot/go.mod @@ -0,0 +1,3 @@ +module example.com/d + +go 1.22 diff --git a/test/receiverevidencefix/gomod/dot/lib/l.go b/test/receiverevidencefix/gomod/dot/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/dot/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/fork/app/app.go b/test/receiverevidencefix/gomod/fork/app/app.go new file mode 100644 index 000000000..2f7675767 --- /dev/null +++ b/test/receiverevidencefix/gomod/fork/app/app.go @@ -0,0 +1,5 @@ +package app + +import "github.com/up/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/fork/go.mod b/test/receiverevidencefix/gomod/fork/go.mod new file mode 100644 index 000000000..c92da86ae --- /dev/null +++ b/test/receiverevidencefix/gomod/fork/go.mod @@ -0,0 +1,5 @@ +module example.com/f + +go 1.22 + +replace github.com/up/lib => ./lib diff --git a/test/receiverevidencefix/gomod/fork/lib/l.go b/test/receiverevidencefix/gomod/fork/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/fork/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/fork2/app/app.go b/test/receiverevidencefix/gomod/fork2/app/app.go new file mode 100644 index 000000000..2f7675767 --- /dev/null +++ b/test/receiverevidencefix/gomod/fork2/app/app.go @@ -0,0 +1,5 @@ +package app + +import "github.com/up/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/fork2/go.mod b/test/receiverevidencefix/gomod/fork2/go.mod new file mode 100644 index 000000000..31baa03bf --- /dev/null +++ b/test/receiverevidencefix/gomod/fork2/go.mod @@ -0,0 +1,5 @@ +module example.com/f + +go 1.22 + +replace github.com/up/lib => ./third/lib diff --git a/test/receiverevidencefix/gomod/fork2/lib/l.go b/test/receiverevidencefix/gomod/fork2/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/fork2/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/fork2/third/lib/l.go b/test/receiverevidencefix/gomod/fork2/third/lib/l.go new file mode 100644 index 000000000..2e68b9c51 --- /dev/null +++ b/test/receiverevidencefix/gomod/fork2/third/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 3 } diff --git a/test/receiverevidencefix/gomod/mixed/lib/l.go b/test/receiverevidencefix/gomod/mixed/lib/l.go new file mode 100644 index 000000000..ac5d0cb23 --- /dev/null +++ b/test/receiverevidencefix/gomod/mixed/lib/l.go @@ -0,0 +1,4 @@ +package lib + +// no go.mod at or above this file: its import path is unknown, and an import path an in-tree module carries never names it +func Mark(n int) int { return n + 7 } diff --git a/test/receiverevidencefix/gomod/mixed/m/app/app.go b/test/receiverevidencefix/gomod/mixed/m/app/app.go new file mode 100644 index 000000000..fa4de2940 --- /dev/null +++ b/test/receiverevidencefix/gomod/mixed/m/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/x/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/mixed/m/go.mod b/test/receiverevidencefix/gomod/mixed/m/go.mod new file mode 100644 index 000000000..ff963b68b --- /dev/null +++ b/test/receiverevidencefix/gomod/mixed/m/go.mod @@ -0,0 +1,3 @@ +module example.com/x + +go 1.22 diff --git a/test/receiverevidencefix/gomod/mixed/m/lib/l.go b/test/receiverevidencefix/gomod/mixed/m/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/mixed/m/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/nest/app/app.go b/test/receiverevidencefix/gomod/nest/app/app.go new file mode 100644 index 000000000..5ed7b8586 --- /dev/null +++ b/test/receiverevidencefix/gomod/nest/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/other/util" + +func Use(n int) int { return util.Mark(n) } diff --git a/test/receiverevidencefix/gomod/nest/go.mod b/test/receiverevidencefix/gomod/nest/go.mod new file mode 100644 index 000000000..1e9be762e --- /dev/null +++ b/test/receiverevidencefix/gomod/nest/go.mod @@ -0,0 +1,3 @@ +module example.com/root + +go 1.22 diff --git a/test/receiverevidencefix/gomod/nest/other/go.mod b/test/receiverevidencefix/gomod/nest/other/go.mod new file mode 100644 index 000000000..8fce098ba --- /dev/null +++ b/test/receiverevidencefix/gomod/nest/other/go.mod @@ -0,0 +1,3 @@ +module example.com/other + +go 1.22 diff --git a/test/receiverevidencefix/gomod/nest/other/util/u.go b/test/receiverevidencefix/gomod/nest/other/util/u.go new file mode 100644 index 000000000..8c3fd2f78 --- /dev/null +++ b/test/receiverevidencefix/gomod/nest/other/util/u.go @@ -0,0 +1,3 @@ +package util + +func Mark(n int) int { return n + 2 } diff --git a/test/receiverevidencefix/gomod/nest/util/u.go b/test/receiverevidencefix/gomod/nest/util/u.go new file mode 100644 index 000000000..1b26ae9ba --- /dev/null +++ b/test/receiverevidencefix/gomod/nest/util/u.go @@ -0,0 +1,3 @@ +package util + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/pfx/ab/go.mod b/test/receiverevidencefix/gomod/pfx/ab/go.mod new file mode 100644 index 000000000..d6b4e7647 --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/ab/go.mod @@ -0,0 +1,3 @@ +module example.com/ab + +go 1.22 diff --git a/test/receiverevidencefix/gomod/pfx/ab/lib/l.go b/test/receiverevidencefix/gomod/pfx/ab/lib/l.go new file mode 100644 index 000000000..d42bdd639 --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/ab/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 2 } diff --git a/test/receiverevidencefix/gomod/pfx/app/app.go b/test/receiverevidencefix/gomod/pfx/app/app.go new file mode 100644 index 000000000..3f31895d7 --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/ab/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/pfx/app/near.go b/test/receiverevidencefix/gomod/pfx/app/near.go new file mode 100644 index 000000000..d12914ac1 --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/app/near.go @@ -0,0 +1,5 @@ +package app + +import "example.com/a/lib" + +func Near(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/pfx/go.mod b/test/receiverevidencefix/gomod/pfx/go.mod new file mode 100644 index 000000000..df2941f19 --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/go.mod @@ -0,0 +1,3 @@ +module example.com/a + +go 1.22 diff --git a/test/receiverevidencefix/gomod/pfx/lib/l.go b/test/receiverevidencefix/gomod/pfx/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/pfx/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/rep/app/app.go b/test/receiverevidencefix/gomod/rep/app/app.go new file mode 100644 index 000000000..bf8143e71 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/legacy/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/rep/app/near.go b/test/receiverevidencefix/gomod/rep/app/near.go new file mode 100644 index 000000000..60a91086f --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/app/near.go @@ -0,0 +1,5 @@ +package app + +import "example.com/r/lib" + +func Near(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/rep/go.mod b/test/receiverevidencefix/gomod/rep/go.mod new file mode 100644 index 000000000..0bb14a6b8 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/go.mod @@ -0,0 +1,5 @@ +module example.com/r + +go 1.22 + +replace example.com/legacy => ./legacy diff --git a/test/receiverevidencefix/gomod/rep/legacy/go.mod b/test/receiverevidencefix/gomod/rep/legacy/go.mod new file mode 100644 index 000000000..85e10f5a8 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/legacy/go.mod @@ -0,0 +1,3 @@ +module example.com/legacy + +go 1.22 diff --git a/test/receiverevidencefix/gomod/rep/legacy/lib/l.go b/test/receiverevidencefix/gomod/rep/legacy/lib/l.go new file mode 100644 index 000000000..d42bdd639 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/legacy/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 2 } diff --git a/test/receiverevidencefix/gomod/rep/lib/l.go b/test/receiverevidencefix/gomod/rep/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/rep/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/rep2/app/app.go b/test/receiverevidencefix/gomod/rep2/app/app.go new file mode 100644 index 000000000..bf8143e71 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep2/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/legacy/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/rep2/app/near.go b/test/receiverevidencefix/gomod/rep2/app/near.go new file mode 100644 index 000000000..60a91086f --- /dev/null +++ b/test/receiverevidencefix/gomod/rep2/app/near.go @@ -0,0 +1,5 @@ +package app + +import "example.com/r/lib" + +func Near(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/rep2/go.mod b/test/receiverevidencefix/gomod/rep2/go.mod new file mode 100644 index 000000000..0bb14a6b8 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep2/go.mod @@ -0,0 +1,5 @@ +module example.com/r + +go 1.22 + +replace example.com/legacy => ./legacy diff --git a/test/receiverevidencefix/gomod/rep2/legacy/lib/l.go b/test/receiverevidencefix/gomod/rep2/legacy/lib/l.go new file mode 100644 index 000000000..d42bdd639 --- /dev/null +++ b/test/receiverevidencefix/gomod/rep2/legacy/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 2 } diff --git a/test/receiverevidencefix/gomod/rep2/lib/l.go b/test/receiverevidencefix/gomod/rep2/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/rep2/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/tail/app/app.go b/test/receiverevidencefix/gomod/tail/app/app.go new file mode 100644 index 000000000..2180cd014 --- /dev/null +++ b/test/receiverevidencefix/gomod/tail/app/app.go @@ -0,0 +1,5 @@ +package app + +import "example.com/t/pkg/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/tail/app/near.go b/test/receiverevidencefix/gomod/tail/app/near.go new file mode 100644 index 000000000..38ad013e5 --- /dev/null +++ b/test/receiverevidencefix/gomod/tail/app/near.go @@ -0,0 +1,5 @@ +package app + +import "example.com/t/lib" + +func Near(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/tail/go.mod b/test/receiverevidencefix/gomod/tail/go.mod new file mode 100644 index 000000000..3d7deb44d --- /dev/null +++ b/test/receiverevidencefix/gomod/tail/go.mod @@ -0,0 +1,3 @@ +module example.com/t + +go 1.22 diff --git a/test/receiverevidencefix/gomod/tail/lib/l.go b/test/receiverevidencefix/gomod/tail/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/tail/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/tail/pkg/lib/l.go b/test/receiverevidencefix/gomod/tail/pkg/lib/l.go new file mode 100644 index 000000000..2e68b9c51 --- /dev/null +++ b/test/receiverevidencefix/gomod/tail/pkg/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 3 } diff --git a/test/receiverevidencefix/gomod/vend/app/app.go b/test/receiverevidencefix/gomod/vend/app/app.go new file mode 100644 index 000000000..5487ce1a0 --- /dev/null +++ b/test/receiverevidencefix/gomod/vend/app/app.go @@ -0,0 +1,5 @@ +package app + +import "github.com/x/lib" + +func Use(n int) int { return lib.Mark(n) } diff --git a/test/receiverevidencefix/gomod/vend/go.mod b/test/receiverevidencefix/gomod/vend/go.mod new file mode 100644 index 000000000..10ef5515f --- /dev/null +++ b/test/receiverevidencefix/gomod/vend/go.mod @@ -0,0 +1,5 @@ +module example.com/v + +go 1.22 + +require github.com/x/lib v1.0.0 diff --git a/test/receiverevidencefix/gomod/vend/lib/l.go b/test/receiverevidencefix/gomod/vend/lib/l.go new file mode 100644 index 000000000..d0bcfe79f --- /dev/null +++ b/test/receiverevidencefix/gomod/vend/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 1 } diff --git a/test/receiverevidencefix/gomod/vend/vendor/github.com/x/lib/l.go b/test/receiverevidencefix/gomod/vend/vendor/github.com/x/lib/l.go new file mode 100644 index 000000000..cac212b3f --- /dev/null +++ b/test/receiverevidencefix/gomod/vend/vendor/github.com/x/lib/l.go @@ -0,0 +1,3 @@ +package lib + +func Mark(n int) int { return n + 5 } diff --git a/test/receiverevidencefix/gomod/vend/vendor/modules.txt b/test/receiverevidencefix/gomod/vend/vendor/modules.txt new file mode 100644 index 000000000..23777cf9f --- /dev/null +++ b/test/receiverevidencefix/gomod/vend/vendor/modules.txt @@ -0,0 +1,3 @@ +# github.com/x/lib v1.0.0 +## explicit +github.com/x/lib From 8e5b8fb665a9a8c35d2ef9ab6a18ee18f0371669 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 20:38:52 -0400 Subject: [PATCH 140/229] refactor: FE-B's via column, base walk and scope walk go through shared helpers Three quality acks the review rejected are removed and the code fixed instead: - columnar.h: the dense column joins the existing optional-column helpers (columnarViaPresent, emitColumnarViaColumn, a three-flag columnarOptionalFields), so emitColumnarSymbolRows is back to its pre-lane shape. - receiverevidence.h: methodOf and fieldOf share walkBaseLevels (the level-by-level base walk); typedLocal, aliasedMethod, isLocalName and callerClass share walkScopes (the enclosing-function walk). - serialize.h: emitCalleeCallsBlock calls a CSR-level graph.h edgeNameOnly (the Graph overload forwards to it) instead of re-implementing the lookup. Output is byte-identical. Co-Authored-By: Claude Opus 5.5 --- .ripwire_quality_acks | 8 --- src/columnar.h | 51 ++++++++------ src/graph.h | 22 +++--- src/receiverevidence.h | 150 ++++++++++++++++++++++------------------- src/serialize.h | 14 +--- 5 files changed, 126 insertions(+), 119 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 8ee5bc57e..4f249ea22 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -96,7 +96,6 @@ ack api-surface 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: ack api-surface 77eb2194156050d0 4 cid=ae974822986c10b4 lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. ack api-surface 78d2e234da12136c 9 cid=f882c119f01e545e F4: --recall's --token-budget ceiling travels to the header through RecallShape, so buildRecall/recallFor each take one more argument (8->9). Both are defaulted; both call sites updated; the alternative was a second estimate of the same number beside the one the root prints. ack api-surface 79b6230346ec0208 5 cid=f936b7f7eca7e9bb P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). -ack api-surface 7b4ba1b688c902ab 7 cid=12665f9a238a04cc by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 7c0e20a9156f7379 10 cid=c070b25dc349431d capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface 7c0e356e60b323ba 3 cid=4451e903bc990da2 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate @@ -456,7 +455,6 @@ ack complexity 767211b0b7743f79 23 cid=f8439fcff51f596b capture-audit 2026-09-04 ack complexity 779387a679944627 25 cid=c93357a7d8868988 Kotlin port (PR #126): function_value_parameters counts parameter children only. A Kotlin parameter's vararg modifier and its default-value expression are SIBLINGS of the parameter node in this grammar, so the generic every-named-child rule reads 3 real params as 5 (kotlincheck §10 pins params=3); the Kotlin arm is one guarded branch inside the existing loop. ack complexity 7a04eee0ff6ec2d7 28 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack complexity 7a74ae49acb306cf 78 cid=42744dca5ad71517 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. -ack complexity 7b4ba1b688c902ab 25 cid=d53ccc7632e3889a now=25 was=13 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 7c0e356e60b323ba 19 cid=02317a43a1806043 at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack complexity 7e26e5533d486c7a 39 cid=5220b8e1fc725dbe round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: F5: fromTraceBundleText grows by the named-vs-default ceiling-notes pair, the smallest-named-ceiling computation and the reasoning comments. The rung-sniff it replaces was one line and wrong; the property is the fix. ack complexity 7ed8ad2c213537a4 100 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). @@ -501,7 +499,6 @@ ack complexity d03215f48bec3886 17 cid=d622ffa6d33014f4 E1 (terminality round A, ack complexity d295ac7080c14dd5 18 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity d42c85b67bd0956f 52 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity d63db6944aa504a7 536 cid=70fc957435aa6310 lane for-widen (L-W): the file page, coverage= and the thin next= land in the three --for dispatch/emit bodies the 0.6.1 lanes touched this week — this lane's own churn, not thrash | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack complexity d721ea3d108a3ba8 24 cid=9bfd37570d23d571 now=24 was=15 p=src/serialize.h:5803 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity dbe6ed5269d328a4 130 cid=9c4a7dc830d23192 M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack complexity dd02b378ae6b5b75 30 cid=5a0958e5bca4aaf3 L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack complexity dd627540f10bba76 197 cid=4170efd4f9c90d2f by=src/* t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign | prior: round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes @@ -561,7 +558,6 @@ ack duplication 177c6a61c6c2c3a2 28 the three sidecar read seams keep one O_NOFO ack duplication 189c716a1f2298cc 42 now=42 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication 197e1f32ffe97006 42 by=src/* now=42 was=0 prov=recon slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 199088adf212a996 95 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. -ack duplication 199643b5605561aa 274 now=274 was=0 p=src/receiverevidence.h:491 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 19b0ff8a7ad07b3c 63 now=63 was=0 prov=recon F4: withHeaderField and withHeaderAttrAfter now share headerFieldDigits; the 63 tokens left are the four-line frame both need (copy the line, locate, edit, return). Folding those into one function with a mode flag would be worse than the clone. ack duplication 1a6cabebe02fa29f 44 now=44 was=0 prov=recon dynmapsimd harness oracles deliberately restate the scalar contract (independent test oracle); dynamic_map ctor churn is this feature's own edit window ack duplication 1b10462b15da9a73 64 now=64 was=0 p=src/infra/shquote.h:19 the shSingleQuote|show Type-3 pair (64 tokens, test/jsonwalk_unit.cpp's failure-message renderer) is the SAME clone the base tree already had at 0.87 with jsonesc.h::shSingleQuote; the body moved verbatim to the standard-library-only shquote.h so os.h can include it without a cross-layer forward declaration, which re-keys the group by path. Nothing new is duplicated; show() escapes a different character set for a test message and folding it into the shell quoter would name neither @@ -661,7 +657,6 @@ ack duplication 8a652c25d25d8e47 37 by=src/* now=37 was=0 prov=recon member-vari ack duplication 8b236ff8e22bac99 43 now=43 was=0 prov=recon dynmapsimd harness oracles deliberately restate the scalar contract (independent test oracle); dynamic_map ctor churn is this feature's own edit window ack duplication 8c1252540ce9a74e 56 now=56 was=0 prov=recon OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack duplication 8c49388791b303b8 56 now=56 was=0 prov=recon DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b) -ack duplication 8cbe787ba8c6fce6 95 now=95 was=0 p=src/receiverevidence.h:363 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked ack duplication 8fff3a3ea6d42578 75 by=src/verbs_report.h now=75 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/verbs_report.h): extentSuspectCountsByFile and recall.h docFileMask are the one-pass-over-ing.symbols per-fileId tally idiom (lintrules.h dependencyCapableMask is a third), with different element types and questions: a uint32 count of flagged definitions per file vs a char mask of files holding Markdown symbols. Same class as ack 689e8ff37b877fd0: a filter-and-tally template parameterised by a lambda would read worse than either five-line loop. ack duplication 91876bb9e456fd42 30 now=30 was=0 prov=recon wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD ack duplication 9497791b092d0735 19 now=19 was=0 prov=recon M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. @@ -889,7 +884,6 @@ ack params 5ec38fbd414fa4d4 6 cid=35883c203ba33e28 lane/tc-sliceat MCP half: dis ack params 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 6e58b0a307757079 27 cid=cd68c661a9c12758 by=src/* now=27 was=26 p=src/serialize.h:8704 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. -ack params 7b4ba1b688c902ab 9 cid=d53ccc7632e3889a now=9 was=8 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate ack params 7c8bcef86c83b1aa 10 cid=280a01aa1f0483f0 now=10 was=9 p=src/verbs_for.h:1867 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack params 80a2f3a7c92fbed4 6 cid=b272c333d7d8f297 Narrower's inline members are measured under its constructor; the growth is the reviewed using-declaration re-export and Rule 2c member-field veto rules (lane/using-base-reexport, #280) @@ -1633,7 +1627,6 @@ ack verbosity 767211b0b7743f79 86 cid=f8439fcff51f596b capture-audit 2026-09-04 ack verbosity 775b773b1a3d2349 780 cid=6b4320dfd43b49dd the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack verbosity 7a04eee0ff6ec2d7 87 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack verbosity 7ab4bd7047f034ec 576 cid=df13b023c4001487 now=576 was=0 p=src/receiverevidence.h:114 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows -ack verbosity 7b4ba1b688c902ab 75 cid=d53ccc7632e3889a now=75 was=62 p=src/columnar.h:192 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity 7c0e356e60b323ba 70 cid=4451e903bc990da2 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack verbosity 7e26e5533d486c7a 265 cid=aa596d9bb05d621d cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. | prior: round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes ack verbosity 7ed8ad2c213537a4 375 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). @@ -1684,7 +1677,6 @@ ack verbosity cb7342964b38db9c 112 cid=3af54969638510bc by=src/* member-variable ack verbosity d42c85b67bd0956f 214 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack verbosity d44595768a7cf3af 106 cid=ad934f87aaaed8df preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack verbosity d63db6944aa504a7 1388 cid=1943300f4961480a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through -ack verbosity d721ea3d108a3ba8 73 cid=9bfd37570d23d571 now=73 was=56 p=src/serialize.h:5803 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack verbosity dbe6ed5269d328a4 325 cid=f7116f48272d6777 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack verbosity dd02b378ae6b5b75 126 cid=9ec35c817a609c0f capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost ack verbosity dd627540f10bba76 643 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck diff --git a/src/columnar.h b/src/columnar.h index 6ebd5dd60..7dc3b7527 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -160,14 +160,34 @@ inline void emitColumnarDepthColumn( std::FILE* out, const std::vector& rw::emitTo( out, "{}", column ); } +// FE-B: the optional dense `` column — 1 where the row's edge is name-only (via="name"), one value per row in row +// order. Present only when some row carries it (columnarViaPresent), so an answer with no hedged row pays 0 bytes. +inline bool columnarViaPresent( const std::vector* viaName ) noexcept +{ + return viaName != nullptr && std::any_of( viaName->begin(), viaName->end(), []( char c ) { return c != 0; } ); +} +inline void emitColumnarViaColumn( std::FILE* out, std::size_t rowCount, const std::vector& viaName ) +{ + std::fputs( "", out ); + for( std::size_t i = 0; i < rowCount; ++i ) + { + std::fputs( i ? "," : "", out ); + std::fputc( i < viaName.size() && viaName[ i ] != 0 ? '1' : '0', out ); + } + std::fputs( "", out ); +} + // The optional columns a caller asked for, and the fields= suffix naming them, in ONE place: emitColumnarSymbolRows // reads each through these, so its own branch count does not grow per optional column. -inline std::string_view columnarOptionalFields( bool hasTested, bool hasDepth ) noexcept +inline std::string_view columnarOptionalFields( bool hasTested, bool hasDepth, bool hasVia ) noexcept { - return hasTested ? ( hasDepth ? ",tested,depth" : ",tested" ) : ( hasDepth ? ",depth" : "" ); + static constexpr std::string_view kFields[ 8 ] = { "", ",via", ",depth", ",depth,via", ",tested", ",tested,via", ",tested,depth", + ",tested,depth,via" }; + return kFields[ ( hasTested ? 4 : 0 ) | ( hasDepth ? 2 : 0 ) | ( hasVia ? 1 : 0 ) ]; } inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing, const std::vector& rows, - const std::vector* testReach, const std::vector* depth ) + const std::vector* testReach, const std::vector* depth, + const std::vector* viaName ) { if( testReach ) { @@ -177,6 +197,10 @@ inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing { emitColumnarDepthColumn( out, rows, *depth ); } + if( columnarViaPresent( viaName ) ) + { + emitColumnarViaColumn( out, rows.size(), *viaName ); + } } // COLUMNAR form of a symbol-row verb (--callers/--callees/--impact/--pr-context symbols). `wrapperTag` is the @@ -189,6 +213,7 @@ inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing // column itself is present only when a caller passed the lens, so a caller with no test data pays 0 bytes). // 0.6.5: `depth` (optional, --impact only) adds a dense `` column — transitiveCallersDepth's hop per row — and // names it in fields=; a caller that passes none pays 0 bytes, like the tested column. +// FE-B: `viaName` (optional, one flag per row) adds the dense `` column (emitColumnarViaColumn) when a row is hedged. inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, const char* wrapperTag, const std::string& wrapperAttrs, const std::vector& rows, std::string_view rootPrefix = {}, @@ -197,8 +222,6 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, const std::vector* viaName = nullptr ) { std::vector esc; - // FE-B: a dense `` column (1 = the row's edge is name-only, via="name"), present only when some row carries it - const bool hasVia = viaName != nullptr && std::any_of( viaName->begin(), viaName->end(), []( char c ) { return c != 0; } ); std::vector rowFiles; rowFiles.reserve( rows.size() ); for( NodeId id : rows ) @@ -211,8 +234,8 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( kColumnarLegend, out ); // §B1.5: once per output, before the element it describes rw::emitTo( out, "<{} {} format=\"columnar\">", wrapperTag, wrapperAttrs.c_str() ); emitPathTable( out, ing, uniqueFiles, esc, rootPrefix ); - rw::emitTo( out, "", rows.size(), columnarOptionalFields( testReach != nullptr, depth != nullptr ), - hasVia ? ",via" : "" ); + rw::emitTo( out, "", rows.size(), + columnarOptionalFields( testReach != nullptr, depth != nullptr, columnarViaPresent( viaName ) ) ); // path index array std::fputs( "", out ); @@ -260,18 +283,8 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( symTag( ing.symbols[rows[i]].kind ), out ); } std::fputs( "", out ); - // A6 / 0.6.5: each present only when the caller passed it — see the wrapper banner and emitColumnarOptionalColumns. - emitColumnarOptionalColumns( out, ing, rows, testReach, depth ); - if( hasVia ) - { - std::fputs( "", out ); - for( std::size_t i = 0; i < rows.size(); ++i ) - { - std::fputs( i ? "," : "", out ); - std::fputc( i < viaName->size() && ( *viaName )[ i ] != 0 ? '1' : '0', out ); - } - std::fputs( "", out ); - } + // A6 / 0.6.5 / FE-B: each present only when the caller passed it — see the wrapper banner and emitColumnarOptionalColumns. + emitColumnarOptionalColumns( out, ing, rows, testReach, depth, viaName ); rw::emitTo( out, "", wrapperTag ); } diff --git a/src/graph.h b/src/graph.h index bf0cba3a9..1391b2b68 100644 --- a/src/graph.h +++ b/src/graph.h @@ -177,19 +177,25 @@ struct Graph // emits nothing — the map is byte-identical either way. }; -// FE-B: is the call edge from → to NAME-ONLY (Graph::outNameOnly) — bound by name alone at every site that bound it, so every -// surface renders it via="name". false for an edge that does not exist and on a graph with no name-only edge at all. -inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept +// FE-B: is the call edge from → to NAME-ONLY, over the raw out-CSR and its parallel hedge bits — bound by name alone at +// every site that bound it, so every surface renders it via="name". false for an edge that does not exist and for empty +// hedge bits (a graph with no name-only edge at all). The Graph overload below and serialize.h's block share it. +inline bool edgeNameOnly( const std::vector& outOff, const std::vector& outTargets, + const std::vector& nameOnly, NodeId from, NodeId to ) noexcept { - if( g.outNameOnly.empty() || std::size_t( from ) + 1 >= g.outOff.size() ) + if( nameOnly.empty() || std::size_t( from ) + 1 >= outOff.size() ) { return false; } - EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "the hedge bit is parallel to outTargets (buildGraph allocates it per edge)" ); - const auto b = g.outTargets.begin() + g.outOff[ from ]; - const auto e = g.outTargets.begin() + g.outOff[ std::size_t( from ) + 1 ]; + EXPECTS( nameOnly.size() == outTargets.size(), "the hedge bit is parallel to outTargets (buildGraph allocates it per edge)" ); + const auto b = outTargets.begin() + outOff[ from ]; + const auto e = outTargets.begin() + outOff[ std::size_t( from ) + 1 ]; const auto it = std::lower_bound( b, e, to ); // ascending within a source (buildGraph sorts by (from, to)) - return it != e && *it == to && g.outNameOnly[ std::size_t( it - g.outTargets.begin() ) ] != 0; + return it != e && *it == to && nameOnly[ std::size_t( it - outTargets.begin() ) ] != 0; +} +inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept +{ + return edgeNameOnly( g.outOff, g.outTargets, g.outNameOnly, from, to ); } // FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 0f383c368..21017097e 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -377,51 +377,64 @@ struct ReceiverEvidence // ── queries ─────────────────────────────────────────────────────────────────────────────────────────────────────── // the innermost function/method of the same file whose span strictly holds `id`; kNoNode when none NodeId enclosingFn( NodeId id ) const { return innermostEnclosingFn( ing, fnsByFile, id ); } - // the class a typed local names in `from` or a function enclosing it; nullptr when untyped (or tombstoned) - const std::string* typedLocal( NodeId from, std::string_view var ) const + // visit `from`, then each function enclosing it, innermost first (at most 8 scopes): `visit( scope )` returns true + // to stop there. Returns the scope it stopped at, kNoNode when no visit stopped the walk. + template + NodeId walkScopes( NodeId from, Visit&& visit ) const { NodeId cur = from; for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) { - if( const auto it = localType.find( keyOf( cur, '#', var ) ); it != localType.end() ) + if( visit( cur ) ) { - return it->second.empty() ? nullptr : &it->second; + return cur; } cur = enclosingFn( cur ); } - return nullptr; + return kNoNode; } - const std::pair* aliasedMethod( NodeId from, std::string_view var, NodeId& scope ) const + // the class a typed local names in `from` or a function enclosing it; nullptr when untyped (or tombstoned) + const std::string* typedLocal( NodeId from, std::string_view var ) const { - NodeId cur = from; - for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + const std::string* hit = nullptr; + walkScopes( from, [ & ]( NodeId scope ) { - if( const auto it = methodAlias.find( keyOf( cur, '#', var ) ); it != methodAlias.end() ) + const auto it = localType.find( keyOf( scope, '#', var ) ); + if( it == localType.end() ) { - scope = cur; - return it->second.first.empty() ? nullptr : &it->second; + return false; } - cur = enclosingFn( cur ); + hit = it->second.empty() ? nullptr : &it->second; + return true; + } ); + return hit; + } + // the ( object var, method ) a method alias `var` names in `from` or a function enclosing it; `scope` = where it is bound + const std::pair* aliasedMethod( NodeId from, std::string_view var, NodeId& scope ) const + { + const std::pair* hit = nullptr; + if( const NodeId at = walkScopes( from, [ & ]( NodeId s ) + { + const auto it = methodAlias.find( keyOf( s, '#', var ) ); + if( it == methodAlias.end() ) + { + return false; + } + hit = it->second.first.empty() ? nullptr : &it->second; + return true; + } ); at != kNoNode ) + { + scope = at; } - return nullptr; + return hit; } // is `name` a parameter or local of `from` or of a function enclosing it (any binding the extractor recorded) bool isLocalName( NodeId from, std::string_view name ) const { - if( localNames == nullptr ) - { - return false; - } - NodeId cur = from; - for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) + return localNames != nullptr && walkScopes( from, [ & ]( NodeId s ) { - if( localNames->contains( keyOf( cur, '#', name ) ) || localType.contains( keyOf( cur, '#', name ) ) ) - { - return true; - } - cur = enclosingFn( cur ); - } - return false; + return localNames->contains( keyOf( s, '#', name ) ) || localType.contains( keyOf( s, '#', name ) ); + } ) != kNoNode; } // a receiver root that names a CLASS at this site: a class-name receiver language, a class of that name, and no local // of the caller hiding it (`Interval = make(); Interval.validate( v )`) @@ -432,16 +445,8 @@ struct ReceiverEvidence // the class owning the caller — the caller's own owner, else the nearest enclosing function's (a closure in a method) std::string_view callerClass( NodeId from ) const { - NodeId cur = from; - for( int depth = 0; depth < 8 && cur != kNoNode; ++depth ) - { - if( !ownerClass[ cur ].empty() ) - { - return ownerClass[ cur ]; - } - cur = enclosingFn( cur ); - } - return {}; + const NodeId at = walkScopes( from, [ & ]( NodeId s ) { return !ownerClass[ s ].empty(); } ); + return at == kNoNode ? std::string_view{} : std::string_view( ownerClass[ at ] ); } // `type`'s direct bases: the inheritance names, then (Go) its embedded classes template @@ -478,33 +483,18 @@ struct ReceiverEvidence } return false; } - // `type`'s own `name`, else the shallowest base level defining it (several bases at one level: their union). The - // answer is in `found`; false when nothing in the cone defines it. superOnly skips `type` itself (`super.m()`). - bool methodOf( std::string_view type, std::string_view name, bool superOnly ) const + // visit `type`'s level (depth 0), then its base levels shallowest first, each class once: `visit( level, depth )` + // returns true to stop. Returns whether a visit stopped the walk. + template + bool walkBaseLevels( std::string_view type, Visit&& visit ) const { - EXPECTS( active, "methodOf reads the tables build() fills" ); - found.clear(); - if( type.empty() ) - { - return false; - } std::vector level{ type }; std::vector seen{ type }; for( std::size_t depth = 0; depth < kWalkCap && !level.empty(); ++depth ) { - if( !( superOnly && depth == 0 ) ) + if( visit( level, depth ) ) { - for( std::string_view t : level ) - { - if( const auto it = methodsByType.find( typeKey( t, "::", name ) ); it != methodsByType.end() ) - { - for( NodeId c : it->second ) { found.push_back( c ); } - } - } - if( !found.empty() ) - { - return true; - } + return true; } std::vector next; for( std::string_view t : level ) @@ -518,31 +508,49 @@ struct ReceiverEvidence } return false; } - // the class of field `field` on `type` (or the shallowest base declaring it); "" unknown - std::string_view fieldOf( std::string_view type, std::string_view field ) const + // `type`'s own `name`, else the shallowest base level defining it (several bases at one level: their union). The + // answer is in `found`; false when nothing in the cone defines it. superOnly skips `type` itself (`super.m()`). + bool methodOf( std::string_view type, std::string_view name, bool superOnly ) const { - std::vector level{ type }; - std::vector seen{ type }; - for( std::size_t depth = 0; depth < kWalkCap && !level.empty(); ++depth ) + EXPECTS( active, "methodOf reads the tables build() fills" ); + found.clear(); + if( type.empty() ) { + return false; + } + return walkBaseLevels( type, [ & ]( const std::vector& level, std::size_t depth ) + { + if( superOnly && depth == 0 ) + { + return false; + } for( std::string_view t : level ) { - if( const auto it = memberType.find( typeKey( t, "#", field ) ); it != memberType.end() ) + if( const auto it = methodsByType.find( typeKey( t, "::", name ) ); it != methodsByType.end() ) { - return it->second; // "" = tombstone: unknown + for( NodeId c : it->second ) { found.push_back( c ); } } } - std::vector next; + return !found.empty(); + } ); + } + // the class of field `field` on `type` (or the shallowest base declaring it); "" unknown + std::string_view fieldOf( std::string_view type, std::string_view field ) const + { + std::string_view fieldType; + walkBaseLevels( type, [ & ]( const std::vector& level, std::size_t ) + { for( std::string_view t : level ) { - forEachBase( t, [ & ]( std::string_view b ) + if( const auto it = memberType.find( typeKey( t, "#", field ) ); it != memberType.end() ) { - if( std::find( seen.begin(), seen.end(), b ) == seen.end() ) { seen.push_back( b ); next.push_back( b ); } - } ); + fieldType = it->second; // "" = tombstone: unknown + return true; + } } - level.swap( next ); - } - return {}; + return false; + } ); + return fieldType; } // The receiver chain of a member call as ( root, path, constructed class ), in every language's spelling. diff --git a/src/serialize.h b/src/serialize.h index 8e0887861..c29f8b994 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5821,18 +5821,6 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector std::string callsBody; std::vector nameRows; // names-only rendering: collected, merged by name, written after the walk int shown = 0; - // FE-B: the edge id → cid's hedge bit (the walk reorders, so the bit is found by the target, ascending in the row) - const auto viaOf = [ & ]( NodeId cid ) -> bool - { - if( sink.nameOnly == nullptr || sink.nameOnly->empty() ) - { - return false; - } - const auto b = outTargets.begin() + outOff[ id ]; - const auto e = outTargets.begin() + outOff[ id + 1 ]; - const auto it = std::lower_bound( b, e, cid ); - return it != e && *it == cid && ( *sink.nameOnly )[ std::size_t( it - outTargets.begin() ) ] != 0; - }; for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < 16 && used < budgetBytes; ++k ) { const NodeId cid = walk[ k - outOff[id] ]; @@ -5841,7 +5829,7 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector continue; } const Symbol& cs = ing.symbols[cid]; - const bool via = viaOf( cid ); + const bool via = sink.nameOnly != nullptr && edgeNameOnly( outOff, outTargets, *sink.nameOnly, id, cid ); // FE-B: the walk reorders, so by target if( via && sink.viaLegendCharged != nullptr && !*sink.viaLegendCharged ) { used += viaNameLegendComment().size(); // the legend this row pulls in, charged before the row From b2e951f286b50b395ec74b86341918c1c30ba4db Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 20:49:10 -0400 Subject: [PATCH 141/229] refactor: FE-B's columnar flag column, via charge and go.mod reads as helpers emitColumnarFlagColumn writes any dense 0/1 column (tested and via share it); columnarOptionalFields keeps its contract and columnarViaField names the via suffix; calleeRowVia decides a row's hedge and charges its legend; GoModReads reads each go.mod once with its module line and local replaces. Output is byte-identical. Co-Authored-By: Claude Opus 5.5 --- src/columnar.h | 50 +++++++++++++++--------------- src/graph.h | 82 +++++++++++++++++++++++++++---------------------- src/serialize.h | 21 +++++++++---- 3 files changed, 85 insertions(+), 68 deletions(-) diff --git a/src/columnar.h b/src/columnar.h index 7dc3b7527..d71748b8b 100644 --- a/src/columnar.h +++ b/src/columnar.h @@ -124,21 +124,28 @@ inline void emitPathTable( std::FILE* out, const IngestResult& ing, std::fputs( "", out ); } -// A6: the optional dense 0/1 `` column, split out of emitColumnarSymbolRows so that function's own -// branch count does not grow for a column three of its four callers never ask for. -inline void emitColumnarTestedColumn( std::FILE* out, const IngestResult& ing, const std::vector& rows, - const std::vector& testReach ) +// A dense 0/1 column `1,0,..`, one value per row in row order: `flagAt( i )` is row i's flag. +template +inline void emitColumnarFlagColumn( std::FILE* out, std::string_view tag, std::size_t rowCount, FlagAt&& flagAt ) { - std::fputs( "", out ); - for( std::size_t i = 0; i < rows.size(); ++i ) + rw::emitTo( out, "<{}>", tag ); + for( std::size_t i = 0; i < rowCount; ++i ) { if( i ) { std::fputc( ',', out ); } - std::fputc( isTestedByReach( ing, testReach, rows[i] ) ? '1' : '0', out ); + std::fputc( flagAt( i ) ? '1' : '0', out ); } - std::fputs( "", out ); + rw::emitTo( out, "", tag ); +} + +// A6: the optional dense 0/1 `` column, split out of emitColumnarSymbolRows so that function's own +// branch count does not grow for a column three of its four callers never ask for. +inline void emitColumnarTestedColumn( std::FILE* out, const IngestResult& ing, const std::vector& rows, + const std::vector& testReach ) +{ + emitColumnarFlagColumn( out, "tested", rows.size(), [ & ]( std::size_t i ) { return isTestedByReach( ing, testReach, rows[i] ); } ); } // 0.6.5: the optional dense `` column — --impact's hop depth per row (graph.h transitiveCallersDepth), one value @@ -161,29 +168,22 @@ inline void emitColumnarDepthColumn( std::FILE* out, const std::vector& } // FE-B: the optional dense `` column — 1 where the row's edge is name-only (via="name"), one value per row in row -// order. Present only when some row carries it (columnarViaPresent), so an answer with no hedged row pays 0 bytes. +// order. Present only when some row carries it, so an answer with no hedged row pays 0 bytes; columnarViaField is its +// fields= suffix. inline bool columnarViaPresent( const std::vector* viaName ) noexcept { return viaName != nullptr && std::any_of( viaName->begin(), viaName->end(), []( char c ) { return c != 0; } ); } -inline void emitColumnarViaColumn( std::FILE* out, std::size_t rowCount, const std::vector& viaName ) +inline std::string_view columnarViaField( const std::vector* viaName ) noexcept { - std::fputs( "", out ); - for( std::size_t i = 0; i < rowCount; ++i ) - { - std::fputs( i ? "," : "", out ); - std::fputc( i < viaName.size() && viaName[ i ] != 0 ? '1' : '0', out ); - } - std::fputs( "", out ); + return columnarViaPresent( viaName ) ? ",via" : ""; } // The optional columns a caller asked for, and the fields= suffix naming them, in ONE place: emitColumnarSymbolRows // reads each through these, so its own branch count does not grow per optional column. -inline std::string_view columnarOptionalFields( bool hasTested, bool hasDepth, bool hasVia ) noexcept +inline std::string_view columnarOptionalFields( bool hasTested, bool hasDepth ) noexcept { - static constexpr std::string_view kFields[ 8 ] = { "", ",via", ",depth", ",depth,via", ",tested", ",tested,via", ",tested,depth", - ",tested,depth,via" }; - return kFields[ ( hasTested ? 4 : 0 ) | ( hasDepth ? 2 : 0 ) | ( hasVia ? 1 : 0 ) ]; + return hasTested ? ( hasDepth ? ",tested,depth" : ",tested" ) : ( hasDepth ? ",depth" : "" ); } inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing, const std::vector& rows, const std::vector* testReach, const std::vector* depth, @@ -199,7 +199,7 @@ inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing } if( columnarViaPresent( viaName ) ) { - emitColumnarViaColumn( out, rows.size(), *viaName ); + emitColumnarFlagColumn( out, "via", rows.size(), [ & ]( std::size_t i ) { return i < viaName->size() && ( *viaName )[ i ] != 0; } ); } } @@ -213,7 +213,7 @@ inline void emitColumnarOptionalColumns( std::FILE* out, const IngestResult& ing // column itself is present only when a caller passed the lens, so a caller with no test data pays 0 bytes). // 0.6.5: `depth` (optional, --impact only) adds a dense `` column — transitiveCallersDepth's hop per row — and // names it in fields=; a caller that passes none pays 0 bytes, like the tested column. -// FE-B: `viaName` (optional, one flag per row) adds the dense `` column (emitColumnarViaColumn) when a row is hedged. +// FE-B: `viaName` (optional, one flag per row) adds the dense `` column (columnarViaPresent) when a row is hedged. inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, const char* wrapperTag, const std::string& wrapperAttrs, const std::vector& rows, std::string_view rootPrefix = {}, @@ -234,8 +234,8 @@ inline void emitColumnarSymbolRows( std::FILE* out, const IngestResult& ing, std::fputs( kColumnarLegend, out ); // §B1.5: once per output, before the element it describes rw::emitTo( out, "<{} {} format=\"columnar\">", wrapperTag, wrapperAttrs.c_str() ); emitPathTable( out, ing, uniqueFiles, esc, rootPrefix ); - rw::emitTo( out, "", rows.size(), - columnarOptionalFields( testReach != nullptr, depth != nullptr, columnarViaPresent( viaName ) ) ); + rw::emitTo( out, "", rows.size(), columnarOptionalFields( testReach != nullptr, depth != nullptr ), + columnarViaField( viaName ) ); // path index array std::fputs( "", out ); diff --git a/src/graph.h b/src/graph.h index 1391b2b68..5e5069c4f 100644 --- a/src/graph.h +++ b/src/graph.h @@ -195,6 +195,7 @@ inline bool edgeNameOnly( const std::vector& outOff, const std::v } inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept { + EXPECTS( g.outNameOnly.empty() || g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits per edge, or none" ); return edgeNameOnly( g.outOff, g.outTargets, g.outNameOnly, from, to ); } @@ -5258,27 +5259,44 @@ inline std::string goPackageImportPath( std::string_view modulePath, std::string return path; } -// FE-B: record `fileId`'s package import path, its go.mod found in disk directory `disk` — root-relative, the walk's -// `walkRel` cut at `walkCut` (npos: the root itself) — and that go.mod's local replaces as ( root-relative dir, path ) -inline void noteGoPackagePath( const IngestResult& ing, FalseEdgeRules& rules, const HashMap& modLineOfDir, - const std::string& disk, std::uint32_t fileId, std::string_view walkRel, std::size_t walkCut ) +// The go.mod files collectGoModules reads, once per disk directory: each one's tree paths (empty: no go.mod there), its +// own `module` line, and (FE-B) every local replace as ( root-relative replaced directory, module path ). +struct GoModReads { - if( const auto ml = modLineOfDir.find( disk ); ml != modLineOfDir.end() ) + HashMap> treePaths; + HashMap moduleLine; + std::vector> replacedDirs; + + // the tree paths of the go.mod in disk directory `disk` (root-relative `modDir`), read on first use + const std::vector& at( const std::string& disk, std::string_view modDir ) { - const std::string_view modDir = walkCut == std::string_view::npos ? std::string_view{} : walkRel.substr( 0, walkCut ); - rules.goPackages.note( rules.goPackages.nearest, fileId, goPackageImportPath( ml->second, modDir, includerDir( rootRelPath( ing, fileId ) ) ) ); + auto [ it, fresh ] = treePaths.try_emplace( disk, std::vector{} ); + if( fresh ) + { + if( const std::optional text = docparse::detail::readWholeFile( disk + "/go.mod" ) ) + { + it->second = goModuleTreePaths( *text ); + moduleLine.try_emplace( disk, goModulePathOf( *text ) ); + for( auto& [ module, dir ] : goLocalReplaces( *text ) ) + { + if( std::optional under = goJoinRelDir( modDir, dir ) ) + { + replacedDirs.emplace_back( std::move( *under ), std::move( module ) ); + } + } + } + } + return it->second; } -} +}; -// FE-B: a go.mod in root-relative directory `modDir` maps each local replace's directory to its module path -inline void noteGoLocalReplaces( std::string_view text, std::string_view modDir, std::vector>& replacedDirs ) +// FE-B: record `fileId`'s package import path, its go.mod found in disk directory `disk` (root-relative `modDir`) +inline void noteGoPackagePath( const IngestResult& ing, FalseEdgeRules& rules, const GoModReads& reads, const std::string& disk, + std::uint32_t fileId, std::string_view modDir ) { - for( auto& [ module, dir ] : goLocalReplaces( text ) ) + if( const auto ml = reads.moduleLine.find( disk ); ml != reads.moduleLine.end() ) { - if( std::optional at = goJoinRelDir( modDir, dir ) ) - { - replacedDirs.emplace_back( std::move( *at ), std::move( module ) ); - } + rules.goPackages.note( rules.goPackages.nearest, fileId, goPackageImportPath( ml->second, modDir, includerDir( rootRelPath( ing, fileId ) ) ) ); } } @@ -5316,11 +5334,9 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) rules.goUnderModule.assign( ing.files.size(), 0 ); rules.goPackages.nearest.assign( ing.files.size(), std::string{} ); rules.goPackages.replaced.assign( ing.files.size(), std::string{} ); - HashMap> modOfDir; // disk directory → its go.mod's tree paths (empty: no go.mod) - HashMap modLineOfDir; // disk directory → its go.mod's own `module` path - HashMap seenModule; - std::vector isGoFile( ing.files.size(), 0 ); - std::vector> replacedDirs; // ( root-relative replaced directory, module path ) + GoModReads reads; + HashMap seenModule; + std::vector isGoFile( ing.files.size(), 0 ); for( const Symbol& s : ing.symbols ) { if( s.lang != Lang::Go || s.fileId >= ing.files.size() || rules.goUnderModule[ s.fileId ] != 0 ) @@ -5340,21 +5356,13 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) break; } disk.resize( diskCut ); - auto [ it, fresh ] = modOfDir.try_emplace( disk, std::vector{} ); - if( fresh ) - { - if( const std::optional text = docparse::detail::readWholeFile( disk + "/go.mod" ) ) - { - it->second = goModuleTreePaths( *text ); - modLineOfDir.try_emplace( disk, goModulePathOf( *text ) ); - noteGoLocalReplaces( *text, relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ), replacedDirs ); - } - } - if( !it->second.empty() ) + const std::string_view modDir = relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ); + const std::vector& paths = reads.at( disk, modDir ); + if( !paths.empty() ) { rules.goUnderModule[ s.fileId ] = 1; - noteGoPackagePath( ing, rules, modLineOfDir, disk, s.fileId, rel, relCut ); - for( const std::string& m : it->second ) + noteGoPackagePath( ing, rules, reads, disk, s.fileId, modDir ); + for( const std::string& m : paths ) { if( seenModule.try_emplace( m, '\0' ).second ) { @@ -5367,12 +5375,12 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) { break; // the root itself was the last directory read } - rel = rel.substr( 0, relCut ); + rel = modDir; } } - std::sort( rules.goModules.begin(), rules.goModules.end() ); // determinism: discovery order follows symbol order - std::sort( replacedDirs.begin(), replacedDirs.end() ); // … and so would a tie between two replaces of one dir - noteGoReplacedPaths( ing, rules, isGoFile, replacedDirs ); + std::sort( rules.goModules.begin(), rules.goModules.end() ); // determinism: discovery order follows symbol order + std::sort( reads.replacedDirs.begin(), reads.replacedDirs.end() ); // ... and so would a tie between two replaces of one dir + noteGoReplacedPaths( ing, rules, isGoFile, reads.replacedDirs ); } // FE-A: the binding facts — every ModuleAlias (Go dot imports apart), every name a JS/TS import or alias binds, and every diff --git a/src/serialize.h b/src/serialize.h index c29f8b994..8d9b7fe07 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5792,6 +5792,20 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector row id → cid name-only (the walk reorders, so the hedge bit is found by the target). The first +// hedged row of an answer charges the via legend it pulls in to `used`, before the row. +inline bool calleeRowVia( const std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, + std::size_t& used, const CalleeCallsSink& sink ) +{ + const bool via = sink.nameOnly != nullptr && edgeNameOnly( outOff, outTargets, *sink.nameOnly, id, cid ); + if( via && sink.viaLegendCharged != nullptr && !*sink.viaLegendCharged ) + { + used += viaNameLegendComment().size(); + *sink.viaLegendCharged = true; + } + return via; +} + // §P10.1: the disclosed block // for one body's 1-hop callee signatures — extracted out of packBodies so the disclosure logic doesn't // inflate packBodies' own complexity/LOC. `total` is outOff[id+1]-outOff[id] — outTargets is deduped-per- @@ -5829,12 +5843,7 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector continue; } const Symbol& cs = ing.symbols[cid]; - const bool via = sink.nameOnly != nullptr && edgeNameOnly( outOff, outTargets, *sink.nameOnly, id, cid ); // FE-B: the walk reorders, so by target - if( via && sink.viaLegendCharged != nullptr && !*sink.viaLegendCharged ) - { - used += viaNameLegendComment().size(); // the legend this row pulls in, charged before the row - *sink.viaLegendCharged = true; - } + const bool via = calleeRowVia( outOff, outTargets, id, cid, used, sink ); // FE-B // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) From 1a385800a1d7a8ed42f94e7e7107e509465956be Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 20:52:49 -0400 Subject: [PATCH 142/229] refactor: emitCalleeCallsBlock reads the via attribute once (its length is the charge) Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 8d9b7fe07..c05a1df8a 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5843,12 +5843,12 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector continue; } const Symbol& cs = ing.symbols[cid]; - const bool via = calleeRowVia( outOff, outTargets, id, cid, used, sink ); // FE-B + const std::string_view viaAttr = viaNameAttr( calleeRowVia( outOff, outTargets, id, cid, used, sink ) ); // FE-B: via="name" or "" // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) { - collectCalleeNameRow( nameRows, cs, used, sink, via ); + collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty() ); ++shown; continue; } @@ -5863,10 +5863,10 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector { continue; } - char hb[ 48 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}>", cs.line, viaNameAttr( via ) ); // FE-B: via="name" + char hb[ 48 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}>", cs.line, viaAttr ); callsBody += " Date: Wed, 7 Oct 2026 21:05:22 -0400 Subject: [PATCH 143/229] whereis: say which pages the default compares, and restate the measured totals The full legend and --help said the defs page is compared with "the page listing every hit"; under the 60-row cap the all page does not list every hit. They now say the listing=all page under the same row cap, compared without limit/offset so every page of one answer lists the same rows. printf parity re-pins help_all for exactly those help lines. CHANGELOG totals re-measured with this build on the same 90 rung-0 cells: 631,732 -> 568,600 B with the default (-10.0%; the 8 where-defined lean cells each carry the +136 B listing reading), lossless 611,785 B unchanged, where-defined median 1,560 B; round trip 90/90, gold 502/502. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 22 ++++++++++++---------- src/cli.h | 20 ++++++++++---------- src/crossref.h | 5 +++-- test/printf_parity.manifest | 2 +- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5aefd7c..43a17ec14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,22 +18,23 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ### Changed — leaner answers: `--whereis` lists definitions, two lossless row spellings, the MCP legend once per session Measured on 90 rung-0 answers of a comparison table (eight repos, three in-sample and five held out), base vs this -build: 631,732 → 611,785 B for the lossless changes alone (−3.2%), 567,512 B with the `--whereis` default (−10.2%). +build: 631,732 → 611,785 B for the lossless changes alone (−3.2%), 568,600 B with the `--whereis` default (−10.0%). Every gold item the base answers supplied is still supplied (502 of 502), and decoding the lossless spellings gives the base answer back element for element on all 90. -- **`--whereis=SYM` lists the definitions and counts the references, when that answer is shorter.** Every - `kind="def"` row is listed; the `kind="ref"` rows are counted in one - `` element whose `next=` lists exactly those rows. The - default serves this page only when it is strictly shorter, in bytes, than the whole hit list (compared as written and +- **`--whereis=SYM` lists the definitions and counts the references, when that answer is shorter.** Every `kind="def"` + row is listed; the `kind="ref"` rows are counted in one `` element whose `next=` lists exactly those rows. The default serves this page only when it + is strictly shorter, in bytes, than the `--whereis-listing=all` page under the same row cap (compared as written and in the compact legend); otherwise it lists every hit. On a symbol with few references (one to three on a 60-symbol sample) the count and its legend reading cost more than the rows they replace, and the default never serves a page that lists fewer rows in more bytes (an explicit `--whereis-listing=defs` is served as asked). `--whereis-listing=defs|refs|all` (MCP `listing`) picks the rows; `all` is the whole hit list. An answer with no reference row, or no definition row (the mentions are then the answer), lists every hit. `kind="def"` is the - parser's label: a definition it does not model (a Ruby `define_method`, a `setattr`, a name bound by assignment) - is among the counted references, and the legend says so. Median where-defined answer 10,455 → 1,424 B. `shown=`/`capped=`/`--limit`/`--offset` and `` - window the listed rows; `hits=` counts every row; `complete=` reads the listing. + parser's label: a definition it does not model (a Ruby `define_method`, a `setattr`, a name bound by assignment) is + among the counted references, and the legend says so. Median where-defined answer 10,455 → 1,560 B. + `shown=`/`capped=`/`--limit`/`--offset` and `` window the listed rows; `hits=` counts every row; + `complete=` reads the listing. - **Lossless:** a `--whereis` row on HEAD's commit omits `tip=`/`date=` (they read `at=` and the new root `head_date=`); a `--for` lens row omits `cx=`/`ccx=`/`in=` when 0 (the legend says an absent one is 0; the rows kept by the byte budget are unchanged). Median per class: how-it-works −0.24 KB, orient-for −0.26 KB, @@ -45,8 +46,9 @@ the base answer back element for element on all 90. CLI and the HTTP transport are unchanged. A ten-call session on one repo: 45,463 → 32,622 B (inline) → 29,679 B. - **Manifest.** The `tools/list` manifest grows 46,732 → 46,846 B: whereis declares `listing`. -Gates: `crossrefcheck` (LEAN L1–L10 and listing=all twins), `completecheck`, `legendrefcheck` (J), -`mcptwinclaimscheck` (A-default), `compactlegendcheck` (re-pinned with the measured bytes), `mcpmanifestcheck`. +Gates: `crossrefcheck` (LEAN L1–L14 — L11–L14 the shorter-page default: one ref, 73 refs, the exact tie and one byte +either side, the MCP twin — and listing=all twins), `completecheck`, `legendrefcheck` (J), `mcptwinclaimscheck` +(A-default), `compactlegendcheck` (re-pinned with the measured bytes; a listing=all twin pin), `mcpmanifestcheck`. ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/src/cli.h b/src/cli.h index 611d47dad..4668972af 100644 --- a/src/cli.h +++ b/src/cli.h @@ -2096,17 +2096,17 @@ inline constexpr char kHelpTail[] = " (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR.\n" " LISTING: by default only the kind=\"def\" rows are listed and the kind=\"ref\" rows\n" " counted in one element, when that page is strictly shorter\n" - " than listing every hit; otherwise every hit is listed (see --whereis-listing).\n" + " than the --whereis-listing=all page; otherwise every hit is listed.\n" " --whereis-listing=WHICH with --whereis: which rows to list, defs, refs or all (default: the shorter page).\n" - " The default lists defs when that page is strictly shorter in bytes than all, else\n" - " all (a tie lists all). defs lists every kind=\"def\" row and COUNTS the kind=\"ref\"\n" - " rows in one element whose next= lists exactly them\n" - " (refs). kind=\"def\" is the parser's label: a definition it does not\n" - " model (define_method, setattr, a name bound by assignment) is a counted ref. With no\n" - " ref row, or no def row (the mentions are then the answer), every hit is listed and the\n" - " root carries no listing=. all lists every row, the whole hit list. shown=/capped=\n" - " and --limit/--offset window the LISTED rows; hits= counts every row. Refused\n" - " without --whereis, and on an unknown value.\n" + " The default lists defs when that page is strictly shorter in bytes than all under the\n" + " same row cap, else all (a tie lists all). defs lists every kind=\"def\" row and COUNTS\n" + " the kind=\"ref\" rows in one element whose next= lists exactly\n" + " them (refs). kind=\"def\" is the parser's label: a definition it does not model\n" + " (define_method, setattr, a name bound by assignment) is a counted ref. With no ref\n" + " row, or no def row (the mentions are then the answer), every hit is listed and the\n" + " root carries no listing=. all lists every row, the whole hit list. shown=/capped= and\n" + " --limit/--offset window the LISTED rows; hits= counts every row. Refused without\n" + " --whereis, and on an unknown value.\n" " --flags[=SUBSTR] the dark-content dashboard: what is built but switched OFF in this repo\n" " the dark-content dashboard: what is BUILT but OFF in this repo. Harvests all three gate\n" " patterns — #ifndef/#define header gates, CMake option(), and getenv() reads — and reports\n" diff --git a/src/crossref.h b/src/crossref.h index dac24ad35..0ff2caca2 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -3092,8 +3092,9 @@ inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std: "absent exactly when this page reached the end of the hit list. " // lean-answers lane — the default LISTING and the tip/date hoist, defined where they appear. "LISTING: by default only the kind=\"def\" rows are listed (listing=\"defs\" on the root) when that page is " - "STRICTLY shorter in bytes than the page listing every hit (compared unpaged, as written and in the compact " - "dialect); otherwise the default lists every hit. Under listing=\"defs\" the " + "STRICTLY shorter in bytes than the listing=all page under the same row cap (both compared without limit= or " + "offset=, as written and in the compact dialect, so every page of one answer lists the same rows); " + "otherwise the default lists every hit. Under listing=\"defs\" the " "kind=\"ref\" rows are COUNTED by the trailing refs element: count= is exactly the number of kind=\"ref\" " "rows in the hit list, none of them printed here, and its next= lists exactly those rows (listing=\"refs\", " "the same rows byte for byte that listing=all prints). listing= is absent when every hit is listed: the hit " diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 724e94e0f..05d74770b 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 83f5195cee26479c5d751b6cab18a4d1fc37051b105d20eefc7c70d5603954e9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 07a79ee85c5585fcedcf420c1a1f4c27f9a4852406e1d73b3b3d30cc18ea4d0f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 98ee450abf0f2ccfdabd7b5af648f4a8a1d24f92 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 21:05:22 -0400 Subject: [PATCH 144/229] showcase capture: regenerated with test/showcase_capture.py docs/captures/COMMANDS_showcase_2026-10-07.md is the generator's own output from build/ripwire at 1c08714c (257 commands): it covers --whereis-listing and --mcp-legend and records the current legend dictionary (dictv= 6c4d27498f3d0809 entries=786, read from --legend-dict). docs/COMMANDS.md is regenerated from the binary and this newest capture. Co-Authored-By: Claude Opus 5.5 --- docs/COMMANDS.md | 2145 ++--- docs/captures/COMMANDS_showcase_2026-10-07.md | 7598 +++++++++++++++++ 2 files changed, 8693 insertions(+), 1050 deletions(-) create mode 100644 docs/captures/COMMANDS_showcase_2026-10-07.md diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a359ba2b6..27e6c1f0c 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,7 @@ python3 docs/docs_commands_build.py --bin build/ripwire The flag surface below is read from `ripwire --help`, so it cannot disagree with the shipped binary. `test/docscommandscheck.sh` fails if it ever does — in either direction. -Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-09-14.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-07.md`), trimmed to the first few lines and scrubbed of local paths. It is illustrative, not a golden: run the command yourself for the current shape. @@ -68,21 +68,21 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - - - - - - - - - + + + + - + - + + + -... [10 more line(s); run it to see the whole thing] + + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--token-budget`, `--for`, `--recall`, `--graph-query`, `--pack-signatures`, `--expand`, `--from-trace`, `--run-trace` @@ -104,21 +104,15 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ ``` $ ./build/ripwire . --max-tokens=1500 - - - - - - - - - - + + + + - + -... [1 more line(s); run it to see the whole thing] + ``` **Shaped by:** `--token-budget`, `--recall`, `--detail`, `--pr-context`, `--from-trace`, `--run-trace`, `--limit` @@ -141,7 +135,8 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--recall`, `--handoff`, `--pr-context`, `--from-trace`, `--run-trace` @@ -164,8 +159,8 @@ _The honest half of the contract: a task with no ripwire-shaped evidence ABSTAIN ``` $ ./build/ripwire . --help-task="write a cheerful release announcement" - - + + ``` @@ -186,19 +181,19 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - - - + + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` - + & p, float alpha = 0.85f ) ... [17 more line(s); run it to see the whole thing] ``` @@ -231,14 +226,14 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - - - + + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -263,20 +258,18 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - - - - - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() - -errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth -enum class AstWalk : std::uint8_t -constexpr std::array<LangEntry, 48> kLangTable = -... [18 more line(s); run it to see the whole thing] +struct FileHealth +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for` @@ -298,18 +291,18 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - - - - - + + + + + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -inline ChurnRanking churnRankedGraph( const MainDispatch& d ) - +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -struct RankedGraph +struct RankedGraph ... [19 more line(s); run it to see the whole thing] ``` @@ -331,20 +324,18 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - - - - - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() - -errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth -enum class AstWalk : std::uint8_t -constexpr std::array<LangEntry, 48> kLangTable = -... [18 more line(s); run it to see the whole thing] +struct FileHealth +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--detail` @@ -365,16 +356,16 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - - - - + + + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) - + P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() -inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) - +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f ... [20 more line(s); run it to see the whole thing] ``` @@ -397,16 +388,16 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - - - - -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] - + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - -WHAT this delta is measured AGAINST, and on WHICH tree — the one place --quality-delta decides…struct DeltaBasis +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… ... [21 more line(s); run it to see the whole thing] ``` @@ -426,10 +417,9 @@ _Interface -> implementors view: every existing impl of the named interface; the ``` $ ./build/ripwire . --lego=Vehicle - - - - + + + @@ -457,10 +447,11 @@ _The repo's best-in-class instance to imitate before writing new code (picked by ``` $ ./build/ripwire . --exemplar="format byte sizes for humans" - - + + - + f, A&&... a ) { // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; @@ -470,8 +461,7 @@ $ ./build/ripwire . --exemplar="format byte sizes for humans" // Only a MUTABLE char[N] is rejected. A string literal is const char[N]; every implementation formats // that as a string, and the hazard here is the reusable buffer that was written short. static_assert( ( ... && !( std::is_array_v> - && !std::is_const_v>> ) ), -... [3 more line(s); run it to see the whole thing] +... [4 more line(s); run it to see the whole thing] ``` **Shaped by:** `--compress`, `--metrics`, `--doctor`, `--limit`, `--index-out` @@ -488,9 +478,9 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ ``` $ ./build/ripwire . --recall="quality delta gating exit codes" -ripwire recall — "quality delta gating exit codes" — 107 relevant of 200 document files, best-first — total=107 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2337 est_tokens=5372 +ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 8.863) ━━ [sections: 2 of 21 selected (60 in doc), section-granular; whole doc 180569 B; lines="2426-2437,2438-2445"; dropped_by_budget=19] +━━ README.md (relevance 7.995) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -498,7 +488,7 @@ This is the part to wire into a script. | Code | Meaning | | --- | --- | | 0 | The command completed. | -| 1 | The command refused the request. A refusal names the reason on stderr. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | | 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | | 3 | The output exceeded the token budget that you set. | | 4 | `--test-gate` found an open obligation. | @@ -553,20 +543,25 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1870195-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1588991-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` -$ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2041 est_tokens=951 +$ ./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 -━━ commands.md (relevance 9.817) ━━ [sections: 1 of 151 selected (180 in doc), section-granular; whole doc 703917 B; lines="3641-3645"; dropped_by_budget=150] [truncated: 1865 of 7256 bytes] -### `--field-affinity[=STRUCT]` +━━ commands.md (relevance 10.658) ━━ [sections: 1 of 158 selected (187 in doc), section-granular; whole doc 721575 B; lines="515-540"; dropped_by_budget=157] [truncated: 1854 of 4242 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base -**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? -Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens or narrow the query for 1 more (~2548-byte budget)) +... [17 more line(s); run it to see the whole thing] ``` ### `--tree` @@ -581,20 +576,20 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - - - + + - + - + - - + + - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -616,7 +611,7 @@ A navigation or report verb answers instead of the map, so --html beside one ref _Self-contained HTML force-directed call graph._ ``` -$ ./build/ripwire . --html=/aux/map2.html +$ ./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html (empty) ``` @@ -637,7 +632,7 @@ the flag only sets the initial mode _The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector)._ ``` -$ ./build/ripwire . --html=/aux/map2.html --color-by=community +$ ./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html --color-by=community (empty) ``` @@ -653,21 +648,21 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - - - - - - - - + + + + + + + - - -... [10 more line(s); run it to see the whole thing] + + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-stable` @@ -684,21 +679,19 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable ``` $ ./build/ripwire . --no-stable --top-k=3 - - - - - - - - - + + + + - + - + + + -... [2 more line(s); run it to see the whole thing] + + ``` --- @@ -717,20 +710,20 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - - - - - - + + - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -748,7 +741,8 @@ _Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distan ``` $ ./build/ripwire . --callers=DoesNotExist - + + ``` **Shaped by:** `--no-route`, `--callees`, `--uses`, `--impact`, `--expand`, `--dead-code`, `--edit-check`, `--slice-flow` @@ -769,18 +763,16 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - - - - - + + + + + - - + ``` @@ -802,18 +794,17 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - - - + + + + + - - - - - - - - + + + + + ``` @@ -837,20 +828,20 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - - - - - - - - - - - - - - + + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -874,20 +865,20 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - - - - - - - - - - - - - + + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -909,14 +900,13 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - - - + + + + + + + ``` @@ -932,14 +922,12 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - - - - + + - - + + @@ -961,19 +949,19 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - - - + + + - - - + + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1018,11 +1006,11 @@ _Markdown docs that name SYM in a backtick (doc<->code edges)._ ``` $ ./build/ripwire . --mentions=rankGraphTeleport - - - + + + - + @@ -1044,19 +1032,21 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - - - + + + + + + - - +... [5 more line(s); run it to see the whole thing] ``` **Shaped by:** `--exercises`, `--test-gate`, `--edit-target-file`, `--legend` @@ -1079,9 +1069,8 @@ _Which symbols a TEST FILE exercises — the reverse direction of --affected._ ``` $ ./build/ripwire . --exercises=test/regression.sh - - - ``` @@ -1106,7 +1095,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: f8d431396 +at: 1c08714cf (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1124,19 +1113,19 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - - + + - - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1159,8 +1148,8 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - - + untested blast radius ; exit 4 while either exists. tests_capped=/untested_capped=: 1 = cut. counts_floor=1: every count is a FLOOR, never a total. graph_ambiguous=/graph_unresolved=: resolver gauge. graph_unindexed=N: N files no grammar could read (the map header's unindexed=); their calls raise neither gauge. at=: commit+dirty+shallow. next=: the one pasteable follow-up. impacted=N: symbols that transitively call the change (changed symbols excluded). shown_tests=/shown_untested=: t rows and u rows printed, two independent counts. script_gates_unmodelled=N: test/*.sh runners in the corpus, a path count; not call-graph modelled. script_gates_registered=N: shell gates test/regression.sh registers as suite members. script_gates_mapped=N: registered gates with exact dependency evidence (literal paths or RIPWIRE_TEST_DEPS). script_gates_unresolved_dynamic=N: registered gates with no mappable deps; they may cover the change unlisted. ccx_bar=N: the cognitive-complexity bar a u row's ccx= is read against. untested_modscope=N: owners excluded from untested= (#324, uncallable); still in impacted=. tests=/untested=: tests to run (t total) / impacted symbols no test reaches (u total). --> + ``` @@ -1179,24 +1168,24 @@ every hit comes back with its enclosing symbol literal / regex search + enclosin **Try it** -_Regex search + enclosing symbol._ +_Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root._ ``` -$ ./build/ripwire . --regex='fnv1a\w+' - - - - - - - +$ ./build/ripwire . --grep=DISCLOSE + - - +$ ./build/ripwire . --grep=DISCLOSE --grep-context=1 + - - - - - - - - +$ ./build/ripwire . --grep=DISCLOSE --and=cache + - +CDATA text (/ context around it); complete=1 only for an exhaustive literal scan; = off-index. window: shown= capped= (capped=1 cut). hits_capped=1: hits= is a floor. root=: p= relative to it. parse_degraded=1: ERROR nodes in that parse. next=: the one pasteable f … [line truncated: 1241 more bytes on this line] + - - - - - - - + + - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1300,21 +1289,21 @@ file requires anywhere in the same file. Second occurrence of --grep=/--regex= i _Drop every hit in a file that ALSO contains the --not literal anywhere (file scope)._ ``` -$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file - - +CDATA text (/ context around it); complete=1 only for an exhaustive literal scan; = off-index. window: shown= capped= (capped=1 cut). hits_capped=1: hits= is a floor. root=: p= relative to it. parse_degraded=1: ERROR nodes in that parse. next=: the one pasteable f … [line truncated: 1241 more bytes on this line] + - - - - - - - + + - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1333,21 +1322,21 @@ code (default) serves the CODE tier when any hit is code, and otherwise comment _Span tiers off: the exhaustive view — the comment and string hits the default tier held back (suppressed_comment=96 / suppressed_string=29 in the plain --grep block above) now print alongside the code hits; hits= grows accordingly._ ``` -$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-in=any - - - - - - - +$ ./build/ripwire . --grep=DISCLOSE --grep-in=any + - - - - +$ ./build/ripwire . --grep=DISCLOSE --handles + - + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench @@ -1414,8 +1403,7 @@ $ ./build/ripwire . --match='(if_statement)' if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 if bv: ratios.append( cv / bv - 1 ) if not ratios: return None, None -if rw is None or native is None: return None -... [20 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-redact`, `--sarif`, `--limit` @@ -1454,19 +1442,19 @@ _Raw BM25 ranking (debug lens; --for is the real verb)._ ``` $ ./build/ripwire . --query="teleport pagerank" --top-k=5 - - - - - - - - + + + + - + - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1488,18 +1476,18 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - - - - - + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure - -What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph - -L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… ... [19 more line(s); run it to see the whole thing] ``` @@ -1517,24 +1505,24 @@ test/showcasecapturecheck.sh (C) re-derives this range from the SAME repo every **Try it** -_Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 91.8% fewer bytes at top-10, 85.2% at top-50, 85.5% at top-100 (re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart._ +_Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart._ ``` $ ./build/ripwire . --pack-signatures --top-k=10 - - - - - - - - - - + + + + - + + + + + + - ... [17 more line(s); run it to see the whole thing] ``` @@ -1552,7 +1540,12 @@ _Control-flow skeleton of one symbol, payload-only via the new --top-k=0._ ``` $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 -& p, float alpha = 0.85f ) + + + + +& p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); const std::vector pw = biasPrior( g, p ); @@ -1561,12 +1554,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 std::vector rankDouble( N, 0.0 ); PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun if( N ) - { - ... - } - std::vector r( N, 0.f ); - std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); -... [3 more line(s); run it to see the whole thing] +... [8 more line(s); run it to see the whole thing] ``` **Shaped by:** `--top-k`, `--expand`, `--compress`, `--no-redact`, `--limit`, `--max-memory` @@ -1583,11 +1571,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - - + + +, with a BRANCH-FREE size(). Both, not one or the other. @@ -1663,10 +1655,6 @@ $ ./build/ripwire . --pack-top-n=3 --top-k=0 // canonByName / shard maps, and ~100 more structures after the conversion wave): WRITE-ONCE during the // parse/merge, then READ-HOT during resolve. // -// Three things matter for that shape, and the layout below gets all three: -// • no per-list malloc — the N small lists that would each allocate are inline; -// • a BRANCH-FREE size() — `return sz_`, because the size lives in its own field; -// • 16 BYTES per instance — because `inl_` and `heap_` are never both live, so they share storage. ... [17 more line(s); run it to see the whole thing] ``` @@ -1688,14 +1676,14 @@ _--no-redact: emit bodies verbatim (credential redaction is on by default)._ ``` $ ./build/ripwire . --expand=readAckRecords --top-k=0 --no-redact - - + - - + + - + - + - + @@ -1758,20 +1746,20 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - - - - - - - - - - + + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1794,20 +1782,20 @@ _Complexity x recent git churn (maintenance pain)._ ``` $ ./build/ripwire . --hotspots - - - + - - - - - - - - - - + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1823,20 +1811,20 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - - - - - - - + + + + + + - - - - - + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1854,17 +1842,16 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - - - - - - - - - - + + + + + + + + + + ``` @@ -1888,9 +1875,8 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - - - @@ -1902,6 +1888,7 @@ $ ./build/ripwire . --nonlocal-state --limit=8 + ... [17 more line(s); run it to see the whole thing] ``` @@ -1923,20 +1910,20 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - - - + + - + - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1954,20 +1941,20 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - - - - + + - + + + + + + + - - - - ... [17 more line(s); run it to see the whole thing] ``` @@ -1991,16 +1978,15 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - - - - - - - - - -... [22 more line(s); run it to see the whole thing] + + + + + + + +... [23 more line(s); run it to see the whole thing] ``` **Shaped by:** `--limit` @@ -2023,21 +2009,21 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - - - - + + + + - + - -

-

-

-... [13 more line(s); run it to see the whole thing] + +

+

+

+... [14 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -2058,20 +2044,20 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - - - - - + + + + + + + - + - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2095,20 +2081,20 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - - + + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2130,16 +2116,16 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - - - - - + + + + + + + + + - - - - ``` @@ -2163,19 +2149,17 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - - - - - - - + + + + + + - - + + - + ... [22 more line(s); run it to see the whole thing] ``` @@ -2197,19 +2181,17 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - - - - - - + + + + + + - - - - + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2225,20 +2207,20 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - - - - - - - - + + + + + + + + + + - - + + - - ... [17 more line(s); run it to see the whole thing] ``` @@ -2254,20 +2236,20 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - - - - - + + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2291,9 +2273,9 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes ``` $ ./build/ripwire . --arch=test/archfix/rules.txt - - - + @@ -2321,9 +2303,9 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes ``` $ ./build/ripwire . --arch=test/archfix/rules.txt - - - + @@ -2347,9 +2329,9 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes ``` $ ./build/ripwire . --arch=test/archfix/rules.txt - - - + @@ -2375,20 +2357,20 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - - + + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2411,8 +2393,8 @@ _The built-in rule registry — one row per rule with sev=/category=/rationale/l ``` $ ./build/ripwire . --lint-catalog - - + + a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_cast a goto statement — cppcoreguidelines-avoid-goto a do/while loop shape (on Swift, its do/catch block shares the same grammar node and also matches) @@ -2421,8 +2403,7 @@ $ ./build/ripwire . --lint-catalog a doubly-parenthesized expression — readability-redundant-parentheses an if-body that is just `;` — bugprone-suspicious-semicolon a C-style typedef struct/union where `using` is preferred -a non-trivial numeric literal inside a function body, outside a const/constexpr init -... [20 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--lint` @@ -2437,8 +2418,8 @@ _User lint rules (YAML, ast-grep style) from a directory._ ``` $ ./build/ripwire . --lint-rules=test/lintrulesfix/rules - - + + use LOG() instead of printf @@ -2485,20 +2466,20 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - - - + + + - + - + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2571,20 +2552,20 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - - - - + + + + + + + + + + + - - - - - - - ... [17 more line(s); run it to see the whole thing] ``` @@ -2602,10 +2583,9 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - - - + + ``` @@ -2628,20 +2608,20 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - - - - + + + - - + - + - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2666,16 +2646,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2039 files · 18699 symbols · 22330 edges · 1313 modules (11420 call-graph isolated) +2484 files · 23849 symbols · 34232 edges · 2322 modules (10910 call-graph isolated) Root: `.` -Call-graph isolate provenance: 2633 declaration, 1162 header, 3670 source, 3955 document; 0 connected Louvain singletons +Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 1313) -- **src::assign@infra/svector.h:342:19905 [resolve,compute,collect]** — 652 symbols -- **src::emplace@infra/svector.h:408:22477 [resolve,parse,run]** — 617 symbols -- **src::emitTo@infra/emit.h:51:2620 [write,run,emit]** — 377 symbols +## Modules (call-graph clusters; showing 12 of 2322) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1378 symbols +- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1136 symbols +- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 538 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2691,20 +2671,20 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - - - - - - - - + + + + + + + - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2723,17 +2703,17 @@ $ ./build/ripwire . --mermaid %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n98["src
5422"] - n99["src/infra
630"] + n101["src
7066"] + n102["src/infra
1276"] end subgraph sg1 ["test"] - n100["test
3546"] - n269["test/fixtures/recallpassage
314"] - n255["test/expandmodefix
151"] - n257["test/expandsibsfix
149"] - n315["test/massfix
77"] - n297["test/legofix
60"] - n335["test/optremarksfix
59"] + n103["test
4851"] + n328["test/fixtures/recallpassage
314"] + n280["test/expandmodefix
151"] + n282["test/expandsibsfix
149"] + n458["test/rubyattrsfix
85"] + n395["test/massfix
77"] + n360["test/javamethodreffix
62"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2751,20 +2731,20 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - - - - - - - - - - + + + + + + + + + + + + + - - ... [17 more line(s); run it to see the whole thing] ``` @@ -2778,13 +2758,12 @@ $ ./build/ripwire . --owners **Try it** -_High-confidence internal functions with no caller. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory._ +_Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory._ ``` $ ./build/ripwire . --dead-code=src - - - + + ``` @@ -2848,20 +2827,20 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; only these gate (when major). new-symbol=N: regressions on NEW code; never gate, but the debt is yours: read them. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + + + + + + - - - - - - - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2885,20 +2864,20 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; only these gate (when major). new-symbol=N: regressions on NEW code; never gate, but the debt is yours: read them. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + + + + + + - - - - - - - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2922,8 +2901,8 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm - -

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 2.79s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +**wall time: 1.14s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* + +**wall time: 1.06s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 5.12s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache --top-k=3` + +*Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* + +**wall time: 5.97s** + +````` + + + + + + + + + + + + + +````` + +Artifact written: + +````` + 18642990 ./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache +````` + +## `./build/ripwire . --max-file-size=8K --top-k=3` + +*Skip files above a size bound before parsing (note the corpus shrink in the header).* + +````` + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --scip=does_not_exist.scip --callers=rankGraphTeleport` + +*SCIP overlay with a missing index REFUSES (exit 1) naming the file — never silently serves the name-based map you named a precision index to improve on (it used to degrade in silence).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather than serving the name-based map you named a precision index to improve on (generate one with scip-clang/scip-python, or drop --scip) +````` + +## `./build/ripwire src test --top-k=5` + +*Multi-root workspace: ONE merged graph over two roots, paths labeled /.* + +**wall time: 7.01s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 54.45s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 4.1% 8.5% 11.8% + BM25 12.8% 15.0% 19.5% + BM25sub 17.5% 23.3% 30.1% + BM25body 13.8% 25.9% 44.0% + fused 14.3% 17.1% 29.1% + anchored 13.8% 25.9% 44.0% + same-dir 0.0% 8.8% 13.1% + random 0.2% 0.4% 0.8% <- floor (random ranking over F=2484 files) + note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not + relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 / + BM25sub / BM25body are QUERY-TIME lexical rankers (whole-name / subtoken / + subtoken+body); fused = RRF(ripwire, BM25sub); anchored = BM25body + anchored PPR + expansion (--for --anchor, EXPERIMENTAL). The SHIPPED default for --for/--query is + the subtoken+body lexical family (routed to name-exact only for an identifier-shaped + query — lexical.h chooseForRanker), so a gap between the ripwire and BM25* rows here + is structural-importance-vs-lexical-relatedness on a co-change task, not the shipped + retrieval path losing to an alternative it was never running. +````` + +## `./build/ripwire . --eval-retrieval` + +*Known-item retrieval eval: MRR + recall@k per ranker per query mode.* + +**wall time: 182.66s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=5833 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population + (smallest fnv1a64(scope::name) over the population, cut on the key so an identity is never split; + path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) + ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) + ranker query-mode MRR recall@1 recall@5 recall@10 + subtoken name 0.694 56.2% 85.8% 90.8% + subtoken doc-phrase 0.920 90.0% 94.0% 94.9% + name-exact name 0.905 83.5% 96.1% 97.4% + name-exact doc-phrase 0.013 0.5% 1.8% 2.6% + anchored name 0.699 57.8% 85.2% 89.2% + anchored doc-phrase 0.917 89.5% 94.0% 94.5% + routed name 0.908 83.7% 96.3% 97.7% + routed doc-phrase 0.919 90.0% 93.8% 94.7% + note: routing chose name-exact on 3986/4000 NAME queries (a NAME query is always identifier-shaped); + the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol + (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — + a common name (many definitions, or a subtoken carried by many symbol names) declines the route — so + conceptual prose falls back to subtoken+body; routed tracks the better ranker on BOTH modes + (routed==name-exact on name, ~=subtoken+body on doc-phrase). +````` + +## `./build/ripwire . --eval-stray=./../../tmp/lean-answers-068/showcase_scratch/aux/stray_labels2.tsv` + +*Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* + +**exit code: 3** — **wall time: 14.81s** + +Input file: + +````` +# refverdict labels for --eval-stray (the first three local branches, resolved at capture time; a missing branch is padded with a nonexistent name on purpose) +arm/for-howitworks-067-f2add merged +arm/for-howitworks-067-placebo unmerged +arm/orient-map-067-narrow merged +````` + +````` + + + + + + +````` + +## `./build/ripwire skills --eval-skills=./../../tmp/lean-answers-068/showcase_scratch/aux/skills_labels2.tsv` + +*Labelled skill-ROUTING eval over the repo's own skills/ directory (4 hand-labelled prompts).* + +Input file: + +````` +orient in an unfamiliar codebase fast ripwire-orient judged +who calls this function and what is the blast radius ripwire-navigate judged +plan parallel worktrees so the lanes do not collide ripwire-change-check judged +what is the weather in Paris none neg +````` + +````` +ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus './../../tmp/lean-answers-068/showcase_scratch/aux/skills_labels2.tsv'; split test=4 dev=0) + arm hit@1 hit@2 mrr sep-auc fire/abstain@ORACLE-th (upper bound) + overlap 66.7% 66.7% 0.690 0.000 50.0% (th=-1.000) + name 33.3% 66.7% 0.537 0.667 50.0% (th=0.000) + bm25-desc 66.7% 66.7% 0.690 0.667 75.0% (th=2.958) + bm25-full 33.3% 66.7% 0.611 1.000 50.0% (th=0.426) + for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.685) + random 6.2% 12.5% 0.211 0.500 <- floor (uniform-random ranking; auc 0.5 by definition) + provenance hit@1 (bm25-desc): router 0/0, desc 0/0, judged 2/3 (desc rows quote the descriptions - expect them easiest; judged is the honest number) + judged-only hit@1 per arm: overlap 2/3, name 1/3, bm25-desc 2/3, bm25-full 1/3, for-routed 1/3 + router-magnet: with ripwire-router ADMITTED as a candidate it takes top-1 on 1/3 positive prompts (bm25-desc arm) - why it is excluded above + per-skill (bm25-desc): name / permitted-rows / won / pos-fires / false-fires / neg-fires + ripwire-before-you-build 0 0 1 1 0 + ripwire-change-check 1 0 0 0 0 + ripwire-find-bug 0 0 0 0 0 + ripwire-fresh-eyes 0 0 0 0 1 + ripwire-graph-query 0 0 0 0 0 + ripwire-handoff 0 0 0 0 0 + ripwire-layers 0 0 0 0 0 + ripwire-mcp 0 0 0 0 0 + ripwire-navigate 1 1 1 0 0 + ripwire-opt-remarks 0 0 0 0 0 + ripwire-orient 1 1 1 0 0 + ripwire-perf-target 0 0 0 0 0 + ripwire-quality-bar 0 0 0 0 0 + ripwire-reuse-first 0 0 0 0 0 + ripwire-security-scan 0 0 0 0 0 + ripwire-write-tests 0 0 0 0 0 + misses (overlap): + line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" +… [19 more lines, 3699 bytes total] +````` + +## `./build/ripwire wrap claude` + +*Print the recipe to wire ripwire into Claude Code as an MCP server.* + +````` +# ripwire -> Claude Code (CLI-first) +# RECOMMENDED — this agent can run shell commands, so call the CLI directly. It costs +# nothing until you invoke it, and it reads CLAUDE.md, so the paste block below IS the wiring: +ripwire . --for="" --token-budget=2000 +# +# ...every follow-up call answers with the compact legend by default (terse definitions of only the +# attributes the answer carries; the payload is byte-identical either way). Add --legend=full when a +# definition's reasoning is needed -- `ripwire --help` carries the measured saving (one place, +# gate-held) -- this line does not repeat it, because two copies of a number is one that goes stale: +# ripwire . --callers=SYM --legend=full +# +# ALTERNATIVE — register the MCP server instead, for a warm index across calls: +claude mcp add ripwire -- ripwire --mcp +# verbs the agent can then call mid-task (33 total): +# read: analyze, rank_by, find_symbol, find_referencing_symbols, grep, cochange, memory_recall, situational_awareness, mentions, for, lego, owners, fetch_body, batch, flags, doc_drift, slice +# flagship reflex: exemplar, quality_delta, quality_baseline, impact, uses, affected, path_between, connect, explore, from_trace, edit_check, whereis, stray_content +# edit: replace_symbol_body, insert_before_symbol, insert_after_symbol +bash skills/install.sh # deploy to ${CLAUDE_CONFIG_DIR:-~/.claude}/skills (drift-gated) +bash skills/install.sh --hook # RECOMMENDED: advisory Read/Grep -> ripwire CLI nudge + session primer (opt-in, never blocks) +# +# context wiring — a binary on PATH is invisible to an agent until its rules file says when +# to reach for it. Paste the block below into CLAUDE.md: +# --- paste into CLAUDE.md --- +## ripwire — deterministic codebase maps (on PATH as `ripwire`) +Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after that warm, ~0.1s. +- Orient on a task: `ripwire --for=""` — ranked, quality-annotated + signatures. Paste symbol/file names from the issue verbatim; named mentions get anchored. +- One task: `--pack-task="" --legend=compact`; before parallel agents: `--plan-lanes=N --task=""`, then read `lanes[].execution`. +- Have a stack trace / build error: `ripwire --from-trace=FILE --legend=compact` (`-` = stdin) — + paste the error, don't paraphrase it into a query. +… [14 more lines, 4078 bytes total] +````` + +## `./build/ripwire --version` + +*Version + short build info.* + +````` +ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=1c08714cf) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:5567` + +*Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* + +**wall time: 1.96s** + +````` + + + + +````` + +## `./build/ripwire . --callers=@src/graph.h:5567` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +**wall time: 1.75s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --at=src/graph.h:999999` + +*A seed past the end of the file — the refusal shape for a faulted location.* + +**exit code: 1** — **wall time: 1.89s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 9418 lines — the seed asked for line 999999) +````` + +## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` + +*VERIFY a closed claim in one call: three-valued verdict (confirmed / refuted / not-established) with the evidence rows inline.* + +**wall time: 1.23s** + +````` + + + + + +````` + +## `./build/ripwire . --verify="unused(rankGraphTeleport)"` + +*A claim that is FALSE — the refuted shape, with the references that refute it.* + +**wall time: 4.23s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --verify="contains(src/graph.h, \"no such literal anywhere\")"` + +*A literal-scan absence: refuted only with complete= evidence, never on a partial scan.* + +**wall time: 2.50s** + +````` + + + +````` + +## `./build/ripwire . --verify="frobnicate(x)"` + +*An unparseable claim — the refusal names the accepted shapes.* + +**exit code: 1** — **wall time: 2.81s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. The claim language is CLOSED; the shapes are: calls(A, B) does A transitively call B · uses(SYM) is SYM referenced anywhere · unused(SYM) is SYM referenced nowhere · contains(FILE, "LITERAL") do FILE's bytes contain t … [line truncated: 218 more bytes on this line] +````` + +## `./build/ripwire . --slice=rankGraphTeleport` + +*Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* + +**wall time: 2.53s** + +````` + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport` + +*Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* + +**wall time: 2.11s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3` + +*TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* + +**wall time: 2.51s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + pw = biasPrior( g, p );]]> + + +& p, float alpha = 0.85f )]]> + + +& p, float alpha = 0.85f )]]> + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd` + +*Forward flow: which statements the seed's value reaches, at the default depth bound.* + +**wall time: 2.87s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + + + + + + + + + + + 0.0 )]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:nosuchvar` + +*A variable the definition does not bind — the refusal shape, naming the inventory.* + +**exit code: 1** — **wall time: 2.82s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice: no occurrence of 'nosuchvar' in rankGraphTeleport — sliceable locals: alpha, g, p, pw, N, teleport, rankDouble, run, teleportMass, value, inverseMass, r (bare --slice=rankGraphTeleport lists them with first-def lines) +````` + +## `./build/ripwire . --slice-depth=3` + +*--slice-depth without --slice-flow is refused loudly rather than silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire --slice=parseArgs:argIndex --slice-flow=fwd --slice-depth=4) +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --legend=compact` + +*The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* + +**wall time: 1.97s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + +````` + +## `./build/ripwire . --pattern='rankGraphTeleport($A, $B, $C)'` + +*Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* + +**wall time: 5.03s** + +````` + +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) + +````` + +## `./build/ripwire . --pattern='DISCLOSE(...)'` + +*The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* + +**wall time: 3.91s** + +````` + +DISCLOSE( result, AbiResult::DisclosureWhy::NoMergeBase, "abi: no merge-base for a ref (unrelated history?) — that ref +DISCLOSE( Diagnostics::answerRefused, "--arch exits 1 with path:line and the reason on stderr; no report is printed", +DISCLOSE( baseline, ArchBaselineRead::DisclosureWhy::SymlinkRefused, "arch: refusing to read the arch baseline sidecar t +DISCLOSE( Diagnostics::answerRefused, "--baseline exits 1: pathguard names the refused link on stderr and the verb says +DISCLOSE( ex, Exclusions::DisclosureWhy::ForHeaderSaturated, "atoms: the for-header exclusion stream spent its whole bud +DISCLOSE( ex, Exclusions::DisclosureWhy::StmtCrementSaturated, "atoms: the statement-crement exclusion stream spent its +DISCLOSE( ex, Exclusions::DisclosureWhy::StmtAssignSaturated, "atoms: the statement-assign exclusion stream spent its wh +DISCLOSE( ex, Exclusions::DisclosureWhy::InnerCommaSaturated, "atoms: the inner-comma exclusion stream spent its whole b +DISCLOSE( st, Type3Stats::DisclosureWhy::PairCapHit, "clones: Type-3 pair cap hit — first N compared (both-gate-surviv +DISCLOSE( scan, CommentCoherenceScan::DisclosureWhy::UnreadableFile, "comment-coherence: an indexed file could not be re +DISCLOSE( st, StreamBlobStats::DisclosureWhy::ListUnwritable, "crossref: cannot write the blob-batch list — cross-bran +DISCLOSE( st, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: git cat-file --batch failed to start — cross +DISCLOSE( st, StreamBlobStats::DisclosureWhy::StreamEndedMidBlob, "crossref: git cat-file stream e +DISCLOSE( dropSink, RefEnumeration::DisclosureWhy::TipNotObjectName, "crossref: for-each-ref yielded a ref whose tip is +DISCLOSE( "crossref: refusing a diff whose revision arguments are not resolved object names" ) +DISCLOSE( sweep, ParallelSweep::DisclosureWhy::WorkerThrew, "crossref: a git worker threw — this shard of the sweep is +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::RevisionNotObjectName, "crossref: ref tip or HEAD is not a resolved object +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::MergeBaseNotObjectName, "crossref: merge-base returned something that is no +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::NoMergeBase, "crossref: no merge-base for ref (shallow clone or unrelated h +DISCLOSE( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ) +DISCLOSE( "whereis: the index's def sites match no HEAD row (working tree drifted from HEAD?) — keeping the lexical la +DISCLOSE( scan, WorktreeScan::DisclosureWhy::ListingFailed, "whereis: git could not list the working tree's changes — +DISCLOSE( scan, WorktreeScan::DisclosureWhy::OverPathCap, "whereis: more changed paths than the overlay reads — the re +DISCLOSE( scan, WorktreeScan::DisclosureWhy::CopyUnread, "whereis: a changed path could not be read from the working tre +DISCLOSE( out, CMakeScan::DisclosureWhy::SymlinkEscapesRoot, "flags: a CMake file's symlink target leaves the root — f +DISCLOSE( out, CMakeScan::DisclosureWhy::RootWalkFailed, "flags: cannot walk root for CMake files — cmake gates omitte +DISCLOSE( r, Result::DisclosureWhy::BaseTreeUnavailable, "dmm: the base commit's tree could not be materialized or parse +DISCLOSE( r, Result::DisclosureWhy::TargetTreeUnavailable, "dmm: the target commit's tree could not be materialized or p +… [73 more display lines; full output is 17739 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --pattern='x'` + +*A pattern that collapses to a bare token is REFUSED — never reported as hits=0.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --pattern: this pattern collapses to a single bare token, which is a TEXT search, not a structural one — a clean parse is not enough (a bare word parses fine in most grammars and means nothing structural). Give it a shape (foo($X), $A + $B, if ($C) { ... }) or use the grep flag for litera … [line truncated: 176 more bytes on this line] +````` + +## `./build/ripwire . --grep=DISCLOSE --and=cache` + +*Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default).* + +**wall time: 1.90s** + +````` + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… +enum class AstWalk : std::uint8_t +struct SliceScan +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::array<LangEntry, 51> kLangTable = +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [55 more display lines; full output is 8839 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` + +*Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* + +**wall time: 3.28s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… +enum class AstWalk : std::uint8_t +struct SliceScan +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::array<LangEntry, 51> kLangTable = +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [90 more display lines; full output is 12414 bytes on 55 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` + +*Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* + +**wall time: 4.46s** + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +… [52 more display lines; full output is 9064 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` + +*The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* + +**wall time: 3.86s** + +````` + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) + +runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + +H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +struct QualityDeltaOutcome +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +… [53 more display lines; full output is 9230 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --safe-delete=rankGraphTeleport` + +*"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* + +**wall time: 1.32s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --safe-delete=DoesNotExist` + +*Unknown-symbol refusal shape for --safe-delete.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --safe-delete symbol not found: DoesNotExist +````` + +## `./build/ripwire . --handoff` + +*The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* + +**wall time: 5.21s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --handoff --token-budget=1200` + +*The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* + +**wall time: 4.62s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --skipped` + +*WHY a file is not in the index (oversize / excluded / unsupported-ext / gitignored) and which indexed files it cannot vouch for (degraded-parse, minified-suspect), plus the per-language census.* + +**wall time: 1.13s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [393 more display lines; full output is 42252 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --no-ignore --top-k=3` + +*Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied).* + +**wall time: 1.95s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-stable --top-k=3` + +*--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of.* + +**wall time: 2.02s** + +````` + + + + + + + + + + + + + +````` + +stderr: + +````` +ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable ordering the server turns on) — it changed nothing here; the CLI map orders important-first unless you pass --order=stable +````` + +## `./build/ripwire . --run-trace="cat ./../../tmp/lean-answers-068/showcase_scratch/aux/asan_trace_now.txt; exit 1"` + +*EXEC-MODE --from-trace: run a command, and on a non-zero exit map its captured output onto indexed symbols in the same call — the whole fix-loop entry. ripwire exits 4 here because the wrapped command failed, which is the signal, not an incident.* + +**exit code: 4** — **wall time: 1.77s** + +````` + + + + + + const&, float) src/graph.h:5567 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 + #3 0x102e01a44 in main src/main.cpp:3975 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { + const double inverseMass = 1.0 / teleportMass; + for( double& value : teleport ) + { + value *= inverseMass; + } + } + run = pageRankDouble( g.inEdges, g.wOutDeg, teleport, rankDouble, PageRankConfig{ .alpha = double( alpha ) } ); + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 480 more bytes on this line] +````` + +## `./build/ripwire . --run-trace="true"` + +*A command that exits 0: a minimal success record (exit, measured duration, disclosed output tail) and NO bundle — nothing failed, nothing to map.* + +**wall time: 1.09s** + +````` + + + + +````` + +## `./build/ripwire . --run-trace="sleep 30" --run-timeout=2` + +*A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* + +**exit code: 4** — **wall time: 3.11s** + +````` + + + + +````` + +stderr: + +````` +ripwire: --run-trace: TIMEOUT — the command exceeded the 2 s cap; its process group was killed +````` + +## `./build/ripwire . --run-timeout=5` + +*--run-timeout alone is refused loudly (it only modifies --run-trace).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwire --run-trace="make -j" --run-timeout=60) +````` + + +--- + +# assess quality — the wider lens family + +## `./build/ripwire . --quality-panel` + +*THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* + +**wall time: 14.75s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [186 more display lines; full output is 15787 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --biggest-first --limit=8` + +*Per-function size ranking, largest Halstead volume/token entropy/lines first — a size proxy, not a readability order (withdrawn, docs/EVALS.md §8) — a RANKING lens, not a grade.* + +**wall time: 2.73s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --comment-coherence --limit=8` + +*Functions WITH a doc comment, most name-restating first: c_coeff (high = the comment repeats the name) and cic (Jaccard of comment vs identifier vocabulary), both reported, never collapsed.* + +**wall time: 1.77s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --context-ratio --limit=8` + +*The local-reasoning lens: to understand this symbol, how much must you know that is NOT in front of you (ent_ratio= edge share, read_ratio= token-weighted).* + +**wall time: 2.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11209 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --nonlocal-state --limit=8` + +*Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* + +**wall time: 9.67s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7593 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +**wall time: 6.39s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 7334 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity` + +*The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* + +**wall time: 4.21s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [509 more display lines; full output is 45771 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity=Symbol` + +*The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* + +**wall time: 3.53s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --naming-consistency --limit=8` + +*The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename).* + +**wall time: 1.52s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 4383 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --naming-calibration` + +*Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* + +**wall time: 30.62s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [119 more display lines; full output is 12978 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --naming-locals` + +*The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* + +**wall time: 7.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [640 more display lines; full output is 72432 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-catalog` + +*The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed.* + +**wall time: 2.16s** + +````` + + +a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_cast +a goto statement — cppcoreguidelines-avoid-goto +a do/while loop shape (on Swift, its do/catch block shares the same grammar node and also matches) +a call to an unbounded C string function (strcpy/strcat/sprintf/gets) +a call to a broken hash or cipher (MD5/SHA1/MD4/RC4) +a doubly-parenthesized expression — readability-redundant-parentheses +an if-body that is just `;` — bugprone-suspicious-semicolon +a C-style typedef struct/union where `using` is preferred +a non-trivial numeric literal inside a function body, outside a const/constexpr init +a catch block with an empty body +x = x — almost always a copy-paste bug +a function body over 80 lines +brace nesting depth over 4 inside a function body +a bare `return;` mixed with a value-returning return in the same function +a statement after an unconditional return/break/continue/throw/raise in the same block +a 1-2 letter Function/Method/Var name — visible far beyond any tiny scope [Beniamini/Hofmeister] +more than 5 split tokens in one name [Butler; AlSuhaibani] +foo1/foo2/... digit-suffix siblings sharing a base name in one scope [Butler] +internal consecutive underscores, or a C-family reserved __x/_X form [Butler] +snake_case and camelCase mixed inside one name [Butler] +an is/has/can/should/was-prefixed name whose KNOWN return type is not bool-like [LAPD A2] +a set-prefixed name whose KNOWN return type is not void-like [LAPD A3] +a co-visible pair within edit distance <=2, reordered tokens, or a bare/digit-suffixed twin [Namesake] +every split subtoken is corpus-ubiquitous (BM25 idf) on a body past the size floor — fires only at the low end [Sparck Jones 1972] +the comma operator inside an expression (never a for-header comma) [Gopstein FSE 2017] +++/-- evaluated inside a larger expression (never a whole statement) [Gopstein FSE 2017] +an assignment whose VALUE is consumed (a condition, an argument, ...) [Gopstein FSE 2017] +a conditional expression inside a conditional expression [Gopstein FSE 2017] +… [12 more display lines; full output is 7764 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --legend-dict` + +*The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* + +````` +ripwire legend dictionary ripwire.dict/v1 dictv=6c4d27498f3d0809 entries=786 +: the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] +schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows +window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) +X_capped= (any attribute ending _capped): 1 = cut +under ref, est_tokens= and over_ceiling= price the answer with its inline legend: an upper bound +under ref, a group of n <= 8 runner-less rows prints as its n single rows, each run_unknown=1 +ripwire.map/v1 : ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data +ripwire.map-diff/v1 : the ranked map anchored at at=: what the diff touched, the map's row vocabulary +ripwire.metrics/v1 : the ranked map with per-symbol metrics: in/out, cx/ccx, loc, params, nest, humps/deep, locals, cbo, amp, tested, ev +ripwire.around/v1 : call neighbourhood of of=: depth= hops, fanout= kept per hop; absent rows lie outside that boundary +ripwire.query/v1 : lexical-rank map for the query term, the map's row vocabulary +ripwire.pack-signatures/v1 : the ranked map plus signature rows +ripwire.pack-top-n/v1 : the ranked map plus bodies of the top-N symbols +ripwire.skipped/v1 : why the index lacks a file ; indexed but unvouched ; census +ripwire.notes/v1 : field notes by target: holds ; the kept count comment: notes= rows, targets= rows, dangling= targets matching nothing indexed (listed, surfaced nowhere) +ripwire.lego/v1 : ONE interface/base type: , its method contract, every implementor +ripwire.expand/v1 : full bodies: of ; resolved callees +ripwire.expand-file/v1 : the file's own text: ; per scoped symbol; full id = p::sc::n +ripwire.pack-task/v1 : one-call task bundle for task= under budget_tokens=: ranking, ranked but over 1 hop out (of_top= ranked rows) > with callees > 1-hop from the bodies (o … [line truncated: 105 more bytes on this line] +ripwire.from-trace/v1 : trace frames mapped to indexed symbols, innermost first; the innermost in-corpus body included +ripwire.exemplar/v1 : the best-in-class instance of kind= for the task, chosen by role: , to imitate +ripwire.pack-task/v1 : N minimally overlapping agent bundles carved along call-graph communities plus one shared core; each wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_t … [line truncated: 345 more bytes on this line] +ripwire.callers/v1 : 1-hop CALLERS of of= (defs= matched, count= distinct symbols): ; hop_tested=/hop_untested= +ripwire.callees/v1 : 1-hop CALLEES of of= (defs= matched, count= distinct symbols): +ripwire.uses/v1 : resolvable use-sites of of=: +ripwire.impact/v1 : transitive blast radius of of=: reach set, importers; defs= matched, reaches= their transitive callers, radius_tested= non-tests an indexed test reaches, radius_untested= the rest; importers= files that #include/import a def's file +ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none +ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, +ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans +… [757 more display lines; full output is 78294 bytes on 787 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=cache-` + +*Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* + +**wall time: 6.60s** + +````` + + + + + + + + + + +vector<std::vector<double>> +nodes[ perm[i] ] +nodes[ perm[ i + 1 ] ] +p = p->next +map +byName[ symN[i] ] +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +vector<std::vector<double>> +m[ in.keys[ i ] ] +map +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +map +m[ g_pool[ g_keyOf[i] ] ] +g_pool[ g_readOf[i] ] +g_pool[ g_readOf[i] ] +m[ g_pool[i] ] +… [505 more display lines; full output is 61080 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-ignore=naming-,cache-` + +*DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* + +**wall time: 3.94s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [729 more display lines; full output is 65896 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** — **wall time: 4.81s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-'?) — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --lint-select=nosuchfamily` + +*A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* + +**exit code: 1** — **wall time: 4.83s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --sarif` + +*The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* + +**wall time: 3.72s** + +````` +{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] +{"id":"goto","shortDescription":{"text":"goto"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"do-while","shortDescription":{"text":"do-while"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"unsafe-c-fn","shortDescription":{"text":"unsafe-c-fn"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"weak-crypto","shortDescription":{"text":"weak-crypto"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"redundant-parens","shortDescription":{"text":"redundant-parens"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"suspicious-semicolon","shortDescription":{"text":"suspicious-semicolon"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"typedef-over-using","shortDescription":{"text":"typedef-over-using"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"magic-number","shortDescription":{"text":"magic-number"},"properties":{"builtin":true,"capped":true,"applicable":true}}, +{"id":"empty-catch","shortDescription":{"text":"empty-catch"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"self-assign","shortDescription":{"text":"self-assign"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"large-function","shortDescription":{"text":"large-function"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"deep-nesting","shortDescription":{"text":"deep-nesting"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"inconsistent-return","shortDescription":{"text":"inconsistent-return"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"unreachable-code","shortDescription":{"text":"unreachable-code"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-short","shortDescription":{"text":"naming-short"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-wordy","shortDescription":{"text":"naming-wordy"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-series","shortDescription":{"text":"naming-series"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-underscore","shortDescription":{"text":"naming-underscore"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-case","shortDescription":{"text":"naming-case"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-predicate","shortDescription":{"text":"naming-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-setter","shortDescription":{"text":"naming-setter"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-confusable","shortDescription":{"text":"naming-confusable"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-uninformative","shortDescription":{"text":"naming-uninformative"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-comma-operator","shortDescription":{"text":"atom-comma-operator"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-embedded-crement","shortDescription":{"text":"atom-embedded-crement"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-assign-as-value","shortDescription":{"text":"atom-assign-as-value"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-nested-ternary","shortDescription":{"text":"atom-nested-ternary"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-implicit-predicate","shortDescription":{"text":"atom-implicit-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-octal-literal","shortDescription":{"text":"atom-octal-literal"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +… [10017 more display lines; full output is 1403377 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 5004 +````` + +## `./build/ripwire . --lint --sarif --limit=5` + +*SARIF is always the FULL result set: paging alongside it refuses loudly.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M +````` + +## `./build/ripwire . --dmm` + +*The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* + +**wall time: 7.00s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD` + +*The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* + +**wall time: 14.50s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD~3..HEAD` + +*The range form: tree HEAD vs tree HEAD~3.* + +**wall time: 11.26s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --cochange --cochange-groups` + +*Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* + +**wall time: 3.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [726 more display lines; full output is 47798 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange --cochange-recur=2` + +*Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect.* + +**wall time: 1.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html --color-by=community` + +*The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* + +**wall time: 1.64s** + +````` +(empty) +````` + +Artifact written: + +````` + 158888 ./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html +````` + +## `./build/ripwire . --index-out=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index` + +*CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* + +**wall time: 7.85s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache (18642990 bytes, lean family) +ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache (39998532 bytes, rich family) +````` + +Artifact written: + +````` + 18642990 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache + 39998532 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache + 58641522 total +````` + +## `./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache --top-k=3` + +*Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* + +**wall time: 1.78s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --pin-census=./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv --top-k=3` + +*Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* + +**wall time: 2.44s** + +````` + + + + + + + + + + + + + +````` + +Artifact written: + +````` +# ripwire pin-census v3 C=kind\tmech\tpre\tpost\tflags\tcaller_id\tcallee\ttargets(|-sep)\tline +# v3 field escape: every id and callee field is escaped, so no separator occurs inside one. A backslash is +# written \\, TAB \t, LF \n, CR \r, every other control byte 0x00-0x1f and the targets separator | as \xHH +# (lowercase hex). Split lines on LF, fields on TAB and targets on |, THEN decode. Columns are unchanged +# from v2 and an id without those bytes is spelled exactly as in v2; a C++ out-of-line template member +# whose template-argument list spans lines keeps its line break as \n instead of splitting the row. +# line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): +# the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. + 114908 ./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` + +*The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates.* + +**exit code: 2** — **wall time: 1.32s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave_ledger.md` + +*The ledger-shaped fixture through the same check (exit 2: one gating card).* + +**exit code: 2** + +````` + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=claude` + +*--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* + +**exit code: 1** — **wall time: 6.03s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=nosuch` + +*Other --agent values refuse.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: unsupported --agent value 'nosuch' (supported: codex, claude) +````` + + +--- + +# the dirty-tree verbs (throwaway clone, NOT the read-only repo) + +Everything below runs with `cwd` = the throwaway clone at `./../../tmp/lean-answers-068/showcase_scratch/dirty` (`git clone --local` of this repo, then one deliberate regression in `src/infra/sortutil.h`). The read-only repo is never touched. The binary is the same `build/ripwire`, addressed absolutely. + +## `./build/ripwire . --situ` + +*Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* + +**wall time: 6.15s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: 1c08714cf+dirty + [1] blast radius: 190 symbols across 46 files transitively depend on these changes (showing 8 of 46 files — shown=8 total=46 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=46) + counts_floor=1 graph_ambiguous=10281 graph_unresolved=12860 graph_unindexed=237 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missin … [line truncated: 46 more bytes on this line] + src/mcpverbs.h (32 dependent symbols) + src/slice.h (30 dependent symbols) + src/serialize.h (9 dependent symbols) + src/quality.h (8 dependent symbols) + src/verbs_navigate.h (8 dependent symbols) + src/main.cpp (7 dependent symbols) + src/verbs_quality.h (7 dependent symbols) + src/mergescout.h (6 dependent symbols) + [2] tests to run (5) order=evidence: [changed] you edited it, [partner] named after a changed file, then hops asc (1 = direct); "(n): a, b" = n runner-less files sharing that evidence; a (run: …) is relative to root: + test/verify_radix.cpp [hops=1] (run: bash test/greptiercheck.sh) + test/adaptivecutshapefix/adaptive_cut_shape_test.cpp [hops=2] (run: bash test/adaptivecutshapecheck.sh) + test/includeprecise_unit.cpp [hops=2] (run: bash test/includeprecisecheck.sh) + test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) + test/rustimport_unit.cpp [hops=4] (run: bash test/rustimportprecisecheck.sh) + script_gates_unmodelled=729 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="3930": + (none — 3930 commits were mined and none co-edited a file outside your diff) + next: --test-gate +````` + +## `./build/ripwire . --test-gate` + +*The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* + +**exit code: 4** — **wall time: 4.28s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --quality-delta` + +*Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* + +**exit code: 2** — **wall time: 30.32s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [115 more display lines; full output is 15390 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --legend=compact` + +*The same gating report under --legend=compact — same rows, same exit 2, schema="ripwire.quality-delta/v1", ~4 KB of legend down to one comment: the shape an agent's edit loop should run.* + +**exit code: 2** — **wall time: 19.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [115 more display lines; full output is 15390 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --json` + +*The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* + +**exit code: 2** — **wall time: 25.20s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":133,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"1c08714cf+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] +{"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"duplication","members":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","tokens":59,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"nesting","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":6,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"new-clone-of-reused-helper","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","was":0,"now":4,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"params","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","was":0,"now":8,"p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"verbosity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":8,"now":47,"p":"src/infra/sortutil.h:49","sev":"minor"}], +"sa":[{"kind":"api-surface","key":"085e3d408c2c4a35","why":"target-gone"}, +{"kind":"api-surface","key":"155d74d341a49f3c","why":"target-gone"}, +{"kind":"api-surface","key":"1a15386c2d1e47af","why":"target-gone"}, +{"kind":"api-surface","key":"298e798c7f075715","why":"target-gone"}, +{"kind":"api-surface","key":"56acdf9b5c314a14","why":"target-gone"}, +{"kind":"api-surface","key":"5a07390012b46e06","why":"target-gone"}, +{"kind":"api-surface","key":"6eef859578c8c376","why":"target-gone"}, +{"kind":"api-surface","key":"6f2394762f82a855","why":"target-gone"}, +{"kind":"api-surface","key":"7f2c3eefdf6e512e","why":"target-gone"}, +{"kind":"api-surface","key":"802e513731103806","why":"target-gone"}, +{"kind":"api-surface","key":"b6a24afef32a68a8","why":"target-gone"}, +{"kind":"api-surface","key":"c923e661c197b265","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1926b0d9e94541a0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1d3814ba687a4aef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1db76028879244ef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, +… [112 more display lines; full output is 13998 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope` + +*NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* + +**exit code: 1** — **wall time: 9.31s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 224 re-derived, 9 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1328 ineligible (no cur … [line truncated: 198 more bytes on this line] +ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=api-surface` + +*NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* + +**wall time: 8.48s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 224 re-derived, 9 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1328 ineligible (no cur … [line truncated: 198 more bytes on this line] +ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already acked) → ./.ripwire_quality_acks +````` + +## `./build/ripwire . --quality-delta` + +*Re-run after the partial ack: acked=3, the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 15.80s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [114 more display lines; full output is 15093 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 4 preexisting-worse major finding(s); first: complexity src/infra/sortutil.h::rw::sortutil::lessByScoreDescId at src/infra/sortutil.h:49 (was=1 now=67) +````` + +## `./build/ripwire . --ack-only=gating` + +*--ack-only WITHOUT --quality-ack REFUSES loudly (exit 1, the pairing named) — it used to be silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire

--quality-delta --ack-only=contract-change --quality-ack="reason") +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey` + +*A real contract-change: was=1 now=2 params, with the call sites that are now provably incompatible, and a pasteable next= (--uses=SYM) on the root.* + +**wall time: 2.93s** + +````` + + + + + + +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=compact` + +*The same verdict under --legend=compact: ~5.7 KB of legend becomes one comment, every row identical — the post-edit reflex at its cheapest.* + +**wall time: 2.41s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 11.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [87 more display lines; full output is 8449 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*The map re-ranked with a teleport toward the changed file (changed=1 here, not 0).* + +**wall time: 2.84s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [37 more display lines; full output is 4185 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 8.82s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [307 more display lines; full output is 16180 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zz-orphan` + +*CHANGED: a ref with NO merge base with HEAD now reports v="unknown" ok="0" in its own bucket — the absence of an answer, never a claim it is merged. (The sandbox carries a deliberately parentless branch built with `git commit-tree`; a shallow CI clone puts every ref here.)* + +**wall time: 2.69s** + +````` + + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1171, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --stray-content=zz-orphan --plan` + +*CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* + +**wall time: 5.52s** + +````` + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1171, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --dmm` + +*The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* + +**wall time: 8.00s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --quality-delta --scope=src/graph.h` + +*OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* + +**wall time: 25.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [117 more display lines; full output is 15360 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --quality-ack --scope=src/graph.h --ack-only=api-surface` + +*The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* + +**exit code: 1** — **wall time: 11.52s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=src/graph.h — refusing, and writing nothing at all: + api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 + those rows belong to whoever is editing those paths. Acking them here writes their debt into a committed ledger under YOUR + reason string, which is how a per-finding ratchet becomes a rubber stamp. Narrow the pattern, or widen the scope if they really are yours. +````` + +## `./build/ripwire . --handoff` + +*The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* + +**wall time: 4.86s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --note-add="lessByScoreDescId: keep this branch-free — it sits inside the PageRank sort comparator"` + +*Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* + +**wall time: 1.13s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 keep this branch-free — it sits inside the PageRank sort comparator 1c08714cfd64f810ceb81055ce8f96cb28a5348e lane/lean-answers-068 +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sortutil.h::rw::sortutil::lessByScoreDescId' — that is the id --for/--expand key notes by +````` + +## `./build/ripwire . --notes` + +*The note is listed under the canonical id, dangling="0" — i.e. it will actually surface. (Before the H1 fix the bare name was stored verbatim and read dangling="1": recorded, and surfaced nowhere.)* + +````` + + + + + + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --note-add="gitOneLine: which one?"` + +*Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'gitOneLine' is ambiguous — it matches 2 definitions in 2 distinct contracts, and a note keys ONE canonical id (it would surface on one of them and look absent on the rest). Qualify one: ./src/handoff.h:gitOneLine, ./src/quality.h:gitOneLine — e.g. --note-add="./src/h … [line truncated: 33 more bytes on this line] +````` + +## `./build/ripwire . --note-add="lessByScoreDescIdd: typo"` + +*A name that resolves to nothing is refused with the read verbs' own did-you-mean — never written as a dead note.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByScoreDescId'?) — a note keys the canonical id a read verb resolves; to note a FILE instead, pass a path (one with a '/' or an extension), which may name a file that does not exist yet +````` + +## `./build/ripwire . --note-add="src/infra/sortutil.h::rw::sortutil::lessByScoreDescId: chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort"` + +*The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 1c08714cfd64f810ceb81055ce8f96cb28a5348e lane/lean-answers-068 +````` + +## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` + +*Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* + +**wall time: 1.53s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h` + +*Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* + +**wall time: 8.23s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: bash test/greptiercheck.sh +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --no-post-check` + +*The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* + +**wall time: 2.93s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha (post-check skipped); next: --edit-check=src/infra/sortutil.h:lessByScoreDescId +````` + +## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` + +*Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* + +**wall time: 8.75s** + +````` +{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: --uses=src/infra/sortutil.h:nonNegativeFloatDescKey +````` + +## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h` + +*An unknown TARGET refuses and leaves every file byte-identical.* + +**exit code: 1** — **wall time: 4.58s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found +````` + +## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --dry-run` + +*A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* + +**wall time: 2.31s** + +Input file: + +````` +{ + "version": 1, + "edits": [ + { + "op": "insert_before_symbol", + "target": "nonNegativeFloatDescKey", + "payload": "plan_note.h" + } + ] +} +````` + +````` +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 354 more bytes on this line] +````` + +## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --apply` + +*The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* + +**wall time: 5.20s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true}}]} +````` + +## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json` + +*Neither --dry-run nor --apply: the mode is explicit, so this refuses.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --edit-plan requires exactly one of --dry-run or --apply +````` + +## `./build/ripwire . --quality-delta` + +*After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 33.77s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [111 more display lines; full output is 14457 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:88 (was=0 now=59) +````` + +## `./build/ripwire . --quality-baseline` + +*REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* + +**exit code: 1** — **wall time: 16.82s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against HEAD — pinning here would absorb + them into the floor, and every later --quality-delta would read clean. First: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey (was=0 now=59) + Commit the tree first, or pass --allow-dirty to pin anyway (the sidecar then records the 2 absorbed, and every + report against it carries baseline_absorbed="2"). +````` + +## `./build/ripwire . --quality-baseline --allow-dirty` + +*The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* + +**wall time: 22.87s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 23136 per-symbol rows, 23851 indexed symbols total) +````` + +Artifact written: + +````` + 3875997 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer 2f85b423e4a29cc1ee97ea63a524e3aec90985f784b6c5cfe2fb6d8933202e6b +head 1c08714cfd64f810ceb81055ce8f96cb28a5348e +dirty 1 +absorbed 2 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699bdb 0 +ccx 7d8862be4e +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 17.68s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [106 more display lines; full output is 13322 bytes on 1 raw line(s)] +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional ceiling (the CLI --for --token-budget). The header carries route= (which ranker answere … [line truncated: 1053 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 657 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 806 more bytes on this line] +{"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"affected","description":"WHICH TESTS TO RUN for a change — test files that transitively reach the changed files/symbols, ranked by evidence (edited > partner-named > hop distance). files = changed files and/or symbols, comma-separated: each item is tried as an indexed PATH pattern first, … [line truncated: 1238 more bytes on this line] +{"name":"path_between","description":"Does A REACH B, and HOW? — the shortest directed CALL path between two symbols, hop-by-hop. reachable=\"0\" hops=\"0\" is a valid 'not reachable' answer — call edges are name-based, so a missing dynamic/callback edge can hide a real path. Named path_between … [line truncated: 709 more bytes on this line] +{"name":"connect","description":"When a task touches 2..16 named symbols, returns the minimal subgraph RELATING them - terminals, the fewest joining intermediaries (with signatures), and call edges in true direction - finding the shared-caller joins a directed path_between cannot; unrelated symbols … [line truncated: 950 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 1933 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1364 more bytes on this line] +{"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] +{"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] +{"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] +{"name":"slice","description":"WHERE IS THIS VARIABLE DEFINED AND USED inside one function — NAME-BASED intra-procedural def-use rows of one variable inside ONE uniquely-resolved definition (the ARISE slicer, arXiv:2605.03117). symbol alone lists the sliceable locals to pick from; add var (or spel … [line truncated: 1722 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: + +````` +tools= 33 manifest_bytes= 46880 (~tokens at 4 bytes/token: 11720 ) +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +explore desc_bytes= 1322 schema_bytes= 774 required=['task'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +whereis desc_bytes= 701 schema_bytes= 828 required=['symbol'] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +affected desc_bytes= 790 schema_bytes= 610 required=['files'] +from_trace desc_bytes= 829 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 445 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +quality_delta desc_bytes= 772 schema_bytes= 193 required=[] +path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] +cochange desc_bytes= 388 schema_bytes= 476 required=['file'] +analyze desc_bytes= 546 schema_bytes= 318 required=[] +lego desc_bytes= 360 schema_bytes= 467 required=['type'] +mentions desc_bytes= 305 schema_bytes= 486 required=['symbol'] +quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` + +*MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* + +**wall time: 1.39s** + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= total= capped= has_more= next_offset= offset= limit= (ca … [line truncated: 14122 more bytes on this line] +"_index":"[index: files=2484 symbols=23849 hash=095847f8]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":" @@ -212,6 +212,27 @@ $ ./build/ripwire . --for="rankGraphTeleport" A comma-separated, order-insensitive, closed set (lego and/or compose, each named at most once); every other section of the bundle is unaffected. +**Try it** + +_Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling)._ + +``` +$ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +... [19 more line(s); run it to see the whole thing] +``` + **Shaped by:** `--for` ### `--signatures-only` @@ -226,7 +247,7 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - + @@ -258,7 +279,7 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + @@ -269,7 +290,8 @@ $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() struct FileHealth -... [20 more line(s); run it to see the whole thing] +enum class AstWalk : std::uint8_t +... [19 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for` @@ -291,7 +313,7 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - + @@ -324,7 +346,7 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - + @@ -335,7 +357,8 @@ $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() struct FileHealth -... [20 more line(s); run it to see the whole thing] +enum class AstWalk : std::uint8_t +... [19 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--detail` @@ -356,7 +379,7 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - @@ -388,14 +411,14 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… ... [21 more line(s); run it to see the whole thing] @@ -480,7 +503,7 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ $ ./build/ripwire . --recall="quality delta gating exit codes" ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.995) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 7.969) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -543,25 +566,20 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1588991-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1399178-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -━━ commands.md (relevance 10.658) ━━ [sections: 1 of 158 selected (187 in doc), section-granular; whole doc 721575 B; lines="515-540"; dropped_by_budget=157] [truncated: 1854 of 4242 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 10.211) ━━ [sections: 1 of 157 selected (187 in doc), section-granular; whole doc 533991 B; lines="3700-3704"; dropped_by_budget=156] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] -... [17 more line(s); run it to see the whole thing] +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ``` ### `--tree` @@ -1095,7 +1113,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 1c08714cf +at: 75cc95803 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1114,18 +1132,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1174,7 +1192,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - + @@ -1476,7 +1494,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1784,13 +1802,13 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - + - + @@ -1812,7 +1830,7 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + @@ -2010,7 +2028,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2021,7 +2039,7 @@ $ ./build/ripwire . --naming-calibration

-

+

... [14 more line(s); run it to see the whole thing] ``` @@ -2150,16 +2168,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - + - - - - - - + + + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2182,16 +2200,16 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - + - - - - + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2208,7 +2226,7 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + @@ -2218,7 +2236,7 @@ $ ./build/ripwire . --cochange --cochange-groups - + ... [17 more line(s); run it to see the whole thing] @@ -2237,15 +2255,15 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - + - - + + - + @@ -2732,7 +2750,7 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - + @@ -2828,7 +2846,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2865,7 +2883,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2902,7 +2920,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

-

+

@@ -5641,7 +5612,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* -**wall time: 7.04s** +**wall time: 3.05s** ````` @@ -5681,8 +5652,6 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed.* -**wall time: 2.16s** - ````` @@ -5759,7 +5728,7 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= *Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* -**wall time: 6.60s** +**wall time: 2.14s** ````` @@ -5799,7 +5768,7 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= *DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* -**wall time: 3.94s** +**wall time: 2.31s** ````` @@ -5839,7 +5808,7 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= *An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* -**exit code: 1** — **wall time: 4.81s** +**exit code: 1** — **wall time: 2.09s** ````` (empty) @@ -5855,7 +5824,7 @@ ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-' *A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* -**exit code: 1** — **wall time: 4.83s** +**exit code: 1** — **wall time: 2.04s** ````` (empty) @@ -5871,7 +5840,7 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- *The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* -**wall time: 3.72s** +**wall time: 2.05s** ````` {"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] @@ -5933,11 +5902,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* -**wall time: 7.00s** +**wall time: 4.61s** ````` -

@@ -5948,11 +5917,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* -**wall time: 14.50s** +**wall time: 8.57s** ````` -

@@ -5963,14 +5932,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The range form: tree HEAD vs tree HEAD~3.* -**wall time: 11.26s** +**wall time: 8.87s** ````` - -

-

+ +

+

````` @@ -5978,11 +5947,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* -**wall time: 3.20s** +**wall time: 1.93s** ````` - + @@ -5992,7 +5961,7 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M - + @@ -6005,7 +5974,7 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M - + @@ -6018,37 +5987,37 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect.* -**wall time: 1.71s** +**wall time: 1.19s** ````` - + + + + + - - - - - + - - + + - + @@ -6060,7 +6029,7 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* -**wall time: 1.64s** +**wall time: 1.43s** ````` (empty) @@ -6076,7 +6045,7 @@ Artifact written: *CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* -**wall time: 7.85s** +**wall time: 4.52s** ````` (empty) @@ -6085,24 +6054,22 @@ Artifact written: stderr: ````` -ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache (18642990 bytes, lean family) -ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache (39998532 bytes, rich family) +ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache (18643301 bytes, lean family) +ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache (39963389 bytes, rich family) ````` Artifact written: ````` - 18642990 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache - 39998532 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache - 58641522 total + 18643301 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache + 39963389 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache + 58606690 total ````` ## `./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache --top-k=3` *Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* -**wall time: 1.78s** - ````` @@ -6123,8 +6090,6 @@ Artifact written: *Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* -**wall time: 2.44s** - ````` @@ -6152,18 +6117,18 @@ Artifact written: # whose template-argument list spans lines keeps its line break as \n instead of splitting the row. # line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): # the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. - 114908 ./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv + 114912 ./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv ````` ## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` *The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates.* -**exit code: 2** — **wall time: 1.32s** +**exit code: 2** ````` - + @@ -6185,7 +6150,7 @@ Artifact written: ````` - + @@ -6197,17 +6162,17 @@ Artifact written: *--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* -**exit code: 1** — **wall time: 6.03s** +**exit code: 1** — **wall time: 5.63s** ````` - - + - - + + - + @@ -6244,12 +6209,12 @@ Everything below runs with `cwd` = the throwaway clone at `./../../tmp/lean-answ *Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* -**wall time: 6.15s** +**wall time: 5.32s** ````` ripwire situational-awareness — 1 changed file(s), 13 symbols in them root: . -at: 1c08714cf+dirty +at: 75cc95803+dirty [1] blast radius: 190 symbols across 46 files transitively depend on these changes (showing 8 of 46 files — shown=8 total=46 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=46) counts_floor=1 graph_ambiguous=10281 graph_unresolved=12860 graph_unindexed=237 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missin … [line truncated: 46 more bytes on this line] src/mcpverbs.h (32 dependent symbols) @@ -6267,8 +6232,8 @@ at: 1c08714cf+dirty test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) test/rustimport_unit.cpp [hops=4] (run: bash test/rustimportprecisecheck.sh) script_gates_unmodelled=729 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) - [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="3930": - (none — 3930 commits were mined and none co-edited a file outside your diff) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="3933": + (none — 3933 commits were mined and none co-edited a file outside your diff) next: --test-gate ````` @@ -6276,7 +6241,7 @@ at: 1c08714cf+dirty *The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* -**exit code: 4** — **wall time: 4.28s** +**exit code: 4** — **wall time: 5.00s** ````` @@ -6318,11 +6283,11 @@ at: 1c08714cf+dirty *Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* -**exit code: 2** — **wall time: 30.32s** +**exit code: 2** — **wall time: 22.76s** ````` - @@ -6361,15 +6326,15 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) ````` -## `./build/ripwire . --quality-delta --legend=compact` +## `./build/ripwire . --quality-delta --legend=full` -*The same gating report under --legend=compact — same rows, same exit 2, schema="ripwire.quality-delta/v1", ~4 KB of legend down to one comment: the shape an agent's edit loop should run.* +*The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend.* -**exit code: 2** — **wall time: 19.47s** +**exit code: 2** — **wall time: 10.77s** ````` - - + @@ -6398,7 +6363,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap -… [115 more display lines; full output is 15390 bytes on 1 raw line(s)] +… [115 more display lines; full output is 20204 bytes on 1 raw line(s)] ````` stderr: @@ -6412,10 +6377,10 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* -**exit code: 2** — **wall time: 25.20s** +**exit code: 2** — **wall time: 15.12s** ````` -{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":133,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"1c08714cf+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":133,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"75cc95803+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] {"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, @@ -6459,7 +6424,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* -**exit code: 1** — **wall time: 9.31s** +**exit code: 1** — **wall time: 6.86s** ````` (empty) @@ -6477,7 +6442,7 @@ ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written *NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* -**wall time: 8.48s** +**wall time: 7.83s** ````` (empty) @@ -6495,11 +6460,11 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already *Re-run after the partial ack: acked=3, the rest still gate (exit 2).* -**exit code: 2** — **wall time: 15.80s** +**exit code: 2** — **wall time: 10.20s** ````` - @@ -6558,11 +6523,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6570,15 +6535,15 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire ````` -## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=compact` +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=full` -*The same verdict under --legend=compact: ~5.7 KB of legend becomes one comment, every row identical — the post-edit reflex at its cheapest.* +*The same verdict under --legend=full: the default's one compact comment (the post-edit reflex at its cheapest) becomes ~5.7 KB of prose legend, every row identical.* -**wall time: 2.41s** +**wall time: 1.46s** ````` - - + @@ -6590,11 +6555,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6630,13 +6595,13 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). changed=K: K indexed git-changed files seed the PageRank teleport (0: uniform, incl. no git). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - + @@ -6670,11 +6635,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire of ; dup_loc=/dup_pct=. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). root=: p= relative to it. groups=/type3=: Type-2 and Type-3 group totals over all groups; total= is their sum. exempt_groups=N: groups whose members all sit on fixture/shell-runner paths quality-delta duplication ignores. idiom_groups=/demoted_groups=: groups of one recognized idiom / those quality-delta demotes to minor; floors. clone_groups=N: clusters after merging pairs (rows sharing gid=); a floor under type3_capped=1. total_loc=N: lines of every function body the detector considered; dup_pct= is dup_loc= over it. group exempt=fixture|shell-runner: every member is on such a path; quality-delta duplication ignores it. group idiom=: the recognized shape every member spells: threshold-ladder, switch-name-table, builder-chain. --> - + @@ -6710,12 +6675,12 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire ; v=unknown = no merge base. at=: commit+dirty+shallow. head=: the HEAD commit, bare 9-hex sha (at= adds +dirty). head_ref=: HEAD's branch (HEAD when detached); that branch itself is not scanned. refs=N: local branches scanned (refs/heads only); unmerged + superseded + merged + unknown = refs. blobs=N: distinct git blobs read for the sweep. unmerged=N: refs whose authored work the live line genuinely lacks. superseded=N: refs whose work the live line re-implemented (removed the same base code). merged=N: refs whose work HEAD already has; omitted from the rows. name=: the local branch. tip=: the branch tip commit (9 hex). date=: the tip's committer date, YYYY-MM-DD. files=N: files with stray lines; rows capped at 12, a more element counts the rest (detail=1 lists all). ref superseded=N>: of this ref's stray= lines, those in files the live line re-implemented. unknown=N: refs that could not be analysed (v=unknown, e.g. no merge base); never counted merged. --> - - + + ````` @@ -6730,11 +6695,11 @@ stderr: *CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* -**wall time: 5.52s** +**wall time: 3.86s** ````` - + ````` @@ -6749,11 +6714,11 @@ stderr: *The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* -**wall time: 8.00s** +**wall time: 4.08s** ````` -

@@ -6764,11 +6729,11 @@ stderr: *OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* -**wall time: 25.71s** +**wall time: 10.72s** ````` - @@ -6810,7 +6775,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* -**exit code: 1** — **wall time: 11.52s** +**exit code: 1** — **wall time: 8.51s** ````` (empty) @@ -6830,11 +6795,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* -**wall time: 4.86s** +**wall time: 4.95s** ````` - + @@ -6859,11 +6824,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr - - - - - + + + + + ````` @@ -6872,10 +6837,10 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* -**wall time: 1.13s** +**wall time: 1.57s** ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 keep this branch-free — it sits inside the PageRank sort comparator 1c08714cfd64f810ceb81055ce8f96cb28a5348e lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 keep this branch-free — it sits inside the PageRank sort comparator 75cc95803667a285f5899c2a6218343bea65f8b4 lane/lean-answers-068 ````` stderr: @@ -6900,7 +6865,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort - + @@ -6917,7 +6882,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort *Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry.* -**exit code: 1** +**exit code: 1** — **wall time: 1.04s** ````` (empty) @@ -6952,15 +6917,13 @@ ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByS *The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 1c08714cfd64f810ceb81055ce8f96cb28a5348e lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 75cc95803667a285f5899c2a6218343bea65f8b4 lane/lean-answers-068 ````` ## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` *Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* -**wall time: 1.53s** - ````` - + - + @@ -7083,7 +7046,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* -**wall time: 8.23s** +**wall time: 5.27s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] @@ -7105,7 +7068,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* -**wall time: 2.93s** +**wall time: 2.08s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] @@ -7121,7 +7084,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha (post-check s *Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* -**wall time: 8.75s** +**wall time: 5.53s** ````` {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] @@ -7146,7 +7109,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *An unknown TARGET refuses and leaves every file byte-identical.* -**exit code: 1** — **wall time: 4.58s** +**exit code: 1** — **wall time: 2.95s** ````` (empty) @@ -7162,7 +7125,7 @@ ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found *A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* -**wall time: 2.31s** +**wall time: 1.84s** Input file: @@ -7187,7 +7150,7 @@ Input file: *The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* -**wall time: 5.20s** +**wall time: 4.35s** ````` {"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] @@ -7217,11 +7180,11 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply *After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* -**exit code: 2** — **wall time: 33.77s** +**exit code: 2** — **wall time: 11.55s** ````` - @@ -7264,7 +7227,7 @@ ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: du *REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* -**exit code: 1** — **wall time: 16.82s** +**exit code: 1** — **wall time: 7.54s** ````` (empty) @@ -7283,7 +7246,7 @@ ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against *The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* -**wall time: 22.87s** +**wall time: 11.71s** ````` (empty) @@ -7299,10 +7262,10 @@ ripwire: wrote ./.ripwire_quality_baseline (snapshot of 23136 per-symbol rows, 2 Artifact written: ````` - 3875997 .ripwire_quality_baseline + 3875998 .ripwire_quality_baseline # ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit -producer 2f85b423e4a29cc1ee97ea63a524e3aec90985f784b6c5cfe2fb6d8933202e6b -head 1c08714cfd64f810ceb81055ce8f96cb28a5348e +producer 384869b1e64dca97276ab209b6d3fb50c9f58098e7eadb0088f175cda691d61e +head 75cc95803667a285f5899c2a6218343bea65f8b4 dirty 1 absorbed 2 ccx d19d7c13cbc7 2 @@ -7315,11 +7278,11 @@ ccx 7d8862be4e *Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* -**wall time: 17.68s** +**wall time: 10.92s** ````` - @@ -7343,7 +7306,7 @@ ccx 7d8862be4e - + @@ -7436,90 +7399,120 @@ mentions desc_bytes= 305 schema_bytes= 486 required=['symb quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] ````` +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp --mcp-tools=core` + +*--mcp-tools=core: the server lists only the core profile's tools (explore, batch, from_trace, impact, uses, fetch_body, edit_check, quality_delta), so a host that loads every schema at session start pays only for those.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 955 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELAT … [line truncated: 1092 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 806 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 1933 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 13921 (~tokens at 4 bytes/token: 3480 ) +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +explore desc_bytes= 1322 schema_bytes= 774 required=['task'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +from_trace desc_bytes= 829 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 772 schema_bytes= 193 required=[] +````` + ## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` *MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* -**wall time: 1.39s** +**wall time: 2.19s** ````` {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= total= capped= has_more= next_offset= offset= limit= (ca … [line truncated: 14122 more bytes on this line] -"_index":"[index: files=2484 symbols=23849 hash=095847f8]","_fresh":"ok"}} +"_index":"[index: files=2484 symbols=23849 hash=780d3b8f]","_fresh":"ok"}} {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":""; } +// …and the MAP's spelling, as its own comment: the same three facts as kViaNameLegend in the fewest bytes (a map prints the +// most hedged rows of any answer, and its --max-tokens fit pays for every legend byte in rows), plus x=, the count a merged +// row carries (serialize.h writeMapCalleeRows), and where each merged candidate is listed. No `--` inside an XML comment: +// the flag is named callees=. +inline constexpr std::string_view kMapViaNameLegend = + ""; // …and MCP's spelling: the same sentence as the `via_note` key of an answer one of whose entries carries "via":"name". inline std::string viaNameNoteJson() { diff --git a/src/main.cpp b/src/main.cpp index b420d9ee2..a0b3a03dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1866,6 +1866,7 @@ int runDefaultMap( const MainDispatch& d ) recentOf }; mapAnn.recentMinedHistory = recentAnyHistory; // the block rides on the FACT (serialize.h writeRecentRows) mapAnn.recentMergeBombsSkipped = recentMergeBombsSkipped; // rides (the rows' own window), filled by assignment like seed + mapAnn.viaLegendStripped = cfg.legend == "compact" && !cfg.json; // FE-B: the rewrite would strip the map's via comment mapAnn.notesDegraded = d.notesDegraded; // L3 follow-up (CodeRabbit 4053600616): onto every this run emits mapAnn.codeFirstRows = isDefaultMapScope; // the code-first row pick + its data_sections_cut= / next= (serialize.h) // C1-b (2026-09-12): --in=DIR — the scoped block and the map stub, filled by assignment like seed. The two next= strings diff --git a/src/serialize.h b/src/serialize.h index c05a1df8a..64686c448 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1931,6 +1931,9 @@ struct MapAnnotations // next= that pages them. Every other map keeps the plain rank-order cut, and a map-scope map whose cut swapped // nothing carries neither attribute (byte-identical). Filled by assignment, like the trailing fields above. bool codeFirstRows = false; + // FE-B: the compact posture strips the map's via="name" comment (compactlegend.h, the via/x rows restate it), so the + // map does not write it at all — and the --max-tokens fit, which measures this render, prices the delivered bytes. + bool viaLegendStripped = false; }; // ── the code-first row pick: data_sections_cut= / next= (docs/EVALS.md "Map data Sections never crowd code out of the @@ -2599,6 +2602,72 @@ inline std::uint32_t counterAt( const std::vector* v, NodeId id ) return ( v && id < v->size() ) ? ( *v )[ id ] : 0u; } +// FE-B: one map 's rows over out-edges [from, to). A via="name" row (matched by name alone) that the symbol would +// print several times byte for byte — N same-named by-name candidates, one row each, carrying only n=/prov=/via= — is +// written ONCE, at its first edge, with x="N": the count, and --callees on the lists each candidate with its file +// (graphlegend.h kMapViaNameLegend / compactlegend.h's x row). Every other row, prov="split" arms included, is written as +// before. `scratch` is reused across symbols. +inline void writeMapCalleeRows( XmlWriter& w, const IngestResult& ing, std::uint32_t from, std::uint32_t to, const std::vector& outTargets, + const std::vector* outProv, const std::vector* outNameOnly, std::vector& esc, + std::vector& scratch ) +{ + const auto provOf = [ & ]( std::uint32_t e ) -> std::uint8_t { return ( outProv && e < outProv->size() ) ? ( *outProv )[ e ] : 0; }; + const auto nameOnly = [ & ]( std::uint32_t e ) { return outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[ e ] != 0; }; + // the row's bytes among via="name" rows: name, prov= + const auto rowLess = [ & ]( std::uint32_t x, std::uint32_t y ) + { + const std::string& nx = ing.symbols[ outTargets[ x ] ].name; + const std::string& ny = ing.symbols[ outTargets[ y ] ].name; + return nx != ny ? nx < ny : provOf( x ) < provOf( y ); + }; + scratch.clear(); + for( std::uint32_t e = from; e < to; ++e ) + { + if( nameOnly( e ) ) + { + scratch.push_back( e ); + } + } + std::stable_sort( scratch.begin(), scratch.end(), rowLess ); // each group of equal rows stays in edge order + for( std::uint32_t e = from; e < to; ++e ) + { + std::size_t repeats = 1; + if( nameOnly( e ) ) + { + const auto group = std::equal_range( scratch.begin(), scratch.end(), e, rowLess ); + ASSUME( group.first != group.second, "every via=\"name\" edge of [from, to) was collected into scratch" ); + if( *group.first != e ) + { + continue; // a repeat of an earlier row: counted in that row's x= + } + repeats = std::size_t( group.second - group.first ); + } + w.write( " says K of this symbol's CALLS were guesses and cannot + // say WHICH edges; prov="split" names the arms, and the suspect set becomes the guessed edges and nothing else. + if( provOf( e ) != 0 ) + { + w.write( "\" prov=\"" ); + w.write( provLabel( provOf( e ) ) ); + } + if( nameOnly( e ) ) + { + w.write( "\" via=\"name" ); // FE-B: the same edge bit every other surface reads (viaNameLegendComment) + } + if( repeats > 1 ) + { + w.write( "\" x=\"" ); + w.write( std::to_string( repeats ) ); + } + w.write( "\"/>" ); + } +} + // FE-B: may a row under one of `nodes` carry via="name"? True when any of their out-edges is name-only — an // over-approximation of what a section renders (a block's 16-per-symbol cap may cut that row; a merged overload // row prints one representative's edges), asked BEFORE the rows are written by a surface whose first-screen legend must @@ -2915,11 +2984,14 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const std::size_t declinedTotal = counterTotal( declinedOut ); // calls tier 3 declined: no edge, and no guess legend += declinedTotal > 0 ? kDeclinedMapLegend : ""; // charged to the map that carries declined= legend += gateDeclinedCalls > 0 ? kDeclinedGateMapLegend : ""; // only where the builtin-method gate declined a call - if( outNameOnly && !stubbed && namesOnlyOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ) ) ) + if( outNameOnly && !stubbed && !ann.viaLegendStripped + && namesOnlyOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ) ) ) { - // FE-B: the reading every other surface carries (graphlegend.h kViaNameLegend, one wording), as its own comment — exactly - // when a kept row may print a via="name" (over-approximated by merged overloads, which print one member's edges) - legend += viaNameLegendComment(); + // FE-B: the reading every other surface carries (graphlegend.h kViaNameLegend), in the map's short spelling — exactly + // when a kept row may print a via="name" (over-approximated by merged overloads, which print one member's edges). + // It also defines a merged row's x=. Not written under the compact posture, whose rewrite strips it (compactlegend.h) + // and whose element-qualified via/x rows restate it: the --max-tokens fit then prices what is delivered. + legend += kMapViaNameLegend; } // C1 DRIFT FIX (Round C lane B, found by re-reading this header's own output). `precise=` means "how many // out-edges a SCIP index PINNED", and the emitter's own comment below says it is "emitted ONLY under @@ -3152,6 +3224,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // The children, as ONE renderer both paths call. This was inline code writing through a writer bound to "the buffer, // or `out` when the buffer could not open". That serves a failed open, and cannot serve a buffer that opened and then // lost a write: by then the children were already spent into it, and the only bytes left to print had a hole in them. + std::vector hedgedScratch; // FE-B: writeMapCalleeRows' reused buffer const auto writeChildren = [ & ]( XmlWriter& w ) { // §P8 collision: this prologue spelled its LABEL `l=`, the two characters 22 other sites use for a LINE @@ -3418,31 +3491,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto } w.write( ">" ); - for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) - { - w.write( " says K of this - // symbol's CALLS were guesses and cannot say WHICH edges, so a consumer honouring the honesty - // signal had to distrust every child. prov="split" names the arms, and the suspect set - // becomes the guessed edges and nothing else. - if( outProv && e < outProv->size() && ( *outProv )[e] ) - { - w.write( "\" prov=\"" ); - w.write( provLabel( ( *outProv )[e] ) ); - } - if( outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[e] != 0 ) - { - w.write( "\" via=\"name" ); // FE-B: the same edge bit every other surface reads (viaNameLegendComment) - } - w.write( "\"/>" ); - } + writeMapCalleeRows( w, ing, outOff[id], outOff[id + 1], outTargets, outProv, outNameOnly, esc, hedgedScratch ); w.write( "" ); } w.write( "" ); diff --git a/test/lpincheck.sh b/test/lpincheck.sh index 2a8e1bb7f..b0f62d5da 100755 --- a/test/lpincheck.sh +++ b/test/lpincheck.sh @@ -155,9 +155,11 @@ class Subscriber: PYEOF "$BIN" "$SELFD" --no-cache >"$TMP/selfwin.xml" 2>/dev/null SW="$( sed 's/>\n stands for N edges (serialize.h writeMapCalleeRows), so edges are summed +SW_EDGES="$( printf '%s' "$SW" | grep -o ']*>' | awk '{ n = 1; if ( match( $0, / x="[0-9]+"/ ) ) n = substr( $0, RSTART + 4, RLENGTH - 5 ) + 0; s += n } END { print s + 0 }' )" +[ "$SW_EDGES" = 2 ] \ && ok "(I) the facade keeps BOTH real targets (2 edges) — the caller no longer wins its own tie-break" \ - || no "(I) facade.py::Facade::publish_event has $( printf '%s' "$SW" | grep -c '"$g/u.js" + local full="$TMP/mergemap.full.xml" comp="$TMP/mergemap.compact.xml" cal="$TMP/mergemap.callees.xml" + ( cd "$g" && "$BIN" . --no-cache --top-k=500 --legend=full >"$full" 2>/dev/null ); printf '%s' "$?" >"$full.rc" + ( cd "$g" && "$BIN" . --no-cache --top-k=500 --legend=compact >"$comp" 2>/dev/null ); printf '%s' "$?" >"$comp.rc" + ( cd "$g" && "$BIN" . --no-cache --callees=u.js:use >"$cal" 2>/dev/null ); printf '%s' "$?" >"$cal.rc" + ran_ok "(H) merge: the full-legend map" "$full" || return + ran_ok "(H) merge: the compact-legend map" "$comp" || return + ran_ok "(H) merge: --callees=u.js:use" "$cal" || return + local use proven + use="$( sed 's/]*>' | wc -l | tr -d ' ' )" = 1 ] && printf '%s' "$use" | grep -qE '' \ + && ok "(H) merge: use's three same-named hedged get candidates print ONE row " \ + || no "(H) merge: use's get rows are not one x=\"3\" row: $use" + printf '%s' "$use" | grep -q ']* x="' \ + && no "(H) merge near miss: use's lone put candidate carries x=: $use" \ + || ok "(H) merge near miss: a lone hedged candidate (put) carries no x=" + printf '%s' "$proven" | grep -q 'x="' \ + && no "(H) merge near miss: proven's evidence-bound get row was merged: $proven" \ + || ok "(H) merge near miss: a proven row is never merged (proven's get carries no x=)" + [ "$( grep -o ']*n="get"' "$cal" | wc -l | tr -d ' ' )" = 3 ] \ + && ok "(H) merge: --callees=u.js:use lists all three get candidates (the merged row is recoverable)" \ + || no "(H) merge: --callees=u.js:use does not list three get candidates: $( head -c 600 "$cal" )" + grep -q 'x=N' "$full" && P legend "$full" && [ "$PARSED" = "DEFINED" ] \ + && ok "(H) merge: the full map legend defines via=\"name\" and x=" \ + || no "(H) merge: the full map legend does not define via=\"name\" (name alone + does NOT mean) and x=" + grep -q '' "$comp" && grep -qE '' "$comp" \ + && ok "(H) merge: the compact map defines and keeps the merged row" \ + || no "(H) merge: the compact map does not define beside the merged row" +} +merge_arms + echo "--- (H) MCP twins (fresh TMPDIR: the MCP cache lives there)" mkdir -p "$TMP/mcp" # mcp ROOT TOOL SELECTOR OUT — one tools/call (then tools/list); the raw stream into OUT, the exit status into OUT.rc diff --git a/test/resolverhonestycheck.sh b/test/resolverhonestycheck.sh index 8437be575..df223b31b 100755 --- a/test/resolverhonestycheck.sh +++ b/test/resolverhonestycheck.sh @@ -246,7 +246,8 @@ except Exception as e: for s in root.iter( "s" ): if s.get( "n" ) == sym: for c in s.findall( "c" ): - print( c.get( "n" ), c.get( "prov" ) or "-" ) + for _ in range( int( c.get( "x" ) or 1 ) ): # FE-B: a merged via="name" row x=N stands for N edges + print( c.get( "n" ), c.get( "prov" ) or "-" ) ' "$2" } # how many of SYM's edges carry prov="split" From fd1176e6dcb9a673ae526e4316ae1667fa9f23d4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 22:10:08 -0400 Subject: [PATCH 153/229] defect-shape: count past comparisons, re-pin what the new kind moves, and record the measurement - format-arity: a lone '>' in an argument is a comparison (n > 0), not a template list; only more '<' than '>' (std::pair{} cut by the comma split) makes the count unknowable. The train-25 CLI impact legend (an argument carrying `imUnprovenDefs > 0`) is found now. Gate arms cmpinarg (positive) and tmplarg (negative). - Compact legend entries trimmed so the quality-delta prose stays under its 940 B pin (new-symbol= carries the format-arity exception; preexisting-worse= and r origin= shrink). - Re-pins, each with its measured reason in the gate: qsnap tripwire (scheme 17 -> 18; defectSitesOf joins the manifest), qsnapproducercheck's blob reader (the defect records), printf parity help/help_all (the help text), donelegendcheck's four qd budgets (+87 B core sentence each), the tools/list ceiling 46,750 -> 46,800 (measured 46,793), and cppqualcheck's readWholeFile use count 31 -> 32 (defectSitesOf). - CHANGELOG and EVALS: the kind, its exception, and its recall/precision numbers. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ docs/EVALS.md | 5 ++++- src/compactlegend.h | 6 +++--- src/defectshape.h | 7 ++++--- src/mcp.h | 2 +- src/quality.h | 4 ++++ src/verbs_quality.h | 9 ++++----- test/cppqualcheck.sh | 8 +++++--- test/defectshapecheck.sh | 10 ++++++++-- test/donelegendcheck.sh | 14 ++++++++++---- test/mcpmanifestcheck.sh | 8 +++++++- test/printf_parity.manifest | 4 ++-- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 5 +++++ test/qsnapproducercheck.sh | 1 + 15 files changed, 96 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d22e195d..b0d8427de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,43 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `--quality-delta` gains a twelfth kind, `defect-shape`: four defect shapes code review kept finding by hand + +`kind="defect-shape"` rows name a known defect SHAPE the change added, in `defect=` (`src/defectshape.h`): +- `format-arity`: a literal `std::format` / `format_to` / `format_to_n` / `print` / `println`, `fmt::`, + `rw::emitTo` / `rw::formatTo` or Python `"literal".format(…)` format whose replacement fields do not match its + arguments. `std::format_string` rejects too few arguments at compile time and accepts too many silently; Python + raises on too few and ignores too many. Two sides of a merge that each add the same field and one argument merge + cleanly into one field too few — the case this was built for. +- `utf8-cut` (C++): display text cut at a byte cap and given an ellipsis, with no UTF-8 back-off in the function. +- `dedup-first` (C++): `std::unique` keeping the first of a run over a type with a severity field that neither the + predicate nor the sort before it reads. +- `vacuous-assert` (Bash test scripts): an absence assertion read off a run whose failure is never checked, so a + crash reads as PASS. + +A site is new only when its normalized text is not among the baseline's sites of that shape, repo-wide: a moved or +renamed defect is not new, an edited one is. **New-symbol rows never gate, except defect-shape format-arity**: a +placeholder/argument mismatch is a defect, not debt, so it gates on new code too. The exit predicate lives in one +place (`quality::rowGates`) and every surface asks it — `gating=` and the row's `gating`, `--json` / MCP `"gating"`, +`--ack-only=gating`, the scoped-out and allow-dirty counts, the stderr line. `utf8-cut`, `dedup-first` and +`vacuous-assert` are always `sev="minor"`: report-only by facet, not by size. `--ack-only=FACET` acks one shape; the +ack key carries the facet, so acking one shape never suppresses another on the same definition. + +Measured on the eight review findings it was built from: recall 7/8 (format-arity 2/2 on the train-25 keep-both merge, +utf8-cut 1/1, dedup-first 1/1, vacuous-assert 3/4 — the fourth sits beside a later check on content derived from the +same run, which already fails the script on a crash). With every site of a tree treated as new: format-arity 0 false +positives on this tree (1,330 literal-format calls), on the Python 3.13 standard library (405 `"…".format(` calls) +and on libc++'s std::format tests (80 calls; its 24 hits are the deliberately ill-formed `.verify.cpp` calls); +vacuous-assert 49 rows on this tree's gates, every one an absence read off an unchecked run (sample 30/30, Wilson 95% +lower bound 0.89). The `tools/list` manifest grows 46,732 → 46,793 B: the quality_delta description counts 12 kinds and +states the format-arity exception. + +Not checked, so no row there is no verdict: Python %-formatting and f-strings; a format in a named constant, a macro +or a runtime wrapper; a pack expansion; an unqualified `format()`; the `std::print` ostream overload; a named dedup +predicate; a utf8 cut in a function that backs off a continuation byte anywhere; a run behind a script-defined +wrapper function or a text utility reading a file. The qsnap blob scheme moves 17 → 18; the legend dictionary gains +`r defect=` (785 entries). + ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` The entry below left one floor open, (i). A call to self, or on a receiver the code builds, read a class's defs from both diff --git a/docs/EVALS.md b/docs/EVALS.md index d072bc32f..dcc188f8f 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -6178,7 +6178,10 @@ The first ten each target a failure mode measured in the literature; `placeholde honesty check on "done" — a stub or TODO the change added — and never gates. `defect-shape` carries four facets in `defect=` (`format-arity`, `utf8-cut`, `dedup-first`, `vacuous-assert`), shapes this project's own code review kept finding; only `format-arity` gates, and on any origin (new-symbol rows never gate, except -defect-shape format-arity). Measured recall and false-positive counts: MEASUREMENT_PENDING. +defect-shape format-arity). Measured (2026-10-07): recall 7/8 on the eight review findings it was built +from; with every site treated as new, 0 format-arity false positives on this tree (1,330 literal-format calls), +on the Python 3.13 standard library (405 calls) and on libc++'s std::format tests (80 calls; the 24 hits are its +deliberately ill-formed `.verify.cpp` calls); vacuous-assert sample precision 30/30 on this tree's gates. Note that some user-facing summaries abbreviate four of these (`dup`, `dead`, `churn`, `clone-of-reused-helper` / `reuse-decline`). **Match against the strings above** when grepping real diff --git a/src/compactlegend.h b/src/compactlegend.h index 8377ce626..64764d55e 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -707,8 +707,8 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "from_defs", "from_defs=/to_defs=: definitions of each name, all searched; above 1, qualify file:name", false, "path", MapHeaderRead::No, {}, "path" }, // also defines to_defs= // quality-delta: src/verbs_quality.h (root emit) + src/quality.h identityDisclosure { "stale", "stale=N: ack ledger rows whose target no longer applies (sa rows); never gating", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, - { "preexisting-worse", "preexisting-worse=N: regressions on symbols that existed at baseline; these gate when major (so does defect-shape format-arity, any origin)", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, - { "new-symbol", "new-symbol=N: regressions on NEW code; new-symbol rows never gate, except defect-shape format-arity; the debt is yours: read them", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "preexisting-worse", "preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "new-symbol", "new-symbol=N: regressions on NEW code; new-symbol rows never gate, except defect-shape format-arity; read them", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "register-macro-excluded", "register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "declined-call-excluded", "declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "api-new-surface", "api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, @@ -1115,7 +1115,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // comment-coherence: c_coeff= is spelled only in the purpose, which does not say HIGH c_coeff is BAD (restates the name); outside this gap list. // quality-delta rows (src/verbs_quality.h): the row facets the purpose line does not spell, present-only. { "sev", "r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major)", true, "r", MapHeaderRead::No, {}, "quality-delta" }, - { "origin", "r origin=new-symbol: the finding is on NEW code; new-symbol rows never gate, except defect-shape format-arity (absent: preexisting-worse)", true, "r", MapHeaderRead::No, {}, "quality-delta" }, + { "origin", "r origin=new-symbol: a finding on NEW code (absent: preexisting-worse)", true, "r", MapHeaderRead::No, {}, "quality-delta" }, { "churn", "r churn=self|ambient: the edit modifies lines committed inside the churn window (self) or only adds/touches older ones (ambient); informational", true, "r", MapHeaderRead::No, {}, "quality-delta" }, { "idiom", "r idiom=: the recognized clone-body shape of a duplication row", true, "r", MapHeaderRead::No, {}, "quality-delta" }, { "defect", "r defect=: a defect-shape row's shape: format-arity (gates, any origin) or utf8-cut / dedup-first / vacuous-assert (always sev=minor)", true, "r", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/defectshape.h b/src/defectshape.h index 21c0d8379..c56f6fb92 100644 --- a/src/defectshape.h +++ b/src/defectshape.h @@ -707,8 +707,9 @@ inline bool isAllCapsIdent( std::string_view w ) noexcept } // An argument whose count is unknowable here: a pack expansion, __VA_ARGS__, an all-caps identifier alone (an -// object-like macro may expand to a comma list), or unbalanced angle brackets (a template argument list the -// comma split may have cut). +// object-like macro may expand to a comma list), or more '<' than '>' (a template argument list the comma split +// may have cut: std::pair{} splits as "std::pair{}"). A lone '>' is a comparison +// (n > 0) and does not make the count unknowable. inline bool argCountUnknowable( std::string_view src, const std::vector& toks, const ArgRange& a ) noexcept { if( a.empty() ) @@ -733,7 +734,7 @@ inline bool argCountUnknowable( std::string_view src, const std::vector& to angle += tokIs( src, toks[k], "<" ) ? 1 : 0; angle -= tokIs( src, toks[k], ">" ) ? 1 : 0; } - return angle != 0; + return angle > 0; } // The argument's tokens are one or more adjacent string literals → their concatenated content; else empty diff --git a/src/mcp.h b/src/mcp.h index 39ee9db2a..d1aaed214 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1334,7 +1334,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // paraphrased — this stanza used to describe an ordering the selector does not use. "{\"name\":\"exemplar\",\"description\":\"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. " + std::string( kExemplarSelectionRule ) + ". Beats grep/find_symbol: those find A definition, this ranks every definition and returns the one worth copying. kind = fn|method|class|struct|iface|var, OR a task string (its top match's kind is used).\"," + mcprefuse::toolMetadataFor( "exemplar", pathIsRequired ) + "}," - "{\"name\":\"quality_delta\",\"description\":\"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs baseline, across 12 kinds (complexity, verbosity, nesting, params, new duplication, new dead code, new public API surface, error-masking, short-horizon churn, new clone of a reused helper, an added stub or TODO, and defect-shape: a format/argument mismatch, a UTF-8-unsafe cut, a keep-first dedup, a vacuous test assertion). new-symbol rows never gate, except defect-shape format-arity. Read-only; auto-compares vs git HEAD with no setup, or a pinned .ripwire_quality_baseline sidecar if present and not stale (baseline says which). sev=minor does not gate; acked findings stay suppressed until one worsens. at= is the commit compared at (+dirty = the working tree differed).\"," + "{\"name\":\"quality_delta\",\"description\":\"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs baseline, across 12 kinds (complexity, verbosity, nesting, params, new duplication, new dead code, new public API surface, error-masking, short-horizon churn, new clone of a reused helper, an added stub or TODO, a defect shape). new-symbol rows never gate, except defect-shape format-arity. Read-only; auto-compares vs git HEAD with no setup, or a pinned .ripwire_quality_baseline sidecar if present and not stale (baseline says which). sev=minor does not gate; acked findings stay suppressed until one worsens. at= is the commit compared at (+dirty = the working tree differed).\"," + mcprefuse::toolMetadataFor( "quality_delta", pathIsRequired ) + "}," "{\"name\":\"quality_baseline\",\"description\":\"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor instead of HEAD. Side-effecting (writes a file) — skip it for a simple 'before I push' check, quality_delta already compares vs HEAD with zero setup.\"," + mcprefuse::toolMetadataFor( "quality_baseline", pathIsRequired ) + "}," diff --git a/src/quality.h b/src/quality.h index ee963a5c0..824b6e32d 100644 --- a/src/quality.h +++ b/src/quality.h @@ -3372,6 +3372,10 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // constexpr array, not a function, so this manifest mechanism // cannot hash IT — a future edit that only changes the built-in // NAME LIST (not these functions' text) will not trip this gate. +// defectSitesOf (quality.h) — the defect-shape site identity (anchor + facet + normalized text) +// the baseline stores. NOTE (disclosed limitation): the scanners it +// calls live in defectshape.h, outside this manifest, so a change +// to what a scanner reports must bump kQSnapCacheScheme by hand. // v4 (r27 P0.2) — the blob header gained the EXTRACTION IDENTITY (kIngestCacheVersionMirror + // kIngestParserVerMirror; see the long note at their declaration). Everything a Snapshot contains is a // function of tree-sitter extraction, so a parserVer bump must retire the blob — it did not, and 28c7d32's diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 446ca5553..7b3784864 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -533,15 +533,14 @@ inline constexpr const char* kQdLegendCore = "score). TWELVE KINDS, and kind= on every row names which one: complexity over the ccx bar, verbosity " "(LOC), nesting, params, duplication, dead-code, api-surface (new public contract drift), " "error-masking, short-horizon-churn, new-clone-of-reused-helper, placeholder (added stub/TODO), " - "defect-shape (a known defect shape, named by defect=). " + "defect-shape (defect= names it). " "THREE independent axes, in this " "order: (1) acked findings are suppressed entirely (acked= counts them); (2) ORIGIN — a finding on a " "symbol that EXISTED at the baseline is preexisting-worse (no origin attribute), one that exists only " "because the code is NEW carries origin=\"new-symbol\"; (3) MATERIALITY — a small numeric delta is " - "sev=\"minor\", and minor= counts them. EXIT 2 fires only on a row that is preexisting-worse AND major, or " - "a defect-shape format-arity row of any origin: the gating= count. new-symbol rows never gate, except " - "defect-shape format-arity, so exit 0 is NOT a verdict on the other new-symbol rows — nothing that " - "existed got worse, but the new debt is yours: read them. Clone kinds are new-symbol only when EVERY member is new; short-horizon-churn is " + "sev=\"minor\", and minor= counts them. EXIT 2 fires only on preexisting-worse AND major, or on " + "format-arity, the gating= count; new-symbol rows never gate, except defect-shape format-arity, so exit 0 " + "is NOT a verdict on them — nothing that existed got worse, but the new debt is yours: read them. Clone kinds are new-symbol only when EVERY member is new; short-horizon-churn is " "preexisting by construction. preexisting-worse= and new-symbol= partition regressions=. stale= is a " "FOURTH axis, never gating and never counted in regressions=: rows in the .ripwire_quality_acks ledger " "whose target no longer applies. " diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index 714e3e3c1..b4f369f88 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -256,9 +256,11 @@ US="$( run . --uses=selectBaseline --no-cache )" # to find an annotation naming it — one new call site, same helper. # 30 -> 31 (lane/fe-a-false-edges): FE-A's Go module census (graph.h collectGoModules) reads each go.mod above a Go file # through the canonical helper — one new call site, same helper. -[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 31 ] \ - && ok "repo: --uses=readWholeFile count=31 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ - || no "repo: --uses=readWholeFile expected 31" +# 31 -> 32 (lane/cr-qd-kinds-068): quality.h defectSitesOf reads each C++ / Python / Bash test-script file for the +# defect-shape scanners through the canonical helper — one new call site, same helper. +[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 32 ] \ + && ok "repo: --uses=readWholeFile count=32 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ + || no "repo: --uses=readWholeFile expected 32" [ "$( cnt "$( run . --callers=writeTally --no-cache )" )" = 1 ] \ && ok "repo: --callers=writeTally count=1 (was 0 — both template call sites are in writeDocDriftPage)" \ || no "repo: --callers=writeTally expected 1" diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh index 11d1508ab..58781c0bb 100644 --- a/test/defectshapecheck.sh +++ b/test/defectshapecheck.sh @@ -236,6 +236,8 @@ std::string formattonok( int a, int b ) { char out[16]; std::format_to_n( out, 1 std::string posnested( const std::string& s, int w ) { return std::format( "{}", s ); } std::string swapped( int a, int b ) { return std::format( "{} {}", a, b, b ) + std::format( "{}", a ); } std::string moved( int a ) { return std::format( "m={}", a, a ); } +std::string cmpinarg( int a, int b ) { return std::format( "{}", a > b ); } +std::string tmplarg( int a, int b ) { return std::format( "{}", b ); } std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } int drive() { return 0; } EOF @@ -287,6 +289,8 @@ void ostreamprint( int a ) { std::print( std::cout, "{} {}", a ); } std::string formattonok( int a, int b ) { char out[16]; std::format_to_n( out, 10, "{} {}", a, b ); return out; } std::string posnested( const std::string& s, int w ) { return std::format( "{0:>{1}}", s, w ); } std::string swapped( int a, int b ) { return std::format( "{} {}", a, b ) + std::format( "{}", a, b ); } +std::string cmpinarg( int a, int b ) { return std::format( "{}", a > b, a ); } +std::string tmplarg( int a, int b ) { return std::format( "{} {}", std::pair{ a, b }.first, b ); } std::string legacy( int a ) { return std::format( "{} {}", a, a, a ); } std::string freshbad( int a ) { return std::format( "{}", a, a ); } std::string freshok( int a ) { return std::format( "{} {}", a, a ); } @@ -309,6 +313,7 @@ ds_has "$L" format-arity fmtns gating ds_has "$L" format-arity localebad gating ds_has "$L" format-arity unrefpos gating ds_has "$L" format-arity swapped gating +ds_has "$L" format-arity cmpinarg gating ds_has "$L" format-arity freshbad gating-new ds_wasnow "$L" format-arity extra 0 1 ds_wasnow "$L" format-arity concat 0 1 @@ -338,11 +343,12 @@ ds_none "$L" unqualified "an unqualified in-repo format() is not the format fami ds_none "$L" ostreamprint "the std::print ostream overload is skipped, never guessed (stated floor)" ds_none "$L" formattonok "std::format_to_n puts the format at index 2: two fields, two arguments" ds_none "$L" posnested "a positional nested width {0:>{1}} references both arguments" +ds_none "$L" tmplarg "a template argument list (std::pair) the comma split would cut makes the count unknowable (skipped)" ds_none_p "$L" src/vamacro.h "a format inside a macro body (__VA_ARGS__) is skipped" ds_none "$L" moved "a mismatch moved out of this file is not new" ds_none "$L" movedRenamed "a mismatch moved to a new file and renamed is not new (its call text was already in the baseline)" ds_none_p "$L" src/moved.cpp "the moved mismatch's new file carries no row" -gating_split "$L" 13 +gating_split "$L" 14 OFMT="$QD_OUT" fx_run fmt_cpp if [ "$QD_OK" = 1 ] && [ "$OFMT" = "$QD_OUT" ]; then ok "$L: delta byte-identical run-to-run"; else no "$L: non-deterministic delta"; fi @@ -1334,7 +1340,7 @@ print("ROWS %d FRESH %d FRESHGATE %d TOP %s NG %d" % (len(rows), len(fresh), sum if [ "${1:-}" != ROWS ]; then no "$L $surf: probe: $V"; continue; fi n="$2"; fr="$4"; frg="$6"; top="$8"; ng="${10}" case "$n$fr$frg$ng" in *[!0-9]*) no "$L $surf: probe printed non-numbers: $V"; continue ;; esac - if [ "$n" -ge 13 ]; then ok "$L $surf: carries the $n format-arity rows (kind + defect keys)"; else no "$L $surf: carries $n format-arity rows, want >= 13"; fi + if [ "$n" -ge 14 ]; then ok "$L $surf: carries the $n format-arity rows (kind + defect keys)"; else no "$L $surf: carries $n format-arity rows, want >= 14"; fi if [ "$fr" = 1 ] && [ "$frg" = 1 ]; then ok "$L $surf: the new-symbol freshbad format-arity row carries \"gating\": true" else no "$L $surf: freshbad rows=$fr gating=$frg (want 1 and 1)"; fi if [ "$top" = "$ng" ]; then ok "$L $surf: top-level \"gating\" ($top) equals the rows that carry \"gating\": true" diff --git a/test/donelegendcheck.sh b/test/donelegendcheck.sh index 5ab33f1a3..2eaf1901b 100755 --- a/test/donelegendcheck.sh +++ b/test/donelegendcheck.sh @@ -165,7 +165,13 @@ EOF # this fixture against the pre-change binary: clean 2282 -> 2542, and the other three forms do not move (the # attribute rides only the auto-HEAD basis, and dirty/scope/refpair here do not take it). The pin is the next # multiple of 100 over the measured total, as every anchor in this file is. -run_budget qd_clean 2600 8574 "$FX" --quality-delta +# qd 2600/3900/5200/4300 -> 2700/4000/5300/4400 (2026-10-07, lane/cr-qd-kinds-068, the twelfth kind defect-shape): +# the ALWAYS-printed core gained the kind's name in the kind list and the one exception to "new-symbol rows never +# gate" (defect-shape format-arity gates on any origin), which every form must state because the exit code it +# explains is in every form. Measured on this fixture against the main binary (60dd3b3f): clean 2576 -> 2663, +# dirty 3889 -> 3976, scope 5163 -> 5250, refpair 4293 -> 4380 (+87 B each, the same sentence). Each pin is the +# next multiple of 100 over the measured total, as every anchor in this file is. +run_budget qd_clean 2700 8574 "$FX" --quality-delta printf '%s' "$DIRT" >> "$FX/src/base.cpp" # qd_dirty 3800 -> 3900 (2026-09-10, the string/perf round): the --quality-delta legend gained two facts a reader # needs to act on a row — the api-new-surface= count (one sentence, +105 B in every form, "printed even at zero" @@ -173,8 +179,8 @@ printf '%s' "$DIRT" >> "$FX/src/base.cpp" # against the pre-round binary: clean 2177 -> 2282, dirty 3642 -> 3855, scope 4916 -> 5129, refpair 4058 -> 4271. # Three forms still clear their ceilings; the dirty form's 3800 had 158 B of headroom and the two sentences cost # 213 there, so the ceiling moves by less than the growth (45 B of headroom left) — a ratchet, not an allowance. -run_budget qd_dirty 3900 8574 "$FX" --quality-delta -run_budget qd_dirty_scope 5200 10512 "$FX" --quality-delta "--scope=src/*" +run_budget qd_dirty 4000 8574 "$FX" --quality-delta +run_budget qd_dirty_scope 5300 10512 "$FX" --quality-delta "--scope=src/*" run_budget sd_uses 3800 4112 "$FX" --safe-delete=classifyWidth run_budget sd_none 3800 4112 "$FX" --safe-delete=tangle # tg_empty 1200 -> 1400 (2026-09-04, capture-audit wave-1 close, lane L4 M15): the zero-row root now carries @@ -196,7 +202,7 @@ run_budget tg_empty 1590 1332 "$FXC" --test-gate # own HEAD~1..HEAD: that range means a different diff after every landing, so a budget on it would be a # ratchet whose value depends on whoever committed last. ( cd "$FX" && git add -A >/dev/null 2>&1 && git commit -qm dirt >/dev/null 2>&1 ) -run_budget qd_refpair 4300 8574 "$FX" --quality-delta=HEAD~1..HEAD +run_budget qd_refpair 4400 8574 "$FX" --quality-delta=HEAD~1..HEAD echo echo "=== (b) the fraction, reported not asserted (see the header for why) ===============================" diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 24d6e1591..1f63f5810 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -284,7 +284,13 @@ tools = json.loads( line )[ "result" ][ "tools" ] # description, so each says "value uses (tables, args), not a proven call" (+70 B and +71 B, 141 B in all, no schema # byte moved, no tool added). Headroom after this line: 28 B (the final review tightened a first 46,800 anchor to the # #214-sized margin: 78 B of unattributed headroom was wider than any re-anchor here has taken). -CEILING = 46750 +# RE-ANCHORED 2026-10-07 (lane/cr-qd-kinds-068, the twelfth --quality-delta kind): 46,750 -> 46,800, measured 46,793 (from +# 46,732, 33 tools). The #214 precedent again: the clause describes the RESPONSE. quality_delta's description now +# counts 12 kinds (its "11 measured failure modes" became "12 kinds", -16 B), names the new kind ("a defect shape", +# +16 B), and states the one row family that gates on new code — "new-symbol rows never gate, except defect-shape +# format-arity" (+61 B) — because an MCP client reading "gating" would otherwise take a gating new-symbol row for a +# contradiction of the description. No schema byte moved, no tool added. Headroom after this line: 7 B. +CEILING = 46800 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 41c8cfe8b..44036c879 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 c74a677dde09eb76a71bfd0b9e2c75d074c47ecfac838654f84df80af6725b02 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 7efe3805d7058c9163ee4d74c7e9afe4158214c9ea0afa5585efc19858b38ee9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 5e82bb6e129da4086c1e64f5939be191964e5db6e7c49154edfee9d1cfdc44e6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 1571e38bf69376373754093689f55323e4b033b0e65b0ffef7e10e8f865a3844 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 373536ec8..7d67600f6 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -9fe75d7e33ccde42c2d7f3413786598e09784269d7378a520127681836283213 +2244e7390c2f69c695c9e1f51d39a2e296bcb8418b238c34c4ca89ccc6a55ef7 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 047629dbc..c0991de03 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,11 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-07, lane/cr-qd-kinds-068 (the defect-shape kind): RE-DERIVED with UPDATE_GOLDEN=1 (hash 2244e7390c…a55ef7). +# kQSnapCacheScheme 17 -> 18: the blob gained the defect-shape (anchor, site) records after publicApi (serializeSnapshot, +# deserializeSnapshot and computeSnapshot all moved), a BLOB SHAPE change. The manifest gains defectSitesOf (the site +# identity the baseline stores); the scanners it calls live in defectshape.h, outside the manifest — disclosed in the +# quality.h comment. kParserVer / kCacheVersion unchanged. The train may renumber the scheme. # 2026-10-04, train 26a (PR #373, Ruby method lookup, merged onto main 255dc199): RE-DERIVED ONCE on the merged tree with # UPDATE_GOLDEN=1 (hash 9fe75d7e33…283213). The only watched text that moved is the kParserVer declaration: 143 -> 148, # above the branch's 145 and the 145–147 other branch builds have used (148's full-use file tag is 149); quality.h's diff --git a/test/qsnapproducercheck.sh b/test/qsnapproducercheck.sh index ed30d0c4d..76fceb948 100755 --- a/test/qsnapproducercheck.sh +++ b/test/qsnapproducercheck.sh @@ -190,6 +190,7 @@ def parse(path, sha): dead = [struct.unpack_from(" Date: Wed, 7 Oct 2026 22:14:38 -0400 Subject: [PATCH 154/229] fix(for): never drop rows to pay for a next= that cannot make the answer fit; the unpaid clause spells no tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix round 1 of rv-knob-honesty-068. C3 (orchestrator ruling 2026-10-07, extending the owner's floor ruling; CLI and MCP alike): a charged continuation is paid for in rows ONLY where that makes the answer fit. SigsCutContinuation::payFromRows=false keeps the rows the cut alone leaves (no ledger gap, no payment) and reports the whole handle unpaid. - CLI XML: runForLens runs the paid pass first (the payable case is the run it always was); a paid run whose finished document still lands past its ceiling returns before writing a byte, and the lens re-runs unpaid (redaction tally restored). Only capped, charged, over-ceiling answers pay the second ranking. - --json: the same two passes inside emitForLensJson (rendering only). - MCP for: the assembly is one pass run once or twice over a single ranking; the UNPAID pass runs first and the paid one only when the answer the cut alone leaves fits budget_tokens; the paid one is kept only when it fits. budget_tokens=2000 over the 20 wrap-recipe queries: the 19 capped answers over their ceiling keep 255dc199's rows (dd6e4c8e: 1-2 fewer on all 19); the 20th is unchanged. C2: kForSigsUnpaidOverCeilingNote (was kForSigsFloorOverCeilingNote) is reworded without angle brackets — the literal tag inside the header legend satisfied "the block is intact" greps and moved sigsblock()'s span start into the comment on exactly the floor documents. The clause now covers both unpaid cases. N2: the next_offset= clause says nothing consumes it yet (the for offset pages files; next= re-runs the list). Gates: estchargecheck A7 twin re-based on the unpaid twin (@780) + a C3 arm (@790: the cut's own 5 rows, more than the paid @850 answer) + its payable near miss (@850, unchanged); the sweep control re-anchored on a one-row named task (C3 moved every capped document out of the late-label band); compactlegendcheck P4 twin text; forrankordercheck (10) MCP over-anyway @2000/@3500 and payable @4000; fordisclosurecheck #2c (the intact-block grep fails once the block is cut, sigsblock() starts at the real tag). estcalib for-budgeted re-pinned with tiktoken (o200k 488 / cl100k 483). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 25 +- src/mcpverbs.h | 451 +++++++++++++++++++++---------------- src/serialize.h | 69 +++++- src/verbs_for.h | 109 +++++++-- test/compactlegendcheck.sh | 19 +- test/estcalib.manifest | 2 +- test/estchargecheck.sh | 107 ++++++--- test/fordisclosurecheck.sh | 17 ++ test/forrankordercheck.sh | 27 +++ 9 files changed, 539 insertions(+), 287 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a21b6ab06..150fafb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,23 +26,30 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal [--pack-top-n=N] --token-budget=T`) — and, when rows were dropped, `next_offset=`: the candidate index the cut starts at (the last printed row's rank, so a slot that prints no row cannot skew it). `--json` carries `"sigs_next"` and `"sigs_next_offset"`. The MCP twin carries no CLI argv (an MCP client re-calls the tool): `next_budget_tokens=T` names - the `budget_tokens` a re-call needs, beside the same `next_offset=`. Present-only legend clauses define each. At the + the `budget_tokens` a re-call needs, beside the same `next_offset=`. Nothing consumes `next_offset=` yet + (`--for --offset=N` pages the file-grain list, a different one; `next=` re-runs the list), and its clause says so. + Present-only legend clauses define each. At the default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, they are exempt from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) - `next=` rides alone and is paid from the block's rows, and a compact answer is cut to the rows the full one keeps, so - the answer stays within the budget wherever it can fit; measured at `--token-budget=2000` on 20 queries, that costs one - row on 11 — a row the `next=` itself serves. At the rank 1..4 floor, where no row is left to pay, `next=` still ships - (the call that recovers a cut is never the thing cut) and, when the answer then lands past its ceiling, the root says - `over_ceiling="1"` with a clause naming the cause. No budget, ceiling or token conversion changed. + `next=` rides alone and is paid from the block's rows, and a capped compact answer is cut to the rows the full one + keeps, so the answer stays within the budget wherever it can fit; measured at `--token-budget=2000` on 20 queries, that + costs one row on 11 — a row the `next=` itself serves. Rows pay for it ONLY where that makes the answer fit: when the + answer lands past its ceiling paid or not (the rank 1..4 floor has nothing left to give, or the MCP `for` answer, whose + header overshoots `budget_tokens` on its own), no row is dropped for it — the answer keeps the rows the cut alone + leaves, `next=` still ships (the call that recovers a cut is never the thing cut), and the root says + `over_ceiling="1"`, with a clause naming the cause where the answer's own ceiling was otherwise met. Measured on MCP `for` + at `budget_tokens=2000` over 20 queries: the 19 capped answers over their ceiling keep every row they had before. + No budget, ceiling or token conversion changed. - **`--lint` / `--lint-rules` and `--sarif`.** A rule that spends its per-rule match budget (`count_capped="1"`) now makes - the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`): only the floored rules, under a 10x budget, - through the new `--lint-max-per-rule=N` (the default stays 5000). A re-run still floored names its own. + the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`, which keeps `--sarif`): only the floored + rules, under a 10x budget, through the new `--lint-max-per-rule=N` (sets each rule's budget either way; the default + stays 5000). A re-run still floored names its own. - **`--impact`.** A cut import tier dropped `importers_next=` when `--impact=SYM --limit=N` passed 120 bytes, so a long selector lost its only continuation with no marker. It is now always emitted in full, like every other `next=`. Also: the `ripwire-handoff` skill passed `--top-k=20` to `--for`, which does not read it; it now spells `--signatures-only`. Gates: `forrankordercheck` (9), `lintbudgetcheck` (5), `impactimportcheck` (#9d), `estchargecheck` -(#11 A7 floor twin), `compactlegendcheck` (P4 floor twin). +(#11 A7 unpaid twin), `compactlegendcheck` (P4 floor twin), `forrankordercheck` (10), `fordisclosurecheck` (#2c). ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index d58d38cb3..894220dbf 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2019,13 +2019,6 @@ inline std::optional forTaskText( const std::string& root, const st } } - rw::MemoryStream stream; - std::FILE* const mem = stream.open(); - if( !mem ) - { - return std::nullopt; // the answer buffer could not be opened: an internal error, never "not found" - } - // G4: task and rc.reason are agent-controlled and land verbatim in an XML comment below — a "-->" run // would close the comment early and inject XML, and any "--" run alone breaks strict xmllint parsing. // W3FIX M3: the dash collapse was the ONLY scrub here, so an agent-supplied C0 byte, invalid UTF-8 @@ -2188,223 +2181,285 @@ inline std::optional forTaskText( const std::string& root, const st // knob-honesty-068: the CLI twin's continuation (serialize.h SigsCutContinuation). This surface is signatures-only, // so its sig side IS the ceiling: charged under a caller's budget_tokens, exempt at the default. NO CLI argv here — an // MCP client continues by re-calling the tool: the machine form next_budget_tokens=T (re-call `for` with budget_tokens=T) - // plus next_offset=, the resume index. - const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, - /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false }; + // plus next_offset=, the resume index. Built per assembly pass below (payFromRows: orchestrator ruling C3). // L3: field-notes surfacing — parity with the CLI --for lens. loadNoteIndex reads root/.ripwire_notes (a // small file); nullptr when EMPTY so the bundle stays byte-identical when there is nothing to surface. const notes::NoteIndex noteIndex = notes::loadNoteIndex( root ); const notes::NoteIndex* const notesPtr = noteIndex.empty() ? nullptr : ¬eIndex; - // A2 (survey card, 2026-09-03): sigs render into their OWN buffer (rather than streaming straight into - // `mem` the way this call used to) so droppedPositive is known BEFORE headerStr is written — headerStr, - // once flushed to `mem`, cannot be edited retroactively (the same reason the CLI twin's degrade path - // never gets the attribute). Byte-for-byte the same content this call always produced. - std::size_t mcpDroppedPositive = 0; - bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below - rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does - mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation - std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface - std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) - { - packSignatures( m2, ing, lensRank, forTopN, 0 /* no byte budget in MCP (0 = unlimited) */, true, &fanIn, &impure, redact, - nullptr, nullptr, nullptr, nullptr, // Q3 lens vectors — the MCP verb has no git/clone pass (as before) - /*rankAdaptivePayload=*/true, // B0.3: same rank-adaptive payload rule as the CLI --for lens - sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) - notesPtr, // L3: field-notes surfacing (inert when null) - flRootArg, // R-E: root-relative p=, same argument the CLI twin passes - /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad - &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) - &mcpShownIds, // lane 2: see verbs_for.h shownSigIds - &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below - mcpTopRowNext, // L-W: the widening page on a thin answer, else the body - &mcpSigsCut ); // cut-fix lane A: docs_dropped= / shrunk readings; - // knob-honesty-068: carries mcpSigsNext IN - } ); - // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) - // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare - // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between - // the two dialects, the way every other --for header fragment in this function already is. - if( mcpDroppedPositive > 0 ) + // knob-honesty-068 (orchestrator ruling C3, 2026-10-07): everything from the sigs render to the finished document is ONE + // ASSEMBLY PASS, run once or twice — the ranking above runs once. The pass takes its own copies of what it edits (the header, + // the lego/compose strings, the lego scope and its pre-cap count), so a second pass starts from the same state as the first. + // It returns the document UNPRICED (est_tokens= / over_ceiling= are spliced once, on the chosen one, below). + struct McpForAssembly { - const std::size_t closeAt = headerStr.rfind( " -->" ); - if( closeAt != std::string::npos ) - { - char nb[ 40 ]; - rw::formatTo( nb, sizeof( nb ), " dropped_positive=\"{}\"", mcpDroppedPositive ); - headerStr.insert( closeAt, nb ); // else: unexpected shape, header left as-is + std::optional doc; // nullopt: the answer buffer lost bytes (internal error) + rw::SigsCutReport cut; + }; + const auto assembleMcpFor = [ & ]( bool payFromRows, std::string headerStr, std::string legoStr, std::string composeStr, + std::vector> legoScoped, std::size_t legoPreCapCount ) -> McpForAssembly + { + rw::MemoryStream stream; + std::FILE* const mem = stream.open(); + if( !mem ) + { + return McpForAssembly{}; // the answer buffer could not be opened: an internal error, never "not found" + } + const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, + /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false, + /*ledgerGapBytes=*/0u, payFromRows }; + // A2 (survey card, 2026-09-03): sigs render into their OWN buffer (rather than streaming straight into + // `mem` the way this call used to) so droppedPositive is known BEFORE headerStr is written — headerStr, + // once flushed to `mem`, cannot be edited retroactively (the same reason the CLI twin's degrade path + // never gets the attribute). Byte-for-byte the same content this call always produced. + std::size_t mcpDroppedPositive = 0; + bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below + rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does + mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation + std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface + std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) + { + packSignatures( m2, ing, lensRank, forTopN, 0 /* no byte budget in MCP (0 = unlimited) */, true, &fanIn, &impure, redact, + nullptr, nullptr, nullptr, nullptr, // Q3 lens vectors — the MCP verb has no git/clone pass (as before) + /*rankAdaptivePayload=*/true, // B0.3: same rank-adaptive payload rule as the CLI --for lens + sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) + notesPtr, // L3: field-notes surfacing (inert when null) + flRootArg, // R-E: root-relative p=, same argument the CLI twin passes + /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad + &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) + &mcpShownIds, // lane 2: see verbs_for.h shownSigIds + &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below + mcpTopRowNext, // L-W: the widening page on a thin answer, else the body + &mcpSigsCut ); // cut-fix lane A: docs_dropped= / shrunk readings; + // knob-honesty-068: carries mcpSigsNext IN + } ); + // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) + // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare + // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between + // the two dialects, the way every other --for header fragment in this function already is. + if( mcpDroppedPositive > 0 ) + { + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + char nb[ 40 ]; + rw::formatTo( nb, sizeof( nb ), " dropped_positive=\"{}\"", mcpDroppedPositive ); + headerStr.insert( closeAt, nb ); // else: unexpected shape, header left as-is + } } - } - // budget_bytes= — the CLI twin's disclosure (verbs_for.h, where the full argument lives), on this - // surface for the §P8 reason every other fragment in this function is: one element name, one attribute - // order, both surfaces. This dialect is budgeted by DEFAULT exactly as the CLI is (forBudgetBytes above - // is kForPayloadBudgetBytes when the caller passed no budget_tokens), and it disclosed the cut - // () without naming what did the cutting. Same two conditions as the CLI: the ladder - // actually fired, and the caller named no ceiling of their own — so exactly one ceiling rides a trimmed - // bundle. Same clause text, so the two dialects say the same sentence. Spliced AFTER the sigs render, - // like dropped_positive= above, so sigsBudget and therefore the served row set are untouched; est_tokens - // is spliced onto the finished document further down and so prices these bytes. - if( mcpSigsCapped && budgetTokens == 0 ) - { - const std::size_t rootCloseAt = headerStr.find( ">" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, rw::kForBudgetBytesNote ); + } } - const std::size_t closeAt = headerStr.rfind( " -->" ); - if( closeAt != std::string::npos ) + // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same + // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). + if( const std::string cutNotes = rw::sigsCutReportLegend( mcpSigsCut ); + !cutNotes.empty() ) { - headerStr.insert( closeAt, rw::kForBudgetBytesNote ); + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, cutNotes ); + } } - } - // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same - // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). - if( const std::string cutNotes = rw::sigsCutReportLegend( mcpSigsCut ); - !cutNotes.empty() ) - { - const std::size_t closeAt = headerStr.rfind( " -->" ); - if( closeAt != std::string::npos ) + // L3 follow-up (CodeRabbit 4053600616): same splice shape as dropped_positive=/budget_bytes= above — absent + // entirely on a clean read. `noteIndex.degraded` is read here (not through notesPtr, which is nulled on an + // EMPTY index even when the read itself was not clean — see notesPtr's own comment above). + if( noteIndex.degraded ) { - headerStr.insert( closeAt, cutNotes ); + const std::size_t rootCloseAt = headerStr.find( ">" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, " [" + std::string( rw::notes::kNotesDegradedReading ) + "]" ); + } } - } - // L3 follow-up (CodeRabbit 4053600616): same splice shape as dropped_positive=/budget_bytes= above — absent - // entirely on a clean read. `noteIndex.degraded` is read here (not through notesPtr, which is nulled on an - // EMPTY index even when the read itself was not clean — see notesPtr's own comment above). - if( noteIndex.degraded ) - { - const std::size_t rootCloseAt = headerStr.find( ">" ); - if( closeAt != std::string::npos ) + std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); + std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); + std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); + std::fwrite( routeStr.data(), 1, routeStr.size(), mem ); // B6.3 + // DEEP-TAIL d2, MCP twin: the same shared walk + renderer the CLI --for uses (serialize.h), from the + // same resolved surface — the fixed default budget regime, so the tail rides on top (default row cap) + // and the ranked head above stays byte-identical to a tail-less bundle. { - headerStr.insert( closeAt, " [" + std::string( rw::notes::kNotesDegradedReading ) + "]" ); + std::vector tailEsc; + const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), + kForFileTailShownCap, tailEsc ); + std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); } - } - std::fwrite( headerStr.data(), 1, headerStr.size(), mem ); - // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule - // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this - // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver - // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. - if( !mcpForHdrRows.empty() ) + rw::emitRaw( mem, "" ); + std::optional answer = mcpAnswerText( stream ); + if( !answer ) + { + return McpForAssembly{}; // the buffer lost bytes: the same internal error as the failed open above + } + std::string out = std::move( *answer ); + // R2-L2p: the legend clause disclosing a stubbed lego/compose section (kForSectionStubLegend) — the + // whole contract lives in spliceForSectionStubLegend above; `stubbed` false (the overwhelming common + // path) returns immediately and costs this function nothing but the call. Done BEFORE priceForTaskRoot + // so the clause's bytes are part of the est_tokens price exactly as the CLI's own version is. + spliceForSectionStubLegend( out, mcpSectionsWillStub ); + // F5 (terminality round A 2026-09-05): PRICE the bundle instead of declaring it unpriced. The document is + // complete here, so this is the same measurement the CLI twin makes over its own (deliberately different) + // bytes: pricedRootAttr's ≤4-pass fixpoint at kBytesPerTokenDefault, spliced onto the root by the + // shared splicer — no second estimator, and no number that could disagree with the CLI's for the same + // reason the CLI's could disagree with itself. Charged AFTER the sigs budget on purpose: a disclosure's + // contract is disclosure only, and charging its ~18 bytes against the ranked head would drop a row to pay + // for the attribute that describes the head — the same exemption mcpConfidenceExemptBytes already makes. + // r2-LO: a completeness attribute this bundle carries and its own legend never spells (at=, ccx=, next=) gets its compact + // reading — BEFORE pricing, so est_tokens= prices the bytes served, and after the sigs budget, so the rows are unchanged. + closeRosterGaps( out ); + mcpSigsCut.continuationRequest = nullptr; // it pointed at this pass's mcpSigsNext, which ends here + return McpForAssembly{ std::move( out ), mcpSigsCut }; + }; + // C3 — pay for the handle in rows ONLY where that makes the answer fit. This surface overshoots budget_tokens + // structurally (the header bytes its ledger exempts), so a paid answer is nearly always over anyway; the UNPAID pass runs + // first: the rows the cut alone leaves, the handle riding unpaid. Only when the answer the cut alone left — that document + // minus the handle's bytes, priced by the same rule priceForTaskRoot applies — fits budget_tokens can paying matter, and only + // then does the paid pass run; it is kept when it fits, else the unpaid one ships (over_ceiling="1" either way). + const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; + McpForAssembly chosen = assembleMcpFor( /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + if( !chosen.doc ) { - const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string - { - return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); - }; - std::vector mcpHdrEsc; - std::string mcpHdrXml; - for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) - { - // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver - // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - mcpHdrXml += ""; - } - std::fwrite( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); + return std::nullopt; } - // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs - // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr - // directly now rather than re-slicing it back out of the flushed memstream buffer. - if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr ) ) + const auto estOf = []( std::size_t docBytes ) { - legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount ); } ); // R-R: the re-render dropped the root its first render (above) passed - } - // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution - // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= - // is already CLI-flag-shaped everywhere else, e.g. the r=1 row's --expand=FILE:NAME, so the two - // surfaces do not need two spellings), and the SAME shared size gate (rw::priceSectionStub, - // serialize.h). This surface has no buffered/degrade-path split (renderToString always renders whole), - // so hasRenderedBytes is always true here and the size gate always has a true length to price. - // - // R2-L2p (independent review, 2026-09-19): mcpLegoWillStub/mcpComposeWillStub decide the SAME collapse - // the CLI twin discloses via kForSectionStubLegend (verbs_for.h finishForLensHeaderPriced), but this - // block used to just swap legoStr/composeStr for their stubs and never carried that clause anywhere — - // an MCP caller got a stubbed section with no legend explaining the cut. mcpSectionsWillStub survives - // this block's scope so the splice below (after `out` is assembled, mirroring priceForTaskRoot's own - // late-insertion technique for kOverCeilingLegend) knows whether to add it. - bool mcpSectionsWillStub = false; - { - const bool mcpWantLego = sectionsWant( sections, "lego" ); - const bool mcpWantCompose = sectionsWant( sections, "compose" ); - const bool mcpLegoCandidate = !legoStr.empty() && !mcpWantLego; - const bool mcpComposeCandidate = !composeStr.empty() && !mcpWantCompose; - if( mcpLegoCandidate || mcpComposeCandidate ) - { - std::string sectionsNextMcp = nextFlag( "--for=", task ); - sectionsNextMcp += ' '; - sectionsNextMcp += nextFlag( "--sections=", "lego,compose" ); - const SectionStubPricing legoPricing = mcpLegoCandidate - ? priceSectionStub( "lego", legoPreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, legoStr.size() ) - : SectionStubPricing{}; - const SectionStubPricing composePricing = mcpComposeCandidate - ? priceSectionStub( "compose", composePreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, composeStr.size() ) - : SectionStubPricing{}; - const bool mcpLegoWillStub = mcpLegoCandidate && legoPricing.collapse; - const bool mcpComposeWillStub = mcpComposeCandidate && composePricing.collapse; - // postcondition of the rule itself, checked BEFORE the swap below discards the original size: - // whichever section actually collapses is, by construction, smaller than what it replaced. - ENSURES( !mcpLegoWillStub || legoPricing.stubXml.size() < legoStr.size(), - "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); - ENSURES( !mcpComposeWillStub || composePricing.stubXml.size() < composeStr.size(), - "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); - if( mcpLegoWillStub ) + std::size_t est = 0; + (void)rw::pricedRootAttr( docBytes, rw::kBytesPerTokenDefault, 0, &est ); + return est; + }; + if( budgetTokens > 0 && chosen.cut.hasContinuation ) + { + const std::size_t handleBytes = rw::sigsCutContinuationBytes( chosen.cut ); + ASSUME( handleBytes <= chosen.doc->size() ); + if( estOf( chosen.doc->size() - handleBytes ) <= budgetTokens ) + { + const std::optional tallyUnpaid = redact != nullptr ? std::optional( *redact ) : std::nullopt; + if( tallyAtEntry ) { - legoStr = legoPricing.stubXml; + *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served } - if( mcpComposeWillStub ) + McpForAssembly paid = assembleMcpFor( /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + if( paid.doc && estOf( paid.doc->size() ) <= budgetTokens ) { - composeStr = composePricing.stubXml; + chosen = std::move( paid ); + } + else if( tallyUnpaid ) + { + *redact = *tallyUnpaid; } - // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` - // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. - mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; } } - std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); - std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); - std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); - std::fwrite( routeStr.data(), 1, routeStr.size(), mem ); // B6.3 - // DEEP-TAIL d2, MCP twin: the same shared walk + renderer the CLI --for uses (serialize.h), from the - // same resolved surface — the fixed default budget regime, so the tail rides on top (default row cap) - // and the ranked head above stays byte-identical to a tail-less bundle. - { - std::vector tailEsc; - const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), - kForFileTailShownCap, tailEsc ); - std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); - } - rw::emitRaw( mem, "" ); - std::optional answer = mcpAnswerText( stream ); - if( !answer ) - { - return std::nullopt; // the buffer lost bytes: the same internal error as the failed open above - } - std::string out = std::move( *answer ); - // R2-L2p: the legend clause disclosing a stubbed lego/compose section (kForSectionStubLegend) — the - // whole contract lives in spliceForSectionStubLegend above; `stubbed` false (the overwhelming common - // path) returns immediately and costs this function nothing but the call. Done BEFORE priceForTaskRoot - // so the clause's bytes are part of the est_tokens price exactly as the CLI's own version is. - spliceForSectionStubLegend( out, mcpSectionsWillStub ); - // F5 (terminality round A 2026-09-05): PRICE the bundle instead of declaring it unpriced. The document is - // complete here, so this is the same measurement the CLI twin makes over its own (deliberately different) - // bytes: pricedRootAttr's ≤4-pass fixpoint at kBytesPerTokenDefault, spliced onto the root by the - // shared splicer — no second estimator, and no number that could disagree with the CLI's for the same - // reason the CLI's could disagree with itself. Charged AFTER the sigs budget on purpose: a disclosure's - // contract is disclosure only, and charging its ~18 bytes against the ranked head would drop a row to pay - // for the attribute that describes the head — the same exemption mcpConfidenceExemptBytes already makes. - // r2-LO: a completeness attribute this bundle carries and its own legend never spells (at=, ccx=, next=) gets its compact - // reading — BEFORE pricing, so est_tokens= prices the bytes served, and after the sigs budget, so the rows are unchanged. - closeRosterGaps( out ); + std::string out = std::move( *chosen.doc ); priceForTaskRoot( out, budgetTokens ); // R1: est_tokens=, and over_ceiling="1" when it exceeds budgetTokens return out; } diff --git a/src/serialize.h b/src/serialize.h index 30081a772..1d0d93366 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -871,14 +871,22 @@ static_assert( kForDocExcerptRankCount == 24 && kForDocFullRankCount == 12, inline constexpr std::string_view kForSigsNextNote = " [sigs next=: one call serving all total= rows uncut]"; inline constexpr std::string_view kForSigsNextBudgetNote = " [sigs next_budget_tokens=: call this tool again with that budget_tokens to get all total= rows uncut]"; +// next_offset= is the resume index #362's budgeted candidate page will consume; NOTHING consumes it today — the for lens's own +// offset= pages the FILE-grain widening list, a different list — so the clause says so rather than implying a resume exists +// (rv-knob-honesty-068 N2). No consumer is added in this lane. inline constexpr std::string_view kForSigsNextOffsetNote = - " [sigs next_offset=: the candidate index the cut starts at; a resume must match the root at=]"; -// knob-honesty-068 (orchestrator ruling 2026-10-07, PROCESS rule 5: the recovery handle ALWAYS ships): a charged at its -// rank 1..4 floor cannot give up the bytes of its next=, so the handle rides unpaid; when the document then lands past its -// ceiling, over_ceiling="1" says so and this clause says why. Not "est_tokens exceeds budget_tokens": on a --detail bundle the -// bodies price at the body rate, so the byte allowance (budget_tokens x 2.36 x 1.15) can be the ceiling crossed. -inline constexpr std::string_view kForSigsFloorOverCeilingNote = - " [over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]"; + " [sigs next_offset=: the candidate index the cut starts at; no consumer yet (the for offset pages files; next= re-runs the list)]"; +// knob-honesty-068 (orchestrator rulings 2026-10-07, PROCESS rule 5: the recovery handle ALWAYS ships): a charged pays +// for its next= in rows ONLY where that makes the answer fit. Where it cannot — the rank 1..4 floor has nothing left to give, +// or the answer lands past its ceiling paid or not (dropping rows does not make it fit) — no row is dropped for it: the +// handle rides unpaid on the rows the cut alone left, and when the document lands past its ceiling over_ceiling="1" says so +// and this clause says why. Not "est_tokens exceeds budget_tokens": on a --detail bundle the bodies price at the body rate, +// so the byte allowance (budget_tokens x 2.36 x 1.15) can be the ceiling crossed. +// NO ANGLE BRACKETS (CHECKLIST 17): the clause rides the header legend, BEFORE the real tag, so a literal sigs tag spelled +// here satisfied every "the sigs block is intact" grep and moved sigsblock()'s span start into this comment on exactly the +// documents that carry it (rv-knob-honesty-068 C2). +inline constexpr std::string_view kForSigsUnpaidOverCeilingNote = + " [over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]"; // The clauses a cut report owes, concatenated in a fixed order ("" when it owes none) — defined below SigsCutReport // (sigsCutReportLegend), because the continuation's clauses read the report itself. @@ -4493,8 +4501,9 @@ struct SigsCutReport bool hasContinuation = false; // the caller asked for the continuation (next=/next_budget_tokens=/next_offset=) bool terseContinuation = false; // a CHARGED pasteable one under a hard ceiling: next= alone (no next_offset=, no // clause — the bundle's own next= reading defines it); see SigsCutContinuation - std::size_t continuationUnpaidBytes = 0; // a CHARGED one's bytes the rank 1..4 floor could not give up: it rides anyway - // (the recovery handle always ships) and the caller labels any overshoot + std::size_t continuationUnpaidBytes = 0; // a CHARGED one's bytes no row paid for — the rank 1..4 floor could not give them up, + // or the caller forbade the payment (payFromRows=false): it rides anyway (the recovery + // handle always ships) and the caller labels any overshoot }; // ── THE CUT'S CONTINUATION (knob-honesty-068; PROCESS rule 5: every cut disclosed AND recoverable via next=) ────────── @@ -4522,6 +4531,9 @@ struct SigsCutReport // one fitted (compactlegendcheck P4), and the rungs cannot shed a clause that is the cut's only disclosure. The e= // ruling's shape: under a tight ceiling the extras go, the one thing that must stay (here the call) stays and is paid // for. MCP keeps its clause (next_budget_tokens= has no other reading). +// …PAID ONLY WHERE PAYING MAKES THE ANSWER FIT (orchestrator ruling C3, 2026-10-07, CLI and MCP alike): an answer that +// lands past its ceiling paid or not loses rows for nothing, so its caller re-renders with payFromRows=false — the rows +// the cut alone leaves, the whole handle unpaid, over_ceiling="1" on the root (kForSigsUnpaidOverCeilingNote). // THE RESUME INDEX, next_offset= (present when rows were DROPPED, not only shrunk): the candidate index the cut starts at — // the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no // row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation @@ -4546,6 +4558,10 @@ struct SigsCutContinuation std::size_t ledgerGapBytes = 0; // a CHARGED one's plan also reserves this: the sig room the caller's dialect was given beyond // what its header honestly costs (the --for compact dialect, verbs_for.h compactLedgerGapBytes), // so a capped compact block is cut to the rows the full one keeps + bool payFromRows = true; // a CHARGED one: pay for it in rows (the plan's ledger gap + paySigsContinuationFromRows). + // false = the caller found that paying cannot make its answer fit (over its ceiling either + // way; orchestrator ruling C3, 2026-10-07): the block keeps the rows the cut alone leaves — + // no ledger gap, no payment — and the whole continuation rides unpaid (continuationUnpaidBytes) }; inline std::size_t sigsCutNextTokens( std::size_t blockBytes, std::size_t fixedBytes ) noexcept @@ -4692,6 +4708,21 @@ inline std::string sigsCutReportLegend( const SigsCutReport& cut ) } return notes; } +// The bytes the continuation adds to the tag + the header legend (attributes + clauses), measured as the difference against the +// same report with the continuation stripped — so a caller can price the answer the cut alone would have left (ruling C3). +inline std::string sigsOpenTag( const SigsCutReport& cut ); +inline std::size_t sigsCutContinuationBytes( const SigsCutReport& cut ) +{ + SigsCutReport bare = cut; + bare.next = std::string(); + bare.nextBudgetTokens = 0; + bare.hasContinuation = false; + bare.terseContinuation = false; + const std::size_t with = sigsOpenTag( cut ).size() + sigsCutReportLegend( cut ).size(); + const std::size_t without = sigsOpenTag( bare ).size() + sigsCutReportLegend( bare ).size(); + ENSURES( with >= without, "a continuation only adds bytes" ); + return with - without; +} // the same clauses from the four numbers a caller without a continuation holds (--from-trace, --pack-task) inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped ) { @@ -4720,14 +4751,20 @@ inline std::size_t sigsCutContinuationReserve( const SigsCutContinuation* req, s } // The plan-side share of a CHARGED continuation: the dialect's ledger gap (SigsCutContinuation::ledgerGapBytes). It rides the // marker's reservation, so it is spent only when the ladder fires — an uncapped block is untouched. +inline bool sigsCutPaysFromRows( const SigsCutContinuation* req ) noexcept +{ + return req != nullptr && req->charged && req->payFromRows; +} inline std::size_t sigsCutPlanReserve( const SigsCutContinuation* req ) noexcept { - return ( req != nullptr && req->charged ) ? req->ledgerGapBytes : 0u; + return sigsCutPaysFromRows( req ) ? req->ledgerGapBytes : 0u; } // A CHARGED continuation is paid FROM THE ROWS, byte for byte: the same ladder runs on past the cut until the block has given // up `cost` bytes, so block + continuation never exceeds the block the cut alone left — in either dialect, whatever the plan's // slack (reserving it in the plan let that slack absorb it and the bundle grow). Returns the bytes the rank 1..4 floor could // NOT give up (0 = paid in full); the continuation rides regardless (orchestrator ruling 2026-10-07) and the caller labels it. +// Called only where the caller lets rows pay (sigsCutPaysFromRows): a caller whose answer lands past its ceiling paid or not +// re-renders with payFromRows=false (ruling C3), so no row is dropped for a handle that cannot make the answer fit. template inline std::size_t paySigsContinuationFromRows( std::vector& entries, std::vector& files, std::size_t& total, std::size_t cost, CostFn entryCost ) @@ -5163,7 +5200,11 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: } // knob-honesty-068: a CHARGED continuation is paid from the rows (paySigsContinuationFromRows); what the floor // cannot pay rides anyway and is reported for the caller's over_ceiling label - const std::size_t cutNextUnpaid = paySigsContinuationFromRows( entries, sigFiles, total, plan.capped ? cutNextCost : 0u, entryCost ); + // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows + const std::size_t cutNextOwed = plan.capped ? cutNextCost : 0u; + const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) + ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, entryCost ) + : cutNextOwed; // A2: the exact count, computed AFTER the ladder has made its final drop decisions (droppedPositiveCount // above — the shared arithmetic with the JSON sibling). Pure bookkeeping, no output bytes either way. @@ -9592,7 +9633,11 @@ inline void packSignaturesJson( std::FILE* out, const IngestResult& ing, const s } // knob-honesty-068: a charged continuation is paid from the rows (the XML twin's rule); the floor's unpaid rest rides anyway // and this dialect's own predicate (forLensJsonOverCeiling: bytes past the allowance) labels any overshoot - const std::size_t cutNextUnpaid = paySigsContinuationFromRows( entries, sigFiles, total, plan.capped ? cutNextCost : 0u, jsonSigEntryCost ); + // …only where the caller lets rows pay (payFromRows): otherwise the whole of it rides unpaid on the cut's own rows + const std::size_t cutNextOwed = plan.capped ? cutNextCost : 0u; + const std::size_t cutNextUnpaid = sigsCutPaysFromRows( cutNext ) + ? paySigsContinuationFromRows( entries, sigFiles, total, cutNextOwed, jsonSigEntryCost ) + : cutNextOwed; if( outCapped ) { *outCapped = plan.ladderFires; // the LADDER's verdict (the budget_bytes= stanza names its ceiling); cutOut carries capped diff --git a/src/verbs_for.h b/src/verbs_for.h index 398112bab..c3c6a10cd 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1287,11 +1287,12 @@ struct ForLensRootFinish std::size_t tokenBudget = 0; int maxTokens = 0; bool bodyCeiling = false; - // knob-honesty-068 (orchestrator ruling 2026-10-07): a charged continuation rode unpaid from the rank 1..4 floor - // (SigsCutReport::continuationUnpaidBytes) AND the document lands past its ceiling — byte allowance or budget_tokens. + // knob-honesty-068 (orchestrator rulings 2026-10-07): a charged continuation rode unpaid — the rank 1..4 floor could + // not give its bytes up, or no row was dropped for it because the answer lands past its ceiling paid or not (ruling C3) — + // (SigsCutReport::continuationUnpaidBytes) AND the document lands past its ceiling: byte allowance or budget_tokens. // Decided by the caller on the finished document (a value, never a search of the text); labels it over_ceiling="1" - // with kForSigsFloorOverCeilingNote, which is true where kOverCeilingLegend's est_tokens reading may not be. - bool sigsFloorOver = false; + // with kForSigsUnpaidOverCeilingNote, which is true where kOverCeilingLegend's est_tokens reading may not be. + bool sigsUnpaidOver = false; }; // PR #215 review: the finished header AND the number it prints, handed back together. finishForLensHeader @@ -1376,12 +1377,12 @@ inline ForLensPricedHeader finishForLensHeaderPriced( std::string header, const // adding bytes only RAISES est_tokens — so one extra stage is exact and the flag never oscillates. The // definition rides the legend of the document that carries the attribute. const bool namedCeilingOver = forLensOverCeiling( f.lastRungFired, f.tokenBudget, f.maxTokens, f.bodyCeiling, priced.first ); - if( namedCeilingOver || f.sigsFloorOver ) + if( namedCeilingOver || f.sigsUnpaidOver ) { // each clause rides only where its reading is true: the est_tokens/last-rung one when that predicate holds, the - // floor one when the unpaid recovery handle is what crossed (knob-honesty-068) — both when both hold + // unpaid one when the recovery handle rode unpaid on an answer past its ceiling (knob-honesty-068) — both when both hold spliceBefore( header, " -->", /*fromEnd=*/true, namedCeilingOver ? f.overCeilingLegend : std::string_view() ); - spliceBefore( header, " -->", /*fromEnd=*/true, f.sigsFloorOver ? rw::kForSigsFloorOverCeilingNote : std::string_view() ); + spliceBefore( header, " -->", /*fromEnd=*/true, f.sigsUnpaidOver ? rw::kForSigsUnpaidOverCeilingNote : std::string_view() ); overAttr = " over_ceiling=\"1\""; priced = priceFixpoint( header.size() + f.nonHeaderMarkupBytes, overAttr.size() ); } @@ -1422,7 +1423,11 @@ inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t return stanza; } -inline int emitForLensJson( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) +// knob-honesty-068 (orchestrator ruling C3, 2026-10-07): one RENDER of the --json bundle. `sigsNextPayFromRows` is the charged +// "sigs_next" payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextRetryUnpaid` set, a paid render whose +// bundle still lands past its ceiling writes NOTHING and asks for the unpaid one (emitForLensJson below). +inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in, bool sigsNextPayFromRows, + bool* sigsNextRetryUnpaid ) { using namespace rw; @@ -1466,7 +1471,8 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For const std::size_t sigsBudget = bundleBudget > fixedBytes ? bundleBudget - fixedBytes : 1; // knob-honesty-068: the cut's continuation. This dialect serves no bodies, so its sig side IS the ceiling: charged under // an explicit --token-budget, exempt at the default (the XML twin's rule, SigsCutContinuation) - const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*charged=*/in.tokenBudget > 0, /*json=*/true }; + const rw::SigsCutContinuation cutNext{ in.task, in.packTopN, in.rankFlags, fixedBytes, /*charged=*/in.tokenBudget > 0, /*json=*/true, + /*pasteHandle=*/true, /*ledgerGapBytes=*/0u, sigsNextPayFromRows }; const JsonSigLens lens{ /*metrics=*/true, in.fanIn, in.impure, in.churnPerFile, in.cloneMember, in.tested, in.amp, /*rankAdaptivePayload=*/true, in.noteIndex }; @@ -1661,6 +1667,13 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For overCeiling = ",\"over_ceiling\":true"; } ASSUME( overCeiling.size() == 0 || overCeiling.size() == 20 ); // the 20 the charge above reserved for it + // knob-honesty-068 (ruling C3): rows paid for "sigs_next" and the bundle STILL lands past its ceiling — dropping them bought + // nothing. Write nothing; the caller re-renders unpaid (the cut's own rows, the handle riding unpaid, over_ceiling disclosed). + if( sigsNextRetryUnpaid != nullptr && sigsNextPayFromRows && cutNext.charged && sigsCut.hasContinuation && !overCeiling.empty() ) + { + *sigsNextRetryUnpaid = true; + return 0; + } std::fputs( header.c_str(), out ); std::fwrite( kJsonBundleSigsKey.data(), 1, kJsonBundleSigsKey.size(), out ); std::fwrite( surfaceCountsStanza.data(), 1, surfaceCountsStanza.size(), out ); @@ -1677,6 +1690,26 @@ inline int emitForLensJson( std::FILE* out, const std::string& header, const For return 0; } +// The --json bundle (orchestrator ruling C3, 2026-10-07): a charged "sigs_next" is paid for in rows only where that makes the +// bundle fit. The paid render runs first (the payable case is unchanged); when it lands past its ceiling anyway it wrote nothing, +// and the bundle is rendered again with the rows the cut alone leaves and the handle unpaid. The redaction tally is put back +// to its entry value first, so the rows the summary counts are the ones written. Rendering only — the ranking is the caller's. +inline int emitForLensJson( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) +{ + const std::optional tallyAtEntry = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; + bool retryUnpaid = false; + const int paidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, &retryUnpaid ); + if( !retryUnpaid ) + { + return paidRc; + } + if( tallyAtEntry ) + { + *in.redact = *tallyAtEntry; + } + return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, nullptr ); +} + // ── T3: the terminal-by-default auto section (pre-registered: docs/EVALS.md §4) ───────────────── // The single biggest measured non-terminal chain is map-then-read: the agent runs --for, then opens the // file the map named. The terminal bundle that already served bodies (--pack-task) went uncalled for a @@ -2252,7 +2285,10 @@ inline std::string renderForHdrRowsXml( const rw::IngestResult& ing, const std:: return x; } -std::optional runForLens( const MainDispatch& d ) +// knob-honesty-068 (orchestrator ruling C3, 2026-10-07): one RUN of the --for lens. `sigsNextPayFromRows` is the charged +// continuation's payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextRetryUnpaid` set, a paid run whose +// document still lands past its ceiling writes NOTHING to stdout and asks for the unpaid run (runForLens below). +std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRows, bool* sigsNextRetryUnpaid ) { using namespace rw; const Config& cfg = d.cfg; @@ -3026,11 +3062,14 @@ std::optional runForLens( const MainDispatch& d ) // the room the compact charge was given beyond full's (compactLedgerGapBytes) — in the plan, so only a block the ladder // already trims pays it: a capped compact answer serves the rows the full one does and lands no further over its // ceiling (compactlegendcheck P4). An uncapped answer is untouched. AT THE FLOOR (rank 1..4, nothing left to shed) the - // handle still ships and an overshoot is labelled (rootFinish.sigsFloorOver below; orchestrator ruling 2026-10-07). + // handle still ships and an overshoot is labelled (rootFinish.sigsUnpaidOver below; orchestrator ruling 2026-10-07). + // AND ONLY WHERE PAYING MAKES IT FIT (ruling C3): a paid run that lands past its ceiling anyway is re-run unpaid + // (sigsNextPayFromRows=false: no ledger gap, no payment — the rows the cut alone leaves; runForLens). const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; const std::string forRankFlags = forRankShapingFlags( cfg ); const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, - /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u }; + /*pasteHandle=*/true, /*ledgerGapBytes=*/forSigsNextCharged ? compactLedgerGapBytes : 0u, + sigsNextPayFromRows }; bool forSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= legend clause below rw::SigsCutReport forSigsCut; // cut-fix lane A: the tag's shown/total/docs_dropped — its clauses below forSigsCut.continuationRequest = &forSigsNext; // knob-honesty-068: IN — the capped block's continuation @@ -3514,17 +3553,24 @@ std::optional runForLens( const MainDispatch& d ) /*hasRouteAttr=*/!routeNoteRaw.empty(), kNotes ); headerStr = std::move( chosen.header ); rootFinish.lastRungFired = ( chosen.rung == rw::CeilingRung::OverCeiling ); - // knob-honesty-068 (orchestrator ruling 2026-10-07): the recovery handle ALWAYS ships. When the floor could - // not pay for its next= and the document it rides in lands past its ceiling — the byte allowance, or the token - // budget the root names — the root says so: over_ceiling="1" + kForSigsFloorOverCeilingNote. Read off the - // FINISHED document (the header this ladder chose + every byte stdout receives); the label only adds bytes, - // so a document over before it is over after it. - if( forSigsCut.continuationUnpaidBytes > 0 ) + // knob-honesty-068 (orchestrator rulings 2026-10-07): the recovery handle ALWAYS ships. Read off the FINISHED + // document (the header this ladder chose + every byte stdout receives): does it land past its ceiling — the byte + // allowance, or the token budget the root names? The label only adds bytes, so a document over before it is over + // after it. + const ForLensPricedHeader asChosen = finishForLensHeaderPriced( headerStr, rootFinish ); + const bool landsOver = asChosen.header.size() + emittedNonHeaderBytes > ladderCeiling + || asChosen.estTokens > cfg.tokenBudget; + // C3: rows paid for the handle and the answer STILL lands past its ceiling — they bought nothing. Nothing has + // reached stdout yet: hand back and let runForLens re-run unpaid (the rows the cut alone leaves). + if( sigsNextRetryUnpaid != nullptr && forSigsNext.payFromRows && forSigsNextCharged && forSigsCut.hasContinuation + && ( landsOver || rootFinish.lastRungFired ) ) { - const ForLensPricedHeader asChosen = finishForLensHeaderPriced( headerStr, rootFinish ); - rootFinish.sigsFloorOver = asChosen.header.size() + emittedNonHeaderBytes > ladderCeiling - || asChosen.estTokens > cfg.tokenBudget; + *sigsNextRetryUnpaid = true; + return std::nullopt; } + // the handle rode unpaid (the floor could not give its bytes up, or this is the unpaid run) and the document lands + // past its ceiling: over_ceiling="1" + kForSigsUnpaidOverCeilingNote + rootFinish.sigsUnpaidOver = forSigsCut.continuationUnpaidBytes > 0 && landsOver; } headerStr = finishForLensHeader( std::move( headerStr ), rootFinish ); @@ -3617,6 +3663,27 @@ std::optional runForLens( const MainDispatch& d ) return std::nullopt; } +// The --for lens (orchestrator ruling C3, 2026-10-07: never drop rows to pay for next= when the answer is over its ceiling +// anyway — dropping them does not make it fit). A charged continuation is paid for in rows first, so the payable case is +// the run it always was; a paid run whose finished document still lands past its ceiling wrote nothing (the decision is taken +// before the first byte reaches stdout) and the lens runs again with payFromRows=false: the rows the cut alone leaves, the +// handle unpaid, over_ceiling="1" + kForSigsUnpaidOverCeilingNote. The redaction tally is put back to its entry value first, +// so the summary counts the rows written once. The re-run repeats the ranking: it happens only on a capped, charged answer +// that cannot fit (the tight-ceiling tail), never on the default ceiling. +std::optional runForLens( const MainDispatch& d ) +{ + const rw::RedactCounts tallyAtEntry = d.redactCounts; + bool retryUnpaid = false; + std::optional paidRc = runForLensPass( d, /*sigsNextPayFromRows=*/true, &retryUnpaid ); + if( !retryUnpaid ) + { + return paidRc; + } + ASSUME( !paidRc.has_value() ); // the paid run returned before writing anything + d.redactCounts = tallyAtEntry; + return runForLensPass( d, /*sigsNextPayFromRows=*/false, nullptr ); +} + std::optional runTargetedViews( const MainDispatch& d ) { using namespace rw; diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index de1d28b8a..92a0013b2 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -843,12 +843,13 @@ for tb in 850 1000 1100 1200; do fi done [ "$p4bad" -eq 0 ] && ok "(P4) at --token-budget 850/1000/1100/1200 the compact --for answer is inside its budget or no further over it than the full one" -# (P4 floor twin — knob-honesty-068; orchestrator ruling 2026-10-07, PROCESS rules 4/5) A capped under a hard ceiling -# names the call that recovers its cut (next=) and pays for it from its rows. At 500 tokens this fixture's block is at its -# rank 1..4 floor in BOTH dialects and cannot pay: the recovery handle still ships, and the overshoot is DISCLOSED — root -# over_ceiling="1" with the floor clause defining it, est_tokens pricing the bytes delivered (P3's rule), and the P4 claim -# itself (compact no further over than full). Near miss at 750: the handle is paid — est_tokens <= budget in both dialects, -# next= rides, no over_ceiling= and no floor clause (a label riding every capped answer would pass the 500 half alone). +# (P4 floor twin — knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5) A capped under a hard ceiling +# names the call that recovers its cut (next=) and pays for it from its rows where that makes the answer fit. At 500 tokens +# this fixture's block is at its rank 1..4 floor in BOTH dialects and the answer is past its ceiling paid or not: no row is +# dropped for the handle (ruling C3), it still ships, unpaid, and the overshoot is DISCLOSED — root over_ceiling="1" with the +# unpaid clause defining it, est_tokens pricing the bytes delivered (P3's rule), and the P4 claim itself (compact no further +# over than full). Near miss at 750: paying is what makes it fit — est_tokens <= budget in both dialects, next= rides, no +# over_ceiling= and no unpaid clause (a label riding every capped answer would pass the 500 half alone). p4floor_bad=0 for tb in 500 750; do for lg in compact full; do @@ -857,10 +858,10 @@ for tb in 500 750; do pc="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.compact" | head -1 | tr -dc '0-9' )"; pf="$( grep -o 'est_tokens="[0-9]*"' "$TMP/p4f.full" | head -1 | tr -dc '0-9' )" for lg in compact full; do f="$TMP/p4f.$lg"; pb="$( wc -c <"$f" | tr -d ' ' )"; pe="$( grep -o 'est_tokens="[0-9]*"' "$f" | head -1 | tr -dc '0-9' )" - root="$( grep -oE '^]*>' "$f" | head -1 )"; sigs="$( grep -oE ']*>' "$f" | head -1 )" + root="$( grep -oE '^]*>' "$f" | head -1 )"; sigs="$( grep -oE ']*>' "$f" | tail -1 )" hasnext=0; case "$sigs" in *' capped="1"'*' next="--for='*) hasnext=1;; esac over=0; [ "${root#* over_ceiling=\"1\"}" != "$root" ] && over=1 - clause=0; grep -qF '[over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]' "$f" && clause=1 + clause=0; grep -qF '[over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]' "$f" && clause=1 if [ -z "$pe" ] || [ "$hasnext" -ne 1 ]; then p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb --legend=$lg: est_tokens=${pe:-?}, next= present=$hasnext — a capped block must carry its next=" continue @@ -880,7 +881,7 @@ for tb in 500 750; do p4floor_bad=$(( p4floor_bad + 1 )); no "(P4 floor twin) --token-budget=$tb: compact est_tokens=${pc:-?} over the budget AND over the full answer's ${pf:-?}" fi done -[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= at the rank 1..4 floor with over_ceiling=\"1\", the floor clause and an honest est_tokens, the 750 run's paid next= fits with no label, compact never further over than full" +[ "$p4floor_bad" -eq 0 ] && ok "(P4 floor twin) at 500 both dialects ship next= unpaid at the rank 1..4 floor with over_ceiling=\"1\", the unpaid clause and an honest est_tokens, the 750 run's paid next= fits with no label, compact never further over than full" # (P5) the BUDGET LEDGER survives compaction (orchestrator rule, METHODOLOGY §9.3/§9.4: never cut silently). --pack-task's # full legend ends with "budget=N bytes (T-token target, ceiling C) | ranking: … | bodies: … | callers: … | notes: … | tests: diff --git a/test/estcalib.manifest b/test/estcalib.manifest index aecd0a9fb..b535e4e95 100644 --- a/test/estcalib.manifest +++ b/test/estcalib.manifest @@ -8,5 +8,5 @@ metrics 2381 2370 2509 --metrics pack-signatures 1801 1784 2116 --pack-signatures expand 541 542 455 --expand=billableTotal for-named 789 785 1050 --for=billableTotal -for-budgeted 484 479 679 --for=dedupe --token-budget=400 +for-budgeted 488 483 692 --for=dedupe --token-budget=400 pack-task 882 862 1104 --pack-task=dedupe diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 04e8eacf6..4b2c4424e 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -585,41 +585,44 @@ ALLOW="$( awk "BEGIN{printf \"%d\", $TBF*2.36*1.15}" )" "$BIN" src --for="$FOR_TASK" --token-budget=$TBF --detail=20 --with-graph --no-cache >"$TMP/f_tb.out" 2>"$TMP/f_tb.err" rc_ftb=$? FTB="$( bytes_of "$TMP/f_tb.out" )" -# THE FLOOR TWIN (knob-honesty-068; orchestrator ruling 2026-10-07, PROCESS rules 4/5). A capped under a hard -# ceiling carries next= — the call that recovers its cut — and pays for it from its rows. At the rank 1..4 floor nothing is -# left to shed, and the recovery handle ALWAYS ships: the document may then land past the allowance, and only if it says so. -# This is not a wider allowance: every condition of the old bound still holds for everything except the disclosed handle. -# sigs_floor_twin FILE ALLOW → exit 0 iff ALL of: the root carries over_ceiling="1"; the legend carries the floor clause -# defining it; is capped at shown<=4 (the floor) and carries next="--for=…"; est_tokens is positive and prices the -# delivered bytes inside the 2.00-4.20 B/tok band (honest: it moved with the handle); and the document WITHOUT the handle -# and its label (the next= attribute, over_ceiling="1", the clause) is within ALLOW — the old bound, intact. -sigs_floor_twin(){ python3 - "$1" "$2" <<'PYF' +# THE UNPAID TWIN (knob-honesty-068; orchestrator rulings 2026-10-07, PROCESS rules 4/5). A capped under a +# hard ceiling carries next= — the call that recovers its cut — and pays for it from its rows ONLY where that makes the +# answer fit (ruling C3). Where it cannot (the rank 1..4 floor has nothing left to shed, or the answer lands past its ceiling +# paid or not) no row is dropped for it: the recovery handle ALWAYS ships, unpaid, and the document may then land past the +# allowance, and only if it says so. This is not a wider allowance: every condition of the old bound still holds for +# everything except the disclosed handle. sigs_unpaid_twin FILE ALLOW → exit 0 iff ALL of: the root carries +# over_ceiling="1"; the legend carries the unpaid clause defining it; is capped and carries next="--for=…"; +# est_tokens is positive and prices the delivered bytes inside the 2.00-4.20 B/tok band (honest: it moved with the handle); +# and the document WITHOUT the handle and its label (the next= attribute, over_ceiling="1", the clause) is within ALLOW — +# the old bound, intact — or carries the ladder's last-rung disclosure (the header floor alone exceeds the budget: the one +# overshoot the old bound already admitted). The rows are the ones the cut alone leaves (the C3 arm below pins them). +sigs_unpaid_twin(){ python3 - "$1" "$2" <<'PYF' import sys, re d = open( sys.argv[ 1 ], 'rb' ).read().decode( 'utf-8', 'replace' ); allow = int( sys.argv[ 2 ] ) -clause = ' [over_ceiling=1 also when at its r=1..4 floor cannot pay its next=: the call ships anyway]' +clause = ' [over_ceiling=1 also when the sigs next= rides unpaid: no row is dropped to pay for it on an answer that would not fit either way]' root = re.search( r']*>', d ); sigs = re.search( r']*>', d ) why = [] if not root or ' over_ceiling="1"' not in root.group( 0 ): why.append( 'no root over_ceiling="1"' ) -if clause not in d: why.append( 'no floor clause' ) +if clause not in d: why.append( 'no unpaid clause' ) nxt = re.search( r' next="--for=[^"]*"', sigs.group( 0 ) ) if sigs else None -shown = re.search( r' shown="(\d+)"', sigs.group( 0 ) ) if sigs else None -if not sigs or ' capped="1"' not in sigs.group( 0 ) or not shown or int( shown.group( 1 ) ) > 4: why.append( ' not capped at its floor' ) +if not sigs or ' capped="1"' not in sigs.group( 0 ): why.append( ' not capped' ) if not nxt: why.append( ' carries no next=' ) est = re.search( r' est_tokens="(\d+)"', root.group( 0 ) ) if root else None b = len( d.encode( 'utf-8' ) ) if not est or int( est.group( 1 ) ) <= 0 or not ( 200 <= b * 100 // int( est.group( 1 ) ) <= 420 ): why.append( 'est_tokens absent or out of band' ) rest = b - ( len( nxt.group( 0 ) ) if nxt else 0 ) - len( ' over_ceiling="1"' ) - len( clause ) -if rest > allow: why.append( f'{rest} B without the handle > {allow} B' ) +lastRung = '[over_ceiling= is 1 on the root: the header floor' in d +if rest > allow and not lastRung: why.append( f'{rest} B without the handle > {allow} B (and no last-rung disclosure)' ) print( '; '.join( why ) if why else f'{b} B, {rest} B without the handle <= {allow} B, est_tokens={est.group( 1 )}' ) sys.exit( 1 if why else 0 ) PYF } if { [ "$rc_ftb" -eq 0 ] && [ "$FTB" -le "$ALLOW" ]; } 2>/dev/null; then ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B within the $ALLOW B allowance (exit 0, SHAPED)" -elif [ "$rc_ftb" -eq 0 ] && a7why="$( sigs_floor_twin "$TMP/f_tb.out" "$ALLOW" )"; then - ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: the floor twin — $a7why, labelled over_ceiling=\"1\" with next= (exit 0, SHAPED)" +elif [ "$rc_ftb" -eq 0 ] && a7why="$( sigs_unpaid_twin "$TMP/f_tb.out" "$ALLOW" )"; then + ok "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: the unpaid twin — $a7why, labelled over_ceiling=\"1\" with next= (exit 0, SHAPED)" else - no "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B vs the $ALLOW B allowance, exit $rc_ftb — the budget does not bound the appended sections${a7why:+ (floor twin: $a7why)}" + no "#11 A7 --for --token-budget=$TBF --detail=20 --with-graph: $FTB B vs the $ALLOW B allowance, exit $rc_ftb — the budget does not bound the appended sections${a7why:+ (unpaid twin: $a7why)}" fi # and without an explicit --token-budget the bodies keep their own budget: the bundle must NOT have shrunk { [ "$( bytes_of "$TMP/f_for_detail.out" )" -gt "$ALLOW" ]; } 2>/dev/null \ @@ -663,19 +666,28 @@ a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0; a7s_floor=0 # allowance is consulted; on this corpus the late-label band (over_ceiling="1" INSIDE the allowance — the residual # after that drop) therefore sits at 780..810 instead of inside 1200..1500, and the control below would otherwise be # inert. Swept 700..3300 step 10 on the new binary: default hits at 780 790 800 810, none on the --detail=20 arm. -for spec in "default:760:1500:" "detail_graph:2880:3080:--detail=20 --with-graph"; do - s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_args="${s_rest#*:}" - for (( N = s_from; N <= s_to; N += 10 )); do +# RE-ANCHORED 2026-10-07 (knob-honesty-068, orchestrator ruling C3): a capped, CHARGED whose answer is past its ceiling +# paid or not is now served with the rows the cut alone leaves and its next= unpaid — bigger than the paid answer, so the +# capped documents that sat in the late-label band (dd6e4c8e: the --detail=20 arm's 2880..3080, est_tokens > N inside the +# allowance) now cross the allowance on the ladder's disclosed last rung, and no capped document on this corpus is left in +# the band (swept 700..1500 and 2880..3080 step 10). The band itself is untouched by the ruling: an UNCAPPED block owes no +# continuation, so the third spec — a name-exact task serving ONE row — keeps a document whose header floor lands in it +# (480..488 at the time of writing; step 4 so a band 9 budgets wide is always crossed). Fields: label:from:to:step:task:args. +for spec in "default:760:1500:10:serialize the map:" "detail_graph:2880:3080:10:serialize the map:--detail=20 --with-graph" \ + "named_one_row:400:600:4:serializeRow0_3:"; do + s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_rest="${s_rest#*:}" + s_step="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_task="${s_rest%%:*}"; s_args="${s_rest#*:}" + for (( N = s_from; N <= s_to; N += s_step )); do # shellcheck disable=SC2086 - ( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=$N $s_args --no-cache ) >"$A7S/o.xml" 2>/dev/null + ( cd "$A7S" && "$BIN" corpus --for="$s_task" --token-budget=$N $s_args --no-cache ) >"$A7S/o.xml" 2>/dev/null s_rc=$? a7s_runs=$(( a7s_runs + 1 )) s_b="$( bytes_of "$A7S/o.xml" )" s_a="$( awk "BEGIN{printf \"%d\", $N*2.36*1.15}" )" s_root="$( grep -aoE '^]*>' "$A7S/o.xml" | head -1 )" - # past the allowance: the ladder's last rung says so, or (knob-honesty-068) the floor twin above holds in full + # past the allowance: the ladder's last rung says so, or (knob-honesty-068) the unpaid twin above holds in full if [ "$s_rc" -eq 0 ] && [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml" \ - && sigs_floor_twin "$A7S/o.xml" "$s_a" >/dev/null; then + && sigs_unpaid_twin "$A7S/o.xml" "$s_a" >/dev/null; then a7s_floor=$(( a7s_floor + 1 )) elif [ "$s_rc" -ne 0 ] || { [ "$s_b" -gt "$s_a" ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/o.xml"; }; then a7s_badn=$(( a7s_badn + 1 )) @@ -686,33 +698,54 @@ for spec in "default:760:1500:" "detail_graph:2880:3080:--detail=20 --with-graph done done [ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung, or the disclosed floor twin ($a7s_floor)" \ + && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10; one-row named task 400..600, step 4) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung, or the disclosed unpaid twin ($a7s_floor)" \ || no "#11 A7 sweep: $a7s_badn of $a7s_runs budgets deliver past the allowance with no ladder rung fired (first:$a7s_bad) — a byte spliced in after the ladder priced the document" # control: the sweep must cross the band the defect lives in — a root that says over_ceiling="1" while the document # still fits the allowance (est_tokens > N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. [ "$a7s_inside_labelled" -gt 0 ] \ && ok "#11 A7 sweep control: $a7s_inside_labelled budget(s) carry a root over_ceiling=\"1\" INSIDE the allowance — the sweep crosses the late-label band" \ || no "#11 A7 sweep control: no budget carried over_ceiling=\"1\" inside the allowance — the sweep no longer reaches the est_tokens > N band, re-anchor its ranges" -# A7 FLOOR TWIN, EXERCISED EVERY RUN (knob-honesty-068; ruling 2026-10-07). The A7 arm and the sweep accept the twin only as -# an alternative, so a fixture that stopped reaching the floor would leave it untested. On this git-less corpus at -# --token-budget=790 the default bundle's sits at its rank 1..4 floor and cannot pay its next=: the twin must hold. -# Its near miss, 850: the handle is paid (the block sheds its bytes above the floor) — next= rides inside the allowance, and neither -# over_ceiling="1" nor the floor clause appears (a label that rode on every capped answer would pass the first half alone). -( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=790 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? -fl_a="$( awk 'BEGIN{printf "%d", 790*2.36*1.15}' )" -if [ "$fl_rc" -eq 0 ] && fl_why="$( sigs_floor_twin "$A7S/floor.xml" "$fl_a" )"; then - ok "#11 A7 floor twin @790: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" +# A7 UNPAID TWIN, EXERCISED EVERY RUN (knob-honesty-068; rulings 2026-10-07). The A7 arm and the sweep accept the twin only +# as an alternative, so a fixture that stopped reaching it would leave it untested. On this git-less corpus at +# --token-budget=780 the default bundle's sits at its rank 1..4 floor: the answer the cut alone leaves FITS (the +# pre-continuation binary, 255dc199, serves it at est_tokens=774 with no label), its next= cannot be paid in rows, and the +# document lands past the allowance only by the handle — the twin must hold, with no last-rung disclosure to lean on. +( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=780 --no-cache ) >"$A7S/floor.xml" 2>/dev/null; fl_rc=$? +fl_a="$( awk 'BEGIN{printf "%d", 780*2.36*1.15}' )" +if [ "$fl_rc" -eq 0 ] && ! grep -aqF '[over_ceiling= is 1 on the root: the header floor' "$A7S/floor.xml" \ + && fl_why="$( sigs_unpaid_twin "$A7S/floor.xml" "$fl_a" )"; then + ok "#11 A7 unpaid twin @780: $fl_why — next= ships at the floor and the overshoot is labelled over_ceiling=\"1\" with its clause" else - no "#11 A7 floor twin @790 (exit $fl_rc): ${fl_why:-?} — the floor did not ship its next= with a labelled, honestly priced overshoot" + no "#11 A7 unpaid twin @780 (exit $fl_rc): ${fl_why:-the ladder last rung fired, so the twin is not the only disclosure} — the floor did not ship its next= with a labelled, honestly priced overshoot" fi +# C3 (orchestrator ruling 2026-10-07): NEVER DROP ROWS TO PAY FOR next= WHEN THE ANSWER IS OVER ITS CEILING ANYWAY. At 790 the +# answer the cut alone leaves is past its ceiling already (255dc199 serves 5 rows there at est_tokens=952, last rung fired); +# paying from rows (dd6e4c8e: 4 rows, est_tokens=885) still lands over, so it bought nothing. The answer must keep the cut's +# own rows — at least 255dc199's 5 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it) — +# and MORE than the paid near miss at 850 serves (4 rows: there paying is what makes the answer fit), with next= riding +# unpaid and over_ceiling="1" + the unpaid clause on the root. RED on dd6e4c8e (4 rows). +( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=790 --no-cache ) >"$A7S/c3.xml" 2>/dev/null; c3_rc=$? ( cd "$A7S" && "$BIN" corpus --for="serialize the map" --token-budget=850 --no-cache ) >"$A7S/paid.xml" 2>/dev/null; pd_rc=$? +sigs_shown(){ grep -aoE ']*>' "$1" | tail -1 | grep -oE ' shown="[0-9]+"' | tr -dc '0-9'; } +c3_sh="$( sigs_shown "$A7S/c3.xml" )"; pd_sh="$( sigs_shown "$A7S/paid.xml" )" +c3_root="$( grep -aoE '^]*>' "$A7S/c3.xml" | head -1 )" +if [ "$c3_rc" -eq 0 ] && [ -n "$c3_sh" ] && [ -n "$pd_sh" ] && [ "$c3_sh" -ge 5 ] && [ "$c3_sh" -gt "$pd_sh" ] \ + && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/c3.xml" && [ "${c3_root#* over_ceiling=\"1\"}" != "$c3_root" ] \ + && grep -aqF 'next= rides unpaid: no row is dropped to pay for it' "$A7S/c3.xml"; then + ok "#11 A7 C3 @790: over its ceiling either way, the answer keeps the cut's own $c3_sh rows (>= 255dc199's 5, > the paid 850 answer's $pd_sh) — next= unpaid, over_ceiling=\"1\" + the unpaid clause" +else + no "#11 A7 C3 @790 (exit $c3_rc): shown=${c3_sh:-?} (want >= 5 and > the paid 850 answer's ${pd_sh:-?}), next= + over_ceiling=\"1\" + the unpaid clause — rows were dropped to pay for a handle that cannot make the answer fit" +fi +# Its near miss, 850 — the PAYABLE case, unchanged: paying is what makes the answer fit (255dc199: 5 rows, est_tokens=911, +# over), so the handle is paid (the block sheds its bytes above the floor) — next= rides inside the allowance, and neither +# over_ceiling="1" nor the unpaid clause appears (a label that rode on every capped answer would pass the arms above alone). pd_b="$( bytes_of "$A7S/paid.xml" )"; pd_a="$( awk 'BEGIN{printf "%d", 850*2.36*1.15}' )" pd_root="$( grep -aoE '^]*>' "$A7S/paid.xml" | head -1 )" if [ "$pd_rc" -eq 0 ] && [ "$pd_b" -le "$pd_a" ] && grep -aqE ']* capped="1"[^>]* next="--for=' "$A7S/paid.xml" \ - && [ "${pd_root#* over_ceiling=\"1\"}" = "$pd_root" ] && ! grep -aqF 'r=1..4 floor cannot pay its next=' "$A7S/paid.xml"; then - ok "#11 A7 floor twin near miss @850: the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no floor clause" + && [ "${pd_root#* over_ceiling=\"1\"}" = "$pd_root" ] && ! grep -aqF 'next= rides unpaid' "$A7S/paid.xml"; then + ok "#11 A7 unpaid twin near miss @850: the capped pays for its next= — $pd_b B <= $pd_a B, no over_ceiling=, no unpaid clause" else - no "#11 A7 floor twin near miss @850 (exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" + no "#11 A7 unpaid twin near miss @850 (exit $pd_rc): $pd_b B vs $pd_a B — expected a paid next= inside the allowance with no label" fi # A9/A10 — the header's own spliced attributes are inside the number. IDENTITY, not a band: for a bundle with diff --git a/test/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index 90e3d0628..14845d5b0 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -125,6 +125,23 @@ grep -q ']*> block is intact" \ || no "#2 ceiling-exhausted: the block vanished" +# #2c (rv-knob-honesty-068 C2; CHECKLIST 17) the intact-block grep above CAN FAIL on exactly this document. It is a ceiling- +# exhausted one: its is capped, its next= rides unpaid and the root carries over_ceiling="1" with the unpaid clause in +# the header legend — BEFORE the real tag. A clause that spelled the tag literally (dd6e4c8e's floor clause did) satisfied +# `grep -q '",a); sys.stdout.buffer.write(s[:a]+s[b+7:] if a>=0 and b>=0 else s)' "$T" >"$TMP/n400_nosigs" + grep -q ' block removed — a legend clause spells the tag" \ + || ok "#2c the intact-block grep FAILS once the block is removed (no legend clause spells the tag)" + sigsblock "$T" | grep -q -- '-->' \ + && no "#2c sigsblock() starts inside the header legend comment, not at the real tag" \ + || ok "#2c sigsblock() starts at the real tag" +else + no "#2c premise: the --token-budget=400 document no longer carries the unpaid clause — re-anchor this arm on a document that does" +fi "$BIN" src --for="$NAMETASK" --token-budget=400 --signatures-only --no-cache >"$TMP/n400so" 2>/dev/null sigsblock "$T" >"$TMP/s_a"; sigsblock "$TMP/n400so" >"$TMP/s_s" [ -s "$TMP/s_a" ] || no "#2 presence: could not extract a block from the disclosed run" diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 354c61358..ebbacc0d4 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -545,5 +545,32 @@ else no "(9) MCP twin: the re-call with budget_tokens=$MNB did not serve the block uncut: ${M2:-}" fi +# ── (10) C3: NO ROW IS DROPPED TO PAY FOR A HANDLE THAT CANNOT MAKE THE ANSWER FIT (orchestrator ruling 2026-10-07) ────────── +# The MCP `for` answer overshoots budget_tokens on its own (its sig ledger exempts header bytes), so under a budget that caps +# it is nearly always past its ceiling paid or not; paying next_budget_tokens= from the rows (dd6e4c8e: ~245 B, 1-2 +# rows) bought nothing. Over its ceiling either way → the rows the cut alone leaves, the handle unpaid, over_ceiling="1". +# Where paying IS what makes it fit, it pays (unchanged). Fixture: (9)'s 40 gadget functions. +# over-anyway @2000: 255dc199 serves 14 of 40 (est 2298, over); dd6e4c8e paid 2 rows (12, est 2234, still over) — RED there. +# Rows >= 14 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it), the handle rides, +# over_ceiling="1". Same at 3500 (255dc199: 31 of 40). +# payable @4000: 255dc199 serves 37 of 40 inside the budget (est 3971); the handle's bytes are paid from rows so the answer +# STILL fits — est_tokens <= 4000, no over_ceiling=, next_budget_tokens= present, fewer rows than the 37 (paid). +c3row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["shown"], d["capped"], d["next_budget_tokens"] is not None, d["over"], d["est_tokens"])' 2>/dev/null; } +for spec in 2000:14 3500:31; do + bt="${spec%%:*}"; want="${spec##*:}" + MC3="$( mcpfor ",\"budget_tokens\":$bt" )"; set -- $( c3row "$MC3" ) + if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = True ]; then + ok "(10) MCP budget_tokens=$bt, over its ceiling either way: $1 rows (>= the cut's own $want, none dropped for the handle), next_budget_tokens= unpaid, over_ceiling=\"1\" (est $5)" + else + no "(10) MCP budget_tokens=$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows (the cut's own), the handle, over_ceiling=\"1\"" + fi +done +MP="$( mcpfor ',"budget_tokens":4000' )"; set -- $( c3row "$MP" ) +if [ "$#" -eq 5 ] && [ "$1" -lt 37 ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le 4000 ]; then + ok "(10) MCP budget_tokens=4000, payable: the handle is paid from rows ($1 < 37) and the answer fits (est $5 <= 4000, no over_ceiling=)" +else + no "(10) MCP budget_tokens=4000: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" From b3afe7c4ab8c58e1b4b3ab5cfb6fc542504e11b7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 22:14:38 -0400 Subject: [PATCH 155/229] fix(lint): SARIF findingsNext keeps --sarif; --lint-max-per-rule help says it sets the budget either way rv-knob-honesty-068 N3/N4. A CI consumer pasting a SARIF run's findingsNext got native XML; the call now re-runs in the dialect it was read from (lintbudgetcheck pastes it and parses SARIF). The help's first line said "raise ... above the default"; the flag sets the budget below it too (lintbudgetcheck uses 4). printf_parity: help/help_all re-hashed (only). Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 12 ++++++------ src/verbs_lint.h | 7 ++++++- test/lintbudgetcheck.sh | 16 ++++++++++------ test/printf_parity.manifest | 4 ++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/cli.h b/src/cli.h index 0c7419bae..fd6a77ee0 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1650,12 +1650,12 @@ inline constexpr char kHelpHead[] = " (with --lint / --lint-rules) DROP rules whose name starts with one of these PREFIXes (or '*' to\n" " drop everything, e.g. paired with --lint-select elsewhere to isolate one family) — applied AFTER\n" " --lint-select narrows the set; same unresolvable-PREFIX refusal and root disclosure as --lint-select\n" - " --lint-max-per-rule=N (with --lint) raise each rule's match budget above the default 5000\n" - " (with --lint / --lint-rules) each rule's raw-capture budget; the default (5000) is a runaway\n" - " guard, not a target. A rule that spends it carries count_capped=\"1\" (its count= is a FLOOR)\n" - " and the root names the call that counts the rest: findings_next= (SARIF: run properties\n" - " findingsNext) re-runs only the floored rules under a 10x budget; a re-run still floored\n" - " names its own. Raising it costs time and memory in proportion to the matches kept\n" + " --lint-max-per-rule=N (with --lint / --lint-rules) set each rule's match budget, below or above the default 5000.\n" + " The default is a runaway guard, not a target. A rule that spends its budget carries\n" + " count_capped=\"1\" (its count= is a FLOOR) and the root names the call that counts the rest:\n" + " findings_next= (SARIF: run properties findingsNext, --sarif kept) re-runs only the floored\n" + " rules under a 10x budget; a re-run still floored names its own. Raising it costs time and\n" + " memory in proportion to the matches kept\n" " --sarif (with --lint / --lint-rules) the SAME findings as SARIF 2.1.0 instead of the native XML\n" " block — the shape github/codeql-action/upload-sarif consumes for code scanning.\n" " Pure re-serialization (zero new analysis); results count == the native run's findings\n" diff --git a/src/verbs_lint.h b/src/verbs_lint.h index b555aaea0..8eba755c5 100644 --- a/src/verbs_lint.h +++ b/src/verbs_lint.h @@ -502,7 +502,8 @@ struct RuleCap { std::string rule; bool isUserRule; }; // dead end: disclosed as a floor, with no call that counts the rest, because the budget had no flag. findings_next= is that // call — the floored rules ONLY (by --lint-select, so the re-run spends nothing on rules that were already totals), under a // 10x budget (lintNextMaxPerRule), keeping --lint-rules=DIR when a user rule floored and --naming-locals when it was on (it -// changes the naming-* counts). A re-run that is still floored names its own, so the chain ends. `emitted` is the H8 +// changes the naming-* counts), and --sarif on a SARIF run (its findingsNext). A re-run that is still floored names its own, +// so the chain ends. `emitted` is the H8 // predicate: a rule the selection dropped never floors this answer, so it never joins the call. "" ⇒ nothing floored. // Spelled IN FULL whatever its length (nextverb.h: a next= is never dropped or truncated); nextFlag quotes a value a // shell would split (a rules directory with a space, a user rule id with a quote). @@ -544,6 +545,10 @@ std::string lintFindingsNext( const rw::Config& cfg, const std::vector& } inv += " " + rw::nextFlag( "--lint-select=", joined ); inv += " --lint-max-per-rule=" + std::to_string( rw::lintNextMaxPerRule( spentPerRule ) ); + if( cfg.sarif ) + { + inv += " --sarif"; // the SARIF run's findingsNext re-runs in the dialect it was read from (rv-knob-honesty-068 N4) + } ENSURES( !inv.empty() && inv.front() == '-', "a findings_next= is a pasteable flag list (--lint or --lint-rules leads)" ); return inv; } diff --git a/test/lintbudgetcheck.sh b/test/lintbudgetcheck.sh index 835e77cff..77d3e0c39 100755 --- a/test/lintbudgetcheck.sh +++ b/test/lintbudgetcheck.sh @@ -170,15 +170,19 @@ GS="$( "$BIN" "$GF" --lint --sarif --no-cache 2>/dev/null )" printf '%s' "$GS" | python3 -c ' import json, sys d = json.load( sys.stdin ); p = d["runs"][0]["properties"] -sys.exit( 0 if p.get( "findingsCapped" ) is True and p.get( "findingsNext" ) == "--lint --lint-select=goto --lint-max-per-rule=50000" else 1 )' 2>/dev/null \ - && ok "SARIF: run properties carry findingsCapped=true and the same findingsNext" \ - || no "SARIF: run properties lack findingsNext: $( printf '%s' "$GS" | grep -oE '"properties":\{"findingsCapped"[^}]*' | head -1 )" -"$BIN" "$GF" --lint --sarif --lint-max-per-rule=50000 --no-cache 2>/dev/null | python3 -c ' +sys.exit( 0 if p.get( "findingsCapped" ) is True and p.get( "findingsNext" ) == "--lint --lint-select=goto --lint-max-per-rule=50000 --sarif" else 1 )' 2>/dev/null \ + && ok "SARIF: run properties carry findingsCapped=true and the same findingsNext, --sarif kept" \ + || no "SARIF: run properties lack findingsNext (with --sarif): $( printf '%s' "$GS" | grep -oE '"properties":\{"findingsCapped"[^}]*' | head -1 )" +# …and PASTING it (shlex-split, as a CI step would) re-runs in the dialect it was read from (rv-knob-honesty-068 N4): the +# answer parses as SARIF (a native-XML answer would not), the floor is lifted and no findingsNext rides. RED on dd6e4c8e. +GSN="$( printf '%s' "$GS" | python3 -c 'import json,sys; print(json.load(sys.stdin)["runs"][0]["properties"].get("findingsNext",""))' 2>/dev/null )" +python3 -c 'import shlex,sys; print( "\0".join( shlex.split( sys.argv[1] ) ), end = "" )' "$GSN" > "$TMP/gsn.argv" +( [ -n "$GSN" ] && xargs -0 "$BIN" "$GF" --no-cache < "$TMP/gsn.argv" 2>/dev/null ) | python3 -c ' import json, sys p = json.load( sys.stdin )["runs"][0]["properties"] sys.exit( 0 if p.get( "findingsCapped" ) is False and "findingsNext" not in p else 1 )' 2>/dev/null \ - && ok "SARIF: under the raised budget, findingsCapped=false and no findingsNext" \ - || no "SARIF: the raised-budget run still floors or still carries findingsNext" + && ok "SARIF: pasting findingsNext answers in SARIF, findingsCapped=false and no findingsNext" \ + || no "SARIF: pasting findingsNext ('$GSN') did not answer as SARIF with the floor lifted" # user rules: the call keeps --lint-rules=DIR and selects the floored rule id mkdir -p "$TMP/urules"; printf -- '- id: many-goto\n language: c\n severity: warn\n message: goto\n query: (goto_statement) @hit\n' > "$TMP/urules/g.yml" U1="$( "$BIN" "$GF" --lint-rules="$TMP/urules" --no-cache 2>/dev/null )" diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index f19a55682..68dc86ac7 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 dadaf0250be6587d0fe44fb5db1aebad9659e6761c2c8857d4183a453138d88d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 f2860e86a9331485eb411627d9b6090c98447c9d9c30fc72e164b7399f96c3e5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 c9d925cdd682bdb5d109fe0868294db0e073e198e47ce14004aa4a141f39085b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 4b9537e1185b3bffe91a73c4cd5513dc9942b84a98f31b2a42b47a7c1f70480d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From a1bd5f065b5d91e8b5ac5effad0b231f79003028 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 22:14:38 -0400 Subject: [PATCH 156/229] docs: TUNING.md regenerated; showcase captures --lint-max-per-rule and the 785-entry legend dictionary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI reds of dd6e4c8e (both lane-caused, generators only): - capsweepcheck (C): `python3 bench/capsweep/capsweep.py emit` — the kLintMaxPerRule row now discloses count_capped. - showcasecapturecheck (D)+(I): two captures for --lint-max-per-rule (the floored goto run at 4 naming its findings_next=, and the pasted call at 40 counting the total), registered in test/showcase_capture.py; the --legend-dict block re-read from the binary (dictv=0726f4f1f414afd5 entries=785). docs/COMMANDS.md regenerated from the capture (the train-26a recipe, cd87e30e). Co-Authored-By: Claude Opus 5.5 --- docs/COMMANDS.md | 28 +++++++++--- docs/TUNING.md | 2 +- docs/captures/COMMANDS_showcase_2026-09-14.md | 45 ++++++++++++++++++- test/showcase_capture.py | 2 + 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index e7d960d34..7ad41fdb9 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -2504,14 +2504,30 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- ### `--lint-max-per-rule=N` -**Answers:** (with --lint) raise each rule's match budget above the default 5000 (with --lint / --lint-rules) each rule's raw-capture budget; +**Answers:** (with --lint / --lint-rules) set each rule's match budget, below or above the default 5000. -the default (5000) is a runaway guard, not a target. A rule that spends it carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext) re-runs only the floored rules under a 10x budget; a re-run still floored names its own. Raising it costs time and memory in proportion to the matches kept +The default is a runaway guard, not a target. A rule that spends its budget carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext, --sarif kept) re-runs only the floored rules under a 10x budget; a re-run still floored names its own. Raising it costs time and memory in proportion to the matches kept + +**Try it** + +_A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped="1", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget._ + +``` +$ ./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=4 + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; + +``` **Caveats (stated by the binary):** -- the default (5000) is a runaway guard, not a target. -- A rule that spends it carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext) re-runs only the floored rules under a 10x budget; +- The default is a runaway guard, not a target. +- A rule that spends its budget carries count_capped="1" (its count= is a FLOOR) and the root names the call that counts the rest: findings_next= (SARIF: run properties findingsNext, --sarif kept) re-runs only the floored rules under a 10x budget; - a re-run still floored names its own. ### `--sarif` @@ -2539,7 +2555,7 @@ $ ./build/ripwire . --lint --sarif ... [21 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--legend` +**Shaped by:** `--lint-max-per-rule`, `--legend` **Caveats (stated by the binary):** @@ -4410,7 +4426,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=0726f4f1f414afd5 entries=785 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/TUNING.md b/docs/TUNING.md index 8a09f448f..689ebd673 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -189,7 +189,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kLintMaxPerRule` = `5000` -`src/lintrules.h` — discloses: **none** — probe value `40000` — **4 verb(s) respond** +`src/lintrules.h` — discloses: `count_capped` — probe value `40000` — **4 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 51b6240f2..f8ac96262 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -5351,7 +5351,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=0726f4f1f414afd5 entries=785 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5381,7 +5381,7 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [755 more display lines; full output is 77894 bytes on 785 raw line(s)] +… [756 more display lines; full output is 78001 bytes on 786 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` @@ -5460,6 +5460,47 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= … [724 more display lines; full output is 67006 bytes on 1 raw line(s)] ````` +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=4` + +*A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped="1", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget.* + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; + +````` + +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=40` + +*Pasting that findings_next=: the same rule under a 10x budget counts its true total — no count_capped=, no findings_next=.* + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto done; +goto done; + +````` + ## `./build/ripwire . --lint --lint-select=cach-` *An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* diff --git a/test/showcase_capture.py b/test/showcase_capture.py index ee03fbab3..c315de5f7 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -677,6 +677,8 @@ def chooseInWindow( lo=2, hi=14 ): add(S4B, f"{BIN} . --legend-dict", "The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.") add(S4B, f"{BIN} . --lint --lint-select=cache-", "Run ONLY one rule family; the root carries selected=\"K of N\" so a filtered zero is never confusable with an unfiltered one.") add(S4B, f"{BIN} . --lint --lint-ignore=naming-,cache-", "DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.") +add(S4B, f"{BIN} . --lint --lint-select=goto --lint-max-per-rule=4", "A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped=\"1\", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget.") +add(S4B, f"{BIN} . --lint --lint-select=goto --lint-max-per-rule=40", "Pasting that findings_next=: the same rule under a 10x budget counts its true total — no count_capped=, no findings_next=.") add(S4B, f"{BIN} . --lint --lint-select=cach-", "An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).") add(S4B, f"{BIN} . --lint --lint-select=nosuchfamily", "A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.") add(S4B, f"{BIN} . --lint --sarif", "The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.", From d415b0f3d28f48ca63f9191af5b8c942c5554dc0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 22:43:28 -0400 Subject: [PATCH 157/229] resolve: Java/C#/Kotlin/Swift declared receivers prove their class again FE-B proved a member call only through receiver evidence, and the evidence capture covered Python, JS/TS and Go, so every Java/C#/Kotlin/Swift call on a typed parameter, local or field printed via="name" where the base had bound it. The declarations are now read: a typed parameter, a typed or constructed local (Java/C# `new T()`, Java `var`, Kotlin/Swift `T()` with a capitalized name), a typed or constructed field / property (C# properties, Kotlin `val` constructor properties). A bare field read inside a method is the class's field unless a parameter or local DECLARES that name (Java's copied field names only bind it). An untyped local is recorded as a declaration with no class, so it hides the field and proves nothing. receiverevidencecheck (T): Pump fixtures in the four roots, two classes that both define spill(); 34 arms red on the previous head, all green. Near misses stay hedged: an untyped local, a local that hides the field, an interface or protocol receiver with two implementors (both still listed). kParserVer 150 -> 152 (151 is 150's rich tag); qschemetrip re-pinned (logged). CHANGELOG: the fix round's three changes and the re-measured cost (+2.04%). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 24 +- src/ingest_binds.h | 220 +++++++++++++++++- src/ingest_cache.h | 5 +- src/ingest_sidecap.h | 7 +- src/quality.h | 2 +- src/receiverevidence.h | 36 +++ test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 3 + test/receiverevidencecheck.sh | 49 ++++ test/receiverevidencefix/cs/App/Pump.cs | 26 +++ .../java/src/main/java/app/Pump.java | 23 ++ test/receiverevidencefix/kt/src/app/Pump.kt | 25 ++ .../swift/Sources/App/Pump.swift | 22 ++ 13 files changed, 426 insertions(+), 18 deletions(-) create mode 100644 test/receiverevidencefix/cs/App/Pump.cs create mode 100644 test/receiverevidencefix/java/src/main/java/app/Pump.java create mode 100644 test/receiverevidencefix/kt/src/app/Pump.kt create mode 100644 test/receiverevidencefix/swift/Sources/App/Pump.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6509ba6d0..c5ce59256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,14 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. - **Receivers the source types now resolve** (JavaScript, TypeScript, Python, Go, Java, Kotlin, C#, Swift): `this` / `self` / `cls` inside a class and `super` / `base` through its bases; a parameter or local whose class is written - (a TS/Python/Go annotation, a Go method receiver, a JS `new Foo()`, Python `Foo()` or Go `Foo{}` initializer, an - import alias of the class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields - whose classes are stated (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields); - Python `feed = parser.feed`. A JS/TS call on the class object reaches its `static` members only, and a call on an + (a TS/Python/Go/Java/C#/Kotlin/Swift annotation or declared type, a Go method receiver, a JS `new Foo()`, Java/C# + `new Foo()` (`var` included), Python `Foo()`, Kotlin/Swift `Foo()` or Go `Foo{}` initializer, an import alias of the + class); a constructed receiver (`new Foo().m()`); a class-name receiver; a chain of fields whose classes are stated + (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields, Java/C# fields and + properties, Kotlin/Swift properties and Kotlin `val` constructor properties, read through `this`/`self` or bare); + Python `feed = parser.feed`. A Go module alias proves its package by the exact import path: the nearest `go.mod`'s + module path plus the directory below it, or the path a local `replace` gives a directory. An untyped local, a local + that hides a field, and an interface-typed receiver with several implementors stay name-only. A JS/TS call on the class object reaches its `static` members only, and a call on an instance never one. A Ruby call's candidates that Ruby's own method lookup proves (the entries below) are its answer, never `via="name"`. - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an @@ -42,11 +46,15 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. false. Rule 3's include-file narrow no longer decides a member call: the file a caller imports says nothing about its receiver. Ranking keeps the old ladder's pick (the same-file, else same-directory rung): the extra candidates are listed at edge weight zero, so a guess never moves PageRank. -- Cost, measured over 210 calls on eight public repositories (`--no-cache`): +8.55% bytes in total; 32% of the - ``/``/`` rows carry `via="name"`, most of them the map's `` rows (C trees: no change). The gate is +- **The map merges what it would repeat.** A map `` printed one `` row per same-named by-name candidate, N + byte-identical rows; it now prints such a `via="name"` group once with `x="N"`, and `--callees` on that symbol + lists every candidate with its file. The map's legend is a short spelling of the same reading (and defines `x=`); + the compact dialect states both in its element rows. The README's `--max-tokens=3000` map keeps 20 rows. +- Cost, measured over 210 calls on eight public repositories (`--no-cache`): +2.04% bytes in total (C trees: no + change); the `--max-tokens=3000` maps of the eight repositories keep 337 rows (331 before the change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses - for every evidence rule). Ingest records the receiver chain (`kParserVer` 150, `kCacheVersion` 29), so a cache - written by an earlier build is re-parsed. + for every evidence rule). Ingest records the receiver chain and the Java/C#/Kotlin/Swift declarations + (`kParserVer` 152, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` diff --git a/src/ingest_binds.h b/src/ingest_binds.h index c051a2b4a..2f4a76508 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -4126,13 +4126,34 @@ inline std::string_view annotatedClass( TSNode node, std::string_view src ) { node = fieldChild( node, NodeField::Name ); } - else if( kindIs( t, "generic_type" ) ) // TS `T` / Go `T[U]`: the class, not the argument + else if( kindIs( t, "generic_type" ) || kindIs( t, "generic_name" ) ) // TS `T` / Go `T[U]` / Java / C# `T`: the class { node = ts_node_named_child_count( node ) > 0 ? ts_node_named_child( node, 0 ) : TSNode{}; } + else if( kindIs( t, "nullable_type" ) || kindIs( t, "optional_type" ) ) // C# / Kotlin `T?`, Swift `T?`: the class + { + node = ts_node_named_child_count( node ) > 0 ? ts_node_named_child( node, 0 ) : TSNode{}; + } + else if( kindIs( t, "user_type" ) || kindIs( t, "scoped_type_identifier" ) ) // Kotlin / Swift `a.B`, Java `a.B`: the last + { // name segment, never a type argument + TSNode last{}; + for( std::uint32_t i = 0, k = ts_node_named_child_count( node ); i < k; ++i ) + { + const TSNode c = ts_node_named_child( node, i ); + if( kindIs( ts_node_type( c ), "type_identifier" ) ) + { + last = c; + } + } + node = last; + } + else if( kindIs( t, "qualified_name" ) ) // C# `a.B` + { + node = fieldChild( node, NodeField::Name ); + } else { - return {}; + return {}; // an array, a function type, C#'s implicit `var`, a predefined/primitive type: no single class } } return {}; @@ -4176,10 +4197,196 @@ inline std::string_view constructedBy( TSNode value, Lang lang, std::string_view const TSNode fn = fieldChild( value, NodeField::Function ); return ( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "identifier" ) ) ? nodeTextOf( fn, src ) : std::string_view{}; } + if( lang == Lang::Kotlin || lang == Lang::Swift ) + { + // `Store( … )` is spelled like a function call: a capitalized callee is read as the class it constructs (the house + // convention of both languages). A lower-case factory (`make()`) names no class, and a capitalized name that is no + // class in the tree proves nothing downstream (ReceiverEvidence finds no `Name::member`). + if( !kindIs( ts_node_type( value ), "call_expression" ) || ts_node_named_child_count( value ) == 0 ) + { + return {}; + } + const TSNode callee = ts_node_named_child( value, 0 ); + const std::string_view name = kindIs( ts_node_type( callee ), "simple_identifier" ) ? nodeTextOf( callee, src ) : std::string_view{}; + return ( !name.empty() && name.front() >= 'A' && name.front() <= 'Z' ) ? name : std::string_view{}; + } return constructedClass( value, lang, src ); } -// FE-B: one node's receiver evidence. Python, JS/TS and Go only — the languages whose member calls carry no type rule today. +// FE-B: Java's `var` is inference, not a class: the written type, unless it is `var`, else what the initializer constructs +inline std::string_view declaredOrConstructed( TSNode type, TSNode value, Lang lang, std::string_view src ) +{ + std::string_view cls = annotatedClass( type, src ); + if( cls == "var" && lang == Lang::Java ) + { + cls = {}; + } + return cls.empty() ? constructedBy( value, lang, src ) : cls; +} + +// FE-B (review B3): one declarator's evidence — `name` declared with the class its written `type` names, else the class its +// initializer `value` constructs. A parameter or local whose class is unknown (`var s = make()`) is still a DECLARATION: +// recorded with no class (a tombstone), so it hides a same-named field (receiverevidence.h declaresLocal) and proves nothing. +inline void pushDeclaratorEvidence( BindCtx& cx, LocalBindKind kind, TSNode type, TSNode name, TSNode value, std::uint32_t at ) +{ + if( ts_node_is_null( name ) || !( kindIs( ts_node_type( name ), "identifier" ) || kindIs( ts_node_type( name ), "simple_identifier" ) ) ) + { + return; + } + const std::string_view cls = declaredOrConstructed( type, value, cx.lang, cx.src ); + if( !cls.empty() || kind != LocalBindKind::RecvType ) + { + pushEvidenceBind( cx, kind, nodeTextOf( name, cx.src ), cls, {}, at, false ); + return; + } + RawBind b; + b.fileId = cx.fileId; + b.startByte = at; + b.lang = cx.lang; + b.kind = LocalBindKind::RecvType; + b.var.assign( nodeTextOf( name, cx.src ) ); + cx.binds->push_back( std::move( b ) ); +} + +// the first (or, with `last`, the last) named child of `node` whose kind is one of `kinds`; null when none — Kotlin's grammar +// carries no field names and Swift's reuses `name` for the parameter, its label and its type +inline TSNode namedChildOfKind( TSNode node, std::initializer_list kinds, bool last = false ) +{ + TSNode hit{}; + for( std::uint32_t i = 0, k = ts_node_named_child_count( node ); i < k; ++i ) + { + const TSNode c = ts_node_named_child( node, i ); + if( std::find( kinds.begin(), kinds.end(), std::string_view( ts_node_type( c ) ) ) != kinds.end() ) + { + hit = c; + if( !last ) + { + break; + } + } + } + return hit; +} + +// FE-B (review B3): Java — a typed parameter, and each declarator of a local or field declaration +inline void captureJavaDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + if( kindIs( t, "formal_parameter" ) ) + { + pushDeclaratorEvidence( cx, LocalBindKind::RecvType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), TSNode{}, at ); + return; + } + if( !kindIs( t, "local_variable_declaration" ) && !kindIs( t, "field_declaration" ) ) + { + return; + } + const LocalBindKind kind = kindIs( t, "field_declaration" ) ? LocalBindKind::MemberType : LocalBindKind::RecvType; + const TSNode type = fieldChild( n, NodeField::Type ); + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + const TSNode d = ts_node_named_child( n, i ); + if( kindIs( ts_node_type( d ), "variable_declarator" ) ) + { + pushDeclaratorEvidence( cx, kind, type, fieldChild( d, NodeField::Name ), fieldChild( d, NodeField::Value ), at ); + } + } +} + +// FE-B (review B3): C# — a typed parameter, each declarator of a local or field declaration, and a property +inline void captureCSharpDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + if( kindIs( t, "parameter" ) ) + { + pushDeclaratorEvidence( cx, LocalBindKind::RecvType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), TSNode{}, at ); + return; + } + if( kindIs( t, "property_declaration" ) ) + { + pushDeclaratorEvidence( cx, LocalBindKind::MemberType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), fieldChild( n, NodeField::Value ), at ); + return; + } + if( !kindIs( t, "variable_declaration" ) ) + { + return; + } + // a field's declaration sits in a field_declaration, a local's in a local_declaration_statement (or a using/for) + const TSNode up = ts_node_parent( n ); + const LocalBindKind kind = ( !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "field_declaration" ) ) ? LocalBindKind::MemberType : LocalBindKind::RecvType; + const TSNode type = fieldChild( n, NodeField::Type ); + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + const TSNode d = ts_node_named_child( n, i ); + if( kindIs( ts_node_type( d ), "variable_declarator" ) ) + { + // the initializer is the declarator's last named child, when it has one besides the name (no field names it) + const TSNode name = fieldChild( d, NodeField::Name ); + const TSNode value = ts_node_named_child_count( d ) > 1 ? ts_node_named_child( d, ts_node_named_child_count( d ) - 1 ) : TSNode{}; + pushDeclaratorEvidence( cx, kind, type, name, ts_node_eq( value, name ) ? TSNode{} : value, at ); + } + } +} + +// FE-B (review B3): Kotlin and Swift — a typed parameter (Kotlin `val` constructor parameters are properties), and a local +// or property declaration (a property in a class body, a local elsewhere) +inline void captureKotlinSwiftDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + if( kindIs( t, "parameter" ) || ( cx.lang == Lang::Kotlin && kindIs( t, "class_parameter" ) ) ) + { + const bool property = kindIs( t, "class_parameter" ); + if( property && ts_node_is_null( namedChildOfKind( n, { "binding_pattern_kind" } ) ) ) + { + return; // a plain constructor parameter, not a property + } + // Swift `label p: T`: the LAST identifier is the parameter's own name + pushDeclaratorEvidence( cx, property ? LocalBindKind::MemberType : LocalBindKind::RecvType, + namedChildOfKind( n, { "user_type", "nullable_type", "optional_type" }, true ), + namedChildOfKind( n, { "simple_identifier" }, true ), TSNode{}, at ); + return; + } + if( !kindIs( t, "property_declaration" ) ) + { + return; + } + const TSNode up = ts_node_parent( n ); + const LocalBindKind kind = ( !ts_node_is_null( up ) && ( kindIs( ts_node_type( up ), "class_body" ) || kindIs( ts_node_type( up ), "enum_class_body" ) ) ) + ? LocalBindKind::MemberType : LocalBindKind::RecvType; + if( cx.lang == Lang::Kotlin ) + { + const TSNode var = namedChildOfKind( n, { "variable_declaration" } ); // null for `val ( a, b ) = …`: not this rule's question + if( !ts_node_is_null( var ) ) + { + pushDeclaratorEvidence( cx, kind, namedChildOfKind( var, { "user_type", "nullable_type" } ), namedChildOfKind( var, { "simple_identifier" } ), + namedChildOfKind( n, { "call_expression" } ), at ); + } + return; + } + const TSNode pattern = fieldChild( n, NodeField::Name ); + const TSNode ann = namedChildOfKind( n, { "type_annotation" } ); + pushDeclaratorEvidence( cx, kind, ts_node_is_null( ann ) ? TSNode{} : namedChildOfKind( ann, { "user_type", "optional_type" } ), + ts_node_is_null( pattern ) ? TSNode{} : namedChildOfKind( pattern, { "simple_identifier" } ), fieldChild( n, NodeField::Value ), at ); +} + +// FE-B (review B3): a declaration's evidence in Java, C#, Kotlin and Swift — a typed parameter, a typed or constructed +// local (RecvType), a typed or constructed field / property (MemberType). The base resolver bound these receivers by name +// alone; FE-B proves a member call only through evidence, so the declarations are read here. +inline void captureTypedDeclEvidence( BindCtx& cx, TSNode n, const char* t ) +{ + const std::uint32_t at = ts_node_start_byte( n ); + if( cx.lang == Lang::Java ) + { + captureJavaDeclEvidence( cx, n, t, at ); + } + else if( cx.lang == Lang::CSharp ) + { + captureCSharpDeclEvidence( cx, n, t, at ); + } + else + { + captureKotlinSwiftDeclEvidence( cx, n, t, at ); + } +} + +// FE-B: one node's receiver evidence: Python, JS/TS and Go, and (review B3) Java, C#, Kotlin and Swift declarations. inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) { const Lang lang = cx.lang; @@ -4324,6 +4531,11 @@ inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) } return; } + if( lang == Lang::Java || lang == Lang::CSharp || lang == Lang::Kotlin || lang == Lang::Swift ) + { + captureTypedDeclEvidence( cx, n, t ); + return; + } if( lang != Lang::Go ) { return; @@ -4549,7 +4761,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // DEFINITION's named parameters and a range-for's loop variable. Unconditional (the helper gates // language and node type itself); disjoint from every branch of the Rule-2 chain above. captureShadowScopeDecls( n, t, fileId, lang, src, binds ); - captureReceiverEvidence( cx, n, t ); // FE-B: typed parameters/locals, field types, method aliases (Python, JS/TS, Go) + captureReceiverEvidence( cx, n, t ); // FE-B: typed parameters/locals, field types, method aliases (Python, JS/TS, Go, Java, C#, Kotlin, Swift) // ── L3 fn-pointer/callback capture (C/C++/ObjC) — a SEPARATE if (not part of the Rule-2 chain above): // the same `declaration` node can carry BOTH a Rule-2 var→type fact and a var→function fact diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 7e8b49658..662b340a9 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -312,7 +312,10 @@ constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiver // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 150; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 152; // bump on any grammar/.scm/extraction change + // 152 = lane FE-B fix round 1 (review B3): Java/C#/Kotlin/Swift typed parameters, locals, + // fields and constructed declarations record RecvType/MemberType. 152, not 151: 150's + // rich file tag is 151; the train renumbers if it collides. // 150 = lane FE-B (receiver evidence) merged onto train 26a's 148: the receiver chain // (memberPath/memberCtor), the RecvType/MemberType/MethodAlias/NameAlias/StaticMember // bindings, appended after #373's Ruby kinds. 150, not 149: 148's RICH file tag is 149; diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 7120fe33c..9afdea00f 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -1602,11 +1602,12 @@ void captureSideFacts( const LangEntry& le, std::uint32_t fileId, std::string_vi { arms.rust = &rustCtx; } - // FE-B: JavaScript and Go join for the receiver-evidence records alone (captureReceiverEvidence); every other - // branch of the pass gates its own language, so their record streams gain exactly those kinds. + // FE-B: JavaScript and Go (and, review B3, C#, Kotlin and Swift) join for the receiver-evidence records alone + // (captureReceiverEvidence); every other branch of the pass gates its own language, so their record streams gain + // exactly those kinds. if( le.lang == Lang::Cpp || le.lang == Lang::ObjC || le.lang == Lang::Python || le.lang == Lang::TypeScript || le.lang == Lang::Java || le.lang == Lang::JavaScript || le.lang == Lang::Go - || le.lang == Lang::C ) + || le.lang == Lang::C || le.lang == Lang::CSharp || le.lang == Lang::Kotlin || le.lang == Lang::Swift ) { arms.bind = &bindCtx; } diff --git a/src/quality.h b/src/quality.h index 15dc7a764..ab38597cb 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 150; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 152; // MUST equal ingest.cpp's kParserVer (gated) // 150 = lane FE-B on train 26a (148 = train 26a: PR #373; 146/145 = lane FE-B builds; above 148's rich tag 149) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 21017097e..0cadc2815 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -83,6 +83,12 @@ inline bool implicitReceiverLang( Lang l ) noexcept return l == Lang::Java || l == Lang::Kotlin || l == Lang::CSharp || l == Lang::Swift || l == Lang::Cpp || l == Lang::Ruby; } +// FE-B (review B3): the languages where a bare name inside a method may be a field of the enclosing class (implicit this) +inline bool implicitFieldLang( Lang l ) noexcept +{ + return l == Lang::Java || l == Lang::CSharp || l == Lang::Kotlin || l == Lang::Swift; +} + // the languages where a call written on a receiver reaches a function only as a METHOD (or through a module alias): a // free function is never what `x.f()` calls. JS/TS (an object property may hold one), Kotlin (an extension function is a // top-level `fun T.f()`) and C (a function-pointer field) are left out. @@ -436,6 +442,26 @@ struct ReceiverEvidence return localNames->contains( keyOf( s, '#', name ) ) || localType.contains( keyOf( s, '#', name ) ); } ) != kNoNode; } + // does `from` or a function enclosing it DECLARE `name` (a parameter or a local, typed or not) — not merely bind it: Java + // copies each field name onto its class's methods as a bound-only name (graph.h shadowJavaFieldsOntoMethods), which must + // not hide the field itself + bool declaresLocal( NodeId from, std::string_view name ) const + { + return walkScopes( from, [ & ]( NodeId s ) + { + const std::string& k = keyOf( s, '#', name ); + if( localType.contains( k ) ) + { + return true; + } + if( localNames == nullptr ) + { + return false; + } + const auto it = localNames->find( k ); + return it != localNames->end() && ( it->second & kLocalNameDeclared ) != 0; + } ) != kNoNode; + } // a receiver root that names a CLASS at this site: a class-name receiver language, a class of that name, and no local // of the caller hiding it (`Interval = make(); Interval.validate( v )`) bool namesClass( const Reference& r, std::string_view root, std::string_view cls ) const @@ -616,6 +642,16 @@ struct ReceiverEvidence { return unalias( r.fileId, *t ); } + if( implicitFieldLang( r.lang ) && !declaresLocal( r.fromSymbol, root ) ) + { + // Java/C#/Kotlin/Swift: a bare `field.m()` inside a method is `this.field.m()` — the field the caller's class (or a + // base) declares, when no parameter or local of that name hides it + const std::string_view owner = callerClass( r.fromSymbol ); + if( const std::string_view ft = owner.empty() ? std::string_view{} : fieldOf( owner, root ); !ft.empty() ) + { + return unalias( r.fileId, ft ); + } + } const std::string_view cls = unalias( r.fileId, root ); if( namesClass( r, root, cls ) ) { diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index cf28b9b28..034da2cb4 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -abf245ceffbd202cc04f8ec62ea0085e72645190b9eef989b834c8d90c678aeb +e6abc48760ac5a37459cfb78a07ba46580d1e102d6b16212da342df4c3048773 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 745e609c2..0a34e0308 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,9 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-07, lane FE-B fix round 1 (review B3): RE-DERIVED with UPDATE_GOLDEN=1 (hash e6abc48760…048773). kParserVer +# 150 -> 152 (Java/C#/Kotlin/Swift declarations record RecvType/MemberType; 151 is 150's rich tag), the quality.h +# mirror with it. kCacheVersion and kQSnapCacheScheme unchanged. The train renumbers if needed. # 2026-10-07, lane FE-B merged onto train 26a (cd87e30e): RE-DERIVED with UPDATE_GOLDEN=1 (hash abf245ceff…678aeb). # kParserVer 148 -> 150 (FE-B's receiver chain + bind kinds on top of #373; 149 is 148's rich tag), kCacheVersion # 28 -> 29 (FE-B's ref record), quality.h mirrors with them. kQSnapCacheScheme stays 17. The train renumbers if needed. diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index 0d70fec34..e32447f9a 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -87,6 +87,11 @@ # a replace onto lib/ itself, a replaced legacy/ with and without its own go.mod, a nested module, # example.com/a vs example.com/ab, a vendored copy (never the in-repo lib/), a dot import (never the decoy), # and a path-less root lib/ beside a nested module that carries the path. +# (T) Java/C#/Kotlin/Swift (review B3): a typed parameter, a typed / constructed / Java `var` local, a typed or +# constructed field or property — read bare (implicit this) or through this/self — and a Kotlin `val` +# constructor property RESOLVE to the declared class's member (Pump.*: Tank and Barrel both define spill). +# Near misses stay hedged: an untyped local (a lower-case factory), a local that hides the field, an +# interface/protocol-typed receiver with two implementors (both still listed). # (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE # the tree (or that imports the name from outside) never proves an unrelated class's method. Near # misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), @@ -488,6 +493,50 @@ notproven gomod/vend callees app/app.go:Use "Mark lib/l.go" # near notproven gomod/dot callees app/app.go:Use "Mark decoy/d.go" # near miss: a dot import (lib/ unresolved: a floor) exactproven gomod/mixed callees m/app/app.go:Use "fn Mark m/lib/l.go" # near miss: a path-less lib/ never takes a carried path +echo "=== (T) Java/C#/Kotlin/Swift: declared receivers RESOLVE (review B3) ===" +exactproven java callees src/main/java/app/Pump.java:param "method spill src/main/java/app/Pump.java" +exactproven java callees src/main/java/app/Pump.java:typedLocal "method spill src/main/java/app/Pump.java;method makeTank src/main/java/app/Pump.java" +exactproven java callees src/main/java/app/Pump.java:ctorLocal "cls Tank src/main/java/app/Pump.java;method spill src/main/java/app/Pump.java" +exactproven java callees src/main/java/app/Pump.java:varLocal "cls Tank src/main/java/app/Pump.java;method spill src/main/java/app/Pump.java" +exactproven java callees src/main/java/app/Pump.java:implicitField "method spill src/main/java/app/Pump.java" +exactproven java callees src/main/java/app/Pump.java:thisField "method spill src/main/java/app/Pump.java" +proven java callees src/main/java/app/Pump.java:param "method spill src/main/java/app/Pump.java:7" +notproven java callees src/main/java/app/Pump.java:untyped "spill src/main/java/app/Pump.java:7" "spill src/main/java/app/Pump.java:8" # near miss: an untyped local +notproven java callees src/main/java/app/Pump.java:shadow "spill src/main/java/app/Pump.java:7" "spill src/main/java/app/Pump.java:8" # near miss: a local hides the field +notproven java callees src/main/java/app/Pump.java:viaIface "spill src/main/java/app/Pump.java:7" "spill src/main/java/app/Pump.java:8" # near miss: an interface, two implementors +visible java callees src/main/java/app/Pump.java:viaIface "spill src/main/java/app/Pump.java:7" "spill src/main/java/app/Pump.java:8" +exactproven cs callees App/Pump.cs:Param "method Spill App/Pump.cs" +exactproven cs callees App/Pump.cs:TypedLocal "method Spill App/Pump.cs;method MakeTank App/Pump.cs" +exactproven cs callees App/Pump.cs:CtorLocal "method Spill App/Pump.cs;cls Tank App/Pump.cs" +exactproven cs callees App/Pump.cs:ImplicitField "method Spill App/Pump.cs" +exactproven cs callees App/Pump.cs:ThisField "method Spill App/Pump.cs" +proven cs callees App/Pump.cs:ViaProperty "method Spill App/Pump.cs:8" +notproven cs callees App/Pump.cs:Untyped "Spill App/Pump.cs:7" "Spill App/Pump.cs:8" +notproven cs callees App/Pump.cs:Shadow "Spill App/Pump.cs:7" "Spill App/Pump.cs:8" +notproven cs callees App/Pump.cs:ViaIface "Spill App/Pump.cs:7" "Spill App/Pump.cs:8" +visible cs callees App/Pump.cs:ViaIface "Spill App/Pump.cs:7" "Spill App/Pump.cs:8" +exactproven kt callees src/app/Pump.kt:param "fn spill src/app/Pump.kt" +exactproven kt callees src/app/Pump.kt:typedLocal "fn spill src/app/Pump.kt;fn makeTank src/app/Pump.kt" +exactproven kt callees src/app/Pump.kt:ctorLocal "cls Tank src/app/Pump.kt;fn spill src/app/Pump.kt" +exactproven kt callees src/app/Pump.kt:implicitField "fn spill src/app/Pump.kt" +exactproven kt callees src/app/Pump.kt:thisField "fn spill src/app/Pump.kt" +proven kt callees src/app/Pump.kt:ctorProperty "fn spill src/app/Pump.kt:8" +proven kt callees src/app/Pump.kt:constructedProperty "fn spill src/app/Pump.kt:7" +notproven kt callees src/app/Pump.kt:untyped "spill src/app/Pump.kt:7" "spill src/app/Pump.kt:8" +notproven kt callees src/app/Pump.kt:shadow "spill src/app/Pump.kt:7" "spill src/app/Pump.kt:8" +notproven kt callees src/app/Pump.kt:viaIface "spill src/app/Pump.kt:7" "spill src/app/Pump.kt:8" +visible kt callees src/app/Pump.kt:viaIface "spill src/app/Pump.kt:7" "spill src/app/Pump.kt:8" +exactproven swift callees Sources/App/Pump.swift:param "fn spill Sources/App/Pump.swift" +exactproven swift callees Sources/App/Pump.swift:typedLocal "fn spill Sources/App/Pump.swift;fn makeTank Sources/App/Pump.swift" +exactproven swift callees Sources/App/Pump.swift:ctorLocal "cls Tank Sources/App/Pump.swift;fn spill Sources/App/Pump.swift" +exactproven swift callees Sources/App/Pump.swift:implicitField "fn spill Sources/App/Pump.swift" +exactproven swift callees Sources/App/Pump.swift:selfField "fn spill Sources/App/Pump.swift" +proven swift callees Sources/App/Pump.swift:constructedProperty "fn spill Sources/App/Pump.swift:6" +notproven swift callees Sources/App/Pump.swift:untyped "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" +notproven swift callees Sources/App/Pump.swift:shadow "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" +notproven swift callees Sources/App/Pump.swift:viaIface "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" +visible swift callees Sources/App/Pump.swift:viaIface "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" + echo "=== (I) implicit receiver: a bare call never proves an unrelated class's method ===" notproven java callees src/main/java/app/Logger.java:line "render src/main/java/app/Exporter.java" notproven java callees src/main/java/app/Logger.java:close "flush src/main/java/app/Buffer.java" "flush src/main/java/app/Exporter.java" diff --git a/test/receiverevidencefix/cs/App/Pump.cs b/test/receiverevidencefix/cs/App/Pump.cs new file mode 100644 index 000000000..7f12ef22c --- /dev/null +++ b/test/receiverevidencefix/cs/App/Pump.cs @@ -0,0 +1,26 @@ +// Review B3: C# receivers the source TYPES resolve (a typed parameter, a typed / constructed / `var` local, a typed field +// read bare or through `this`, a typed property), against two classes that both define Spill(). Near misses stay hedged: +// an untyped local, a local that hides the field, an interface-typed receiver with two implementors. +namespace App +{ + interface ISpiller { void Spill(); } + class Tank : ISpiller { public void Spill() {} } + class Barrel : ISpiller { public void Spill() {} } + + public class Pump + { + private Tank tank = new Tank(); + public Barrel Keg { get; } = new Barrel(); + void Param(Tank t) { t.Spill(); } + void TypedLocal() { Tank t = MakeTank(); t.Spill(); } + void CtorLocal() { var t = new Tank(); t.Spill(); } + void ImplicitField() { tank.Spill(); } + void ThisField() { this.tank.Spill(); } + void ViaProperty() { Keg.Spill(); } + void Untyped() { var x = MakeAny(); x.Spill(); } + void Shadow() { var tank = MakeAny(); tank.Spill(); } + void ViaIface(ISpiller s) { s.Spill(); } + Tank MakeTank() { return null; } + dynamic MakeAny() { return null; } + } +} diff --git a/test/receiverevidencefix/java/src/main/java/app/Pump.java b/test/receiverevidencefix/java/src/main/java/app/Pump.java new file mode 100644 index 000000000..a539fbfbf --- /dev/null +++ b/test/receiverevidencefix/java/src/main/java/app/Pump.java @@ -0,0 +1,23 @@ +package app; + +// Review B3: Java receivers the source TYPES resolve (a typed parameter, a typed / constructed / `var` local, a typed field +// read bare or through `this`), against two classes that both define spill(). Near misses stay hedged: an untyped local, +// a local that hides the field, an interface-typed receiver with two implementors. +interface Spiller { void spill(); } +class Tank implements Spiller { public void spill() {} } +class Barrel implements Spiller { public void spill() {} } + +public class Pump { + private Tank tank = new Tank(); + void param(Tank t) { t.spill(); } + void typedLocal() { Tank t = makeTank(); t.spill(); } + void ctorLocal() { Tank t = new Tank(); t.spill(); } + void varLocal() { var t = new Tank(); t.spill(); } + void implicitField() { tank.spill(); } + void thisField() { this.tank.spill(); } + void untyped() { var x = makeAny(); x.spill(); } + void shadow() { var tank = makeAny(); tank.spill(); } + void viaIface(Spiller s) { s.spill(); } + Tank makeTank() { return null; } + Object makeAny() { return null; } +} diff --git a/test/receiverevidencefix/kt/src/app/Pump.kt b/test/receiverevidencefix/kt/src/app/Pump.kt new file mode 100644 index 000000000..b416119ab --- /dev/null +++ b/test/receiverevidencefix/kt/src/app/Pump.kt @@ -0,0 +1,25 @@ +package app + +// Review B3: Kotlin receivers the source TYPES resolve (a typed parameter, a typed / constructed local, a typed or +// constructed property, a `val` constructor property), against two classes that both define spill(). Near misses stay +// hedged: an untyped local from a lower-case factory, a local that hides the property, an interface-typed receiver. +interface Spiller { fun spill() } +class Tank : Spiller { override fun spill() {} } +class Barrel : Spiller { override fun spill() {} } + +class Pump(val keg: Barrel) { + private val tank: Tank = Tank() + var spare = Tank() + fun param(t: Tank) { t.spill() } + fun typedLocal() { val t: Tank = makeTank(); t.spill() } + fun ctorLocal() { val t = Tank(); t.spill() } + fun implicitField() { tank.spill() } + fun thisField() { this.tank.spill() } + fun ctorProperty() { keg.spill() } + fun constructedProperty() { spare.spill() } + fun untyped() { val x = makeAny(); x.spill() } + fun shadow() { val tank = makeAny(); tank.spill() } + fun viaIface(s: Spiller) { s.spill() } + fun makeTank(): Tank = Tank() + fun makeAny(): Any = Tank() +} diff --git a/test/receiverevidencefix/swift/Sources/App/Pump.swift b/test/receiverevidencefix/swift/Sources/App/Pump.swift new file mode 100644 index 000000000..1b8700161 --- /dev/null +++ b/test/receiverevidencefix/swift/Sources/App/Pump.swift @@ -0,0 +1,22 @@ +// Review B3: Swift receivers the source TYPES resolve (a typed parameter, a typed / constructed local, a typed or +// constructed property read bare or through `self`), against two classes that both define spill(). Near misses stay +// hedged: an untyped local from a lower-case factory, a local that hides the property, a protocol-typed receiver. +protocol Spiller { func spill() } +class Tank: Spiller { func spill() {} } +class Barrel: Spiller { func spill() {} } + +class Pump { + var tank: Tank = Tank() + let keg = Barrel() + func param(t: Tank) { t.spill() } + func typedLocal() { let t: Tank = makeTank(); t.spill() } + func ctorLocal() { let t = Tank(); t.spill() } + func implicitField() { tank.spill() } + func selfField() { self.tank.spill() } + func constructedProperty() { keg.spill() } + func untyped() { let x = makeAny(); x.spill() } + func shadow() { let tank = makeAny(); tank.spill() } + func viaIface(s: Spiller) { s.spill() } + func makeTank() -> Tank { return Tank() } + func makeAny() -> Any { return Tank() } +} From b1e92644e7a08cd03575d633fb338f8b1a3fb728 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 22:47:59 -0400 Subject: [PATCH 158/229] refactor: the bare-field rule reads implicitReceiverLang (no second language list) Co-Authored-By: Claude Opus 5.5 --- src/receiverevidence.h | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 0cadc2815..9963d0052 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -83,12 +83,6 @@ inline bool implicitReceiverLang( Lang l ) noexcept return l == Lang::Java || l == Lang::Kotlin || l == Lang::CSharp || l == Lang::Swift || l == Lang::Cpp || l == Lang::Ruby; } -// FE-B (review B3): the languages where a bare name inside a method may be a field of the enclosing class (implicit this) -inline bool implicitFieldLang( Lang l ) noexcept -{ - return l == Lang::Java || l == Lang::CSharp || l == Lang::Kotlin || l == Lang::Swift; -} - // the languages where a call written on a receiver reaches a function only as a METHOD (or through a module alias): a // free function is never what `x.f()` calls. JS/TS (an object property may hold one), Kotlin (an extension function is a // top-level `fun T.f()`) and C (a function-pointer field) are left out. @@ -642,10 +636,11 @@ struct ReceiverEvidence { return unalias( r.fileId, *t ); } - if( implicitFieldLang( r.lang ) && !declaresLocal( r.fromSymbol, root ) ) + // Java/C#/Kotlin/Swift (the implicit-receiver languages but C++, whose Rule 2b reads fields, and Ruby, whose fields + // are @ivars): a bare `field.m()` inside a method is `this.field.m()` — the field the caller's class (or a base) + // declares, when no parameter or local of that name hides it + if( implicitReceiverLang( r.lang ) && r.lang != Lang::Cpp && r.lang != Lang::Ruby && !declaresLocal( r.fromSymbol, root ) ) { - // Java/C#/Kotlin/Swift: a bare `field.m()` inside a method is `this.field.m()` — the field the caller's class (or a - // base) declares, when no parameter or local of that name hides it const std::string_view owner = callerClass( r.fromSymbol ); if( const std::string_view ft = owner.empty() ? std::string_view{} : fieldOf( owner, root ); !ft.empty() ) { From 90557fb19846beadf3f25e31aed4ad7c45f2aecd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:04:18 -0400 Subject: [PATCH 159/229] =?UTF-8?q?fix(mcp=20for):=20paid=20pass=20first,?= =?UTF-8?q?=20unpaid=20when=20the=20paid=20answer=20still=20lands=20over?= =?UTF-8?q?=20=E2=80=94=20the=20CLI's=20rule;=20arms=20path-independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 89422abc ran the MCP `for` assembly UNPAID first and predicted the paid answer's fit from "the cut alone minus the handle". That prediction is wrong where paying drops a whole row: the freed bytes can exceed the handle's, so an answer over its ceiling unpaid can fit paid (255dc199 at budget_tokens=3800 on forrankordercheck's fixture: 35 rows est 3824, over; paid 33 rows est 3773, fits). Now the rule is the CLI twin's, measured not predicted: the paid pass first (the payable case is the answer it always was), the unpaid pass only when the paid answer's est_tokens still exceeds budget_tokens. One ranking, one or two assemblies. sigsCutContinuationBytes (the prediction's helper) is gone. forrankordercheck (10) calls MCP by a relative path from $TMP (root="fxcut"): the header bytes the rows are budgeted against no longer depend on the temp dir's path, so the pinned 255dc199 row counts hold on any machine. Over-anyway arms at 2000/3000 (>= 14/25 rows; RED on dd6e4c8e: 12/23), payable at 3800 (fits; RED on a never-pay mutant: 35 rows, est 3934). fordisclosurecheck #2c's premise reads the document's shape (root over_ceiling="1", a capped with next=), not the clause text, so it runs its two checks on dd6e4c8e too (both RED there). Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 50 +++++++++++++++----------------------- src/serialize.h | 15 ------------ test/fordisclosurecheck.sh | 15 ++++++------ test/forrankordercheck.sh | 37 +++++++++++++++++----------- 4 files changed, 51 insertions(+), 66 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 894220dbf..560a5c820 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2420,43 +2420,33 @@ inline std::optional forTaskText( const std::string& root, const st mcpSigsCut.continuationRequest = nullptr; // it pointed at this pass's mcpSigsNext, which ends here return McpForAssembly{ std::move( out ), mcpSigsCut }; }; - // C3 — pay for the handle in rows ONLY where that makes the answer fit. This surface overshoots budget_tokens - // structurally (the header bytes its ledger exempts), so a paid answer is nearly always over anyway; the UNPAID pass runs - // first: the rows the cut alone leaves, the handle riding unpaid. Only when the answer the cut alone left — that document - // minus the handle's bytes, priced by the same rule priceForTaskRoot applies — fits budget_tokens can paying matter, and only - // then does the paid pass run; it is kept when it fits, else the unpaid one ships (over_ceiling="1" either way). - const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; - McpForAssembly chosen = assembleMcpFor( /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); - if( !chosen.doc ) - { - return std::nullopt; - } + // C3 — pay for the handle in rows ONLY where that makes the answer fit (the CLI twin's rule, runForLens). The PAID pass runs + // first, so the payable case is the answer it always was; when that answer still lands past budget_tokens — and this surface + // overshoots on its own (the header bytes its ledger exempts), so a capped answer under a budget nearly always does — the + // rows it dropped bought nothing, and the UNPAID pass is served instead: the rows the cut alone leaves, the handle riding + // unpaid, over_ceiling="1" (priceForTaskRoot). Both passes share the one ranking above; only the assembly re-runs. const auto estOf = []( std::size_t docBytes ) { std::size_t est = 0; - (void)rw::pricedRootAttr( docBytes, rw::kBytesPerTokenDefault, 0, &est ); + (void)rw::pricedRootAttr( docBytes, rw::kBytesPerTokenDefault, 0, &est ); // priceForTaskRoot's own reading return est; }; - if( budgetTokens > 0 && chosen.cut.hasContinuation ) + const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; + McpForAssembly chosen = assembleMcpFor( /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + if( !chosen.doc ) + { + return std::nullopt; + } + if( budgetTokens > 0 && chosen.cut.hasContinuation && estOf( chosen.doc->size() ) > budgetTokens ) { - const std::size_t handleBytes = rw::sigsCutContinuationBytes( chosen.cut ); - ASSUME( handleBytes <= chosen.doc->size() ); - if( estOf( chosen.doc->size() - handleBytes ) <= budgetTokens ) + if( tallyAtEntry ) { - const std::optional tallyUnpaid = redact != nullptr ? std::optional( *redact ) : std::nullopt; - if( tallyAtEntry ) - { - *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served - } - McpForAssembly paid = assembleMcpFor( /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); - if( paid.doc && estOf( paid.doc->size() ) <= budgetTokens ) - { - chosen = std::move( paid ); - } - else if( tallyUnpaid ) - { - *redact = *tallyUnpaid; - } + *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served + } + chosen = assembleMcpFor( /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + if( !chosen.doc ) + { + return std::nullopt; } } std::string out = std::move( *chosen.doc ); diff --git a/src/serialize.h b/src/serialize.h index 1d0d93366..60c7a62f4 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4708,21 +4708,6 @@ inline std::string sigsCutReportLegend( const SigsCutReport& cut ) } return notes; } -// The bytes the continuation adds to the tag + the header legend (attributes + clauses), measured as the difference against the -// same report with the continuation stripped — so a caller can price the answer the cut alone would have left (ruling C3). -inline std::string sigsOpenTag( const SigsCutReport& cut ); -inline std::size_t sigsCutContinuationBytes( const SigsCutReport& cut ) -{ - SigsCutReport bare = cut; - bare.next = std::string(); - bare.nextBudgetTokens = 0; - bare.hasContinuation = false; - bare.terseContinuation = false; - const std::size_t with = sigsOpenTag( cut ).size() + sigsCutReportLegend( cut ).size(); - const std::size_t without = sigsOpenTag( bare ).size() + sigsCutReportLegend( bare ).size(); - ENSURES( with >= without, "a continuation only adds bytes" ); - return with - without; -} // the same clauses from the four numbers a caller without a continuation holds (--from-trace, --pack-task) inline std::string sigsCutLegendNotes( bool isCapped, std::size_t shown, std::size_t total, std::size_t docsDropped ) { diff --git a/test/fordisclosurecheck.sh b/test/fordisclosurecheck.sh index 14845d5b0..537b01889 100755 --- a/test/fordisclosurecheck.sh +++ b/test/fordisclosurecheck.sh @@ -126,12 +126,13 @@ grep -q ']*> block is intact" \ || no "#2 ceiling-exhausted: the block vanished" # #2c (rv-knob-honesty-068 C2; CHECKLIST 17) the intact-block grep above CAN FAIL on exactly this document. It is a ceiling- -# exhausted one: its is capped, its next= rides unpaid and the root carries over_ceiling="1" with the unpaid clause in -# the header legend — BEFORE the real tag. A clause that spelled the tag literally (dd6e4c8e's floor clause did) satisfied -# `grep -q ' is capped, its next= rides unpaid and the root carries over_ceiling="1" with the clause defining +# that in the header legend — BEFORE the real tag. A clause that spelled the tag literally (dd6e4c8e's floor clause did) +# satisfied `grep -q ']* over_ceiling="1"' "$T" && grep -oE ']*>' "$T" | tail -1 | grep -q ' capped="1".* next="'; then python3 -c 'import sys; s=open(sys.argv[1],"rb").read(); a=s.rfind(b"",a); sys.stdout.buffer.write(s[:a]+s[b+7:] if a>=0 and b>=0 else s)' "$T" >"$TMP/n400_nosigs" grep -q ' block removed — a legend clause spells the tag" \ @@ -140,7 +141,7 @@ if grep -qF 'next= rides unpaid' "$T"; then && no "#2c sigsblock() starts inside the header legend comment, not at the real tag" \ || ok "#2c sigsblock() starts at the real tag" else - no "#2c premise: the --token-budget=400 document no longer carries the unpaid clause — re-anchor this arm on a document that does" + no "#2c premise: the --token-budget=400 document is no longer a floor document (root over_ceiling=\"1\", a capped with next=) — re-anchor this arm on one that is" fi "$BIN" src --for="$NAMETASK" --token-budget=400 --signatures-only --no-cache >"$TMP/n400so" 2>/dev/null sigsblock "$T" >"$TMP/s_a"; sigsblock "$TMP/n400so" >"$TMP/s_s" diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index ebbacc0d4..d6f6d3488 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -547,29 +547,38 @@ fi # ── (10) C3: NO ROW IS DROPPED TO PAY FOR A HANDLE THAT CANNOT MAKE THE ANSWER FIT (orchestrator ruling 2026-10-07) ────────── # The MCP `for` answer overshoots budget_tokens on its own (its sig ledger exempts header bytes), so under a budget that caps -# it is nearly always past its ceiling paid or not; paying next_budget_tokens= from the rows (dd6e4c8e: ~245 B, 1-2 -# rows) bought nothing. Over its ceiling either way → the rows the cut alone leaves, the handle unpaid, over_ceiling="1". -# Where paying IS what makes it fit, it pays (unchanged). Fixture: (9)'s 40 gadget functions. -# over-anyway @2000: 255dc199 serves 14 of 40 (est 2298, over); dd6e4c8e paid 2 rows (12, est 2234, still over) — RED there. -# Rows >= 14 (re-pin from a pre-continuation binary if row bytes change; smaller rows only raise it), the handle rides, -# over_ceiling="1". Same at 3500 (255dc199: 31 of 40). -# payable @4000: 255dc199 serves 37 of 40 inside the budget (est 3971); the handle's bytes are paid from rows so the answer -# STILL fits — est_tokens <= 4000, no over_ceiling=, next_budget_tokens= present, fewer rows than the 37 (paid). +# it is often past its ceiling paid or not; paying next_budget_tokens= from the rows (dd6e4c8e: ~245 B, 1-2 rows) +# bought nothing there. Over its ceiling either way → the rows the cut alone leaves, the handle unpaid, over_ceiling="1". +# Where paying IS what makes it fit, it pays (unchanged). Fixture: (9)'s 40 gadget functions, called by a RELATIVE path from +# $TMP so no checkout or temp-dir path rides in the header bytes the rows are budgeted against (root="fxcut" everywhere). +# over-anyway @2000 / @3000: 255dc199 serves 14 / 25 of 40 (est 2270 / 3125, over); dd6e4c8e paid 2 rows (12 / 23) and was +# still over (est 2206 / 3071) — RED there. Rows >= 255dc199's (re-pin from a pre-continuation binary if row bytes or the +# header change; smaller rows only raise it), the handle rides, over_ceiling="1". (Budgets with a margin on both sides of +# the ceiling: near one, a few bytes of legend decide whether paying fits, and the arm would pin that, not the rule.) +# payable @3800: 255dc199 serves 35 rows at est 3824 — OVER; paying the handle from rows (33, est 3773) is what makes it fit: +# the answer carries next_budget_tokens= and NO over_ceiling= (a binary that never pays serves 35 rows at est 3934, over). +mcprel(){ ( cd "$TMP" && printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"fxcut","task":"gadget assembler","budget_tokens":%s}}}\n' "$1" \ + | "$BIN" --mcp 2>/dev/null ) | python3 -c 'import json,sys +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line) + if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm; } c3row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["shown"], d["capped"], d["next_budget_tokens"] is not None, d["over"], d["est_tokens"])' 2>/dev/null; } -for spec in 2000:14 3500:31; do +for spec in 2000:14 3000:25; do bt="${spec%%:*}"; want="${spec##*:}" - MC3="$( mcpfor ",\"budget_tokens\":$bt" )"; set -- $( c3row "$MC3" ) + set -- $( c3row "$( mcprel "$bt" )" ) if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = True ]; then ok "(10) MCP budget_tokens=$bt, over its ceiling either way: $1 rows (>= the cut's own $want, none dropped for the handle), next_budget_tokens= unpaid, over_ceiling=\"1\" (est $5)" else no "(10) MCP budget_tokens=$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows (the cut's own), the handle, over_ceiling=\"1\"" fi done -MP="$( mcpfor ',"budget_tokens":4000' )"; set -- $( c3row "$MP" ) -if [ "$#" -eq 5 ] && [ "$1" -lt 37 ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le 4000 ]; then - ok "(10) MCP budget_tokens=4000, payable: the handle is paid from rows ($1 < 37) and the answer fits (est $5 <= 4000, no over_ceiling=)" +set -- $( c3row "$( mcprel 3800 )" ) +if [ "$#" -eq 5 ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le 3800 ]; then + ok "(10) MCP budget_tokens=3800, payable: the handle is paid from rows ($1 shown) and that is what makes the answer fit (est $5 <= 3800, no over_ceiling=)" else - no "(10) MCP budget_tokens=4000: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" + no "(10) MCP budget_tokens=3800: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" fi [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" From 481149caa6b991d84c2022914a3b73c154d4f34d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:09:39 -0400 Subject: [PATCH 160/229] style: graphlegend.h's zero-reading constants to .clang-format The CI style job (scripts/formatcheck.sh, a gated file) failed on the column alignment of kForZeroAbsentFullNote / kForZeroAbsentMcpNote, added by this lane in 874eb65d. clang-format -i output, whitespace only. Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index aa49c6b23..f832adc2f 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -356,8 +356,8 @@ inline constexpr std::string_view kForRouteCodeLegend = // is EXEMPT from the sig ledger (forZeroNoteBytes): spelling the reading must never cost the answer a row, so the rows // kept are exactly the rows kept before the elision. Each is checked verbatim inside the clause that carries it. inline constexpr std::string_view kForZeroAbsentCompactNote = " (absent cx/ccx/in = 0)"; -inline constexpr std::string_view kForZeroAbsentFullNote = ", each absent when 0;"; -inline constexpr std::string_view kForZeroAbsentMcpNote = "; an absent cx/ccx/in is 0"; +inline constexpr std::string_view kForZeroAbsentFullNote = ", each absent when 0;"; +inline constexpr std::string_view kForZeroAbsentMcpNote = "; an absent cx/ccx/in is 0"; // The bytes of those readings a header carries — what its sig ledger leaves uncharged. inline std::size_t forZeroNoteBytes( std::string_view header ) noexcept { From 95d4f55cdfdd45c97d68f6fa821920fc48de9ed6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:24:23 -0400 Subject: [PATCH 161/229] refactor(mcp for): the assembly pass is a free function (assembleMcpFor), not a lambda inside forTaskText quality-delta gated forTaskText's complexity 109 -> 139 (bar 15) once the assembly ran inside a lambda there. The pass is a step of its own: assembleMcpFor takes forTaskText's locals by reference (McpForAssemblyInputs) and its own copies of what it edits; the body is moved unchanged. Output byte-identical to 90557fb1 (MCP for on the fxcut fixture at 0, 1000, 2000, 3000, 3800, 4400 budget_tokens and two real-corpus tasks at 0 and 2000). Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 515 +++++++++++++++++++++++++++---------------------- 1 file changed, 281 insertions(+), 234 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 560a5c820..9907a64bb 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1802,6 +1802,281 @@ inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) rw::spliceRootAttrs( doc, rootAttrs ); } +// knob-honesty-068 (orchestrator ruling C3, 2026-10-07): MCP `for`'s ASSEMBLY — everything from the sigs render to the +// finished, UNPRICED document (est_tokens= / over_ceiling= are spliced once, on the chosen one, by forTaskText) — as one pass +// forTaskText runs once or twice over a single ranking (paid, then unpaid when the paid answer still lands over). The pass +// takes its own copies of what it edits (the header, the lego/compose strings, the lego scope and its pre-cap count), so a +// second pass starts from the same state as the first. A free function, not a lambda: forTaskText is already the largest +// function in this file, and the assembly is a step of its own. +struct McpForAssembly +{ + std::optional doc; // nullopt: the answer buffer lost bytes (internal error) + rw::SigsCutReport cut; +}; +struct McpForAssemblyInputs // forTaskText's locals the assembly reads, by reference; all outlive the call +{ + const IngestResult& ing; + const std::vector& lensRank; + int forTopN; + const std::vector& fanIn; + const std::vector& impure; + RedactCounts* redact; + std::size_t sigsBudget; + std::size_t fixedBytes; + std::size_t budgetTokens; + const notes::NoteIndex& noteIndex; + const notes::NoteIndex* notesPtr; + std::string_view flRootArg; + const std::string& task; + const std::string& mcpTopRowNext; + const std::vector& mcpForHdrRows; + const std::string& routeStr; + std::size_t composePreCapCount; + const std::string& sections; +}; +inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, bool payFromRows, std::string headerStr, std::string legoStr, + std::string composeStr, std::vector> legoScoped, std::size_t legoPreCapCount ) +{ + const IngestResult& ing = in.ing; + const std::vector& lensRank = in.lensRank; + const int forTopN = in.forTopN; + const std::vector& fanIn = in.fanIn; + const std::vector& impure = in.impure; + RedactCounts* const redact = in.redact; + const std::size_t sigsBudget = in.sigsBudget; + const std::size_t fixedBytes = in.fixedBytes; + const std::size_t budgetTokens = in.budgetTokens; + const notes::NoteIndex& noteIndex = in.noteIndex; + const notes::NoteIndex* const notesPtr = in.notesPtr; + const std::string_view flRootArg = in.flRootArg; + const std::string& task = in.task; + const std::string& mcpTopRowNext = in.mcpTopRowNext; + const std::vector& mcpForHdrRows = in.mcpForHdrRows; + const std::string& routeStr = in.routeStr; + const std::size_t composePreCapCount = in.composePreCapCount; + const std::string& sections = in.sections; + const auto renderToString = [ ]( auto&& emitFn ) -> std::string { return captureXml( emitFn ); }; // forTaskText's own seam + rw::MemoryStream stream; + std::FILE* const mem = stream.open(); + if( !mem ) + { + return McpForAssembly{}; // the answer buffer could not be opened: an internal error, never "not found" + } + const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, + /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false, + /*ledgerGapBytes=*/0u, payFromRows }; + // A2 (survey card, 2026-09-03): sigs render into their OWN buffer (rather than streaming straight into + // `mem` the way this call used to) so droppedPositive is known BEFORE headerStr is written — headerStr, + // once flushed to `mem`, cannot be edited retroactively (the same reason the CLI twin's degrade path + // never gets the attribute). Byte-for-byte the same content this call always produced. + std::size_t mcpDroppedPositive = 0; + bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below + rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does + mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation + std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface + std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) + { + packSignatures( m2, ing, lensRank, forTopN, 0 /* no byte budget in MCP (0 = unlimited) */, true, &fanIn, &impure, redact, + nullptr, nullptr, nullptr, nullptr, // Q3 lens vectors — the MCP verb has no git/clone pass (as before) + /*rankAdaptivePayload=*/true, // B0.3: same rank-adaptive payload rule as the CLI --for lens + sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) + notesPtr, // L3: field-notes surfacing (inert when null) + flRootArg, // R-E: root-relative p=, same argument the CLI twin passes + /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad + &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) + &mcpShownIds, // lane 2: see verbs_for.h shownSigIds + &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below + mcpTopRowNext, // L-W: the widening page on a thin answer, else the body + &mcpSigsCut ); // cut-fix lane A: docs_dropped= / shrunk readings; + // knob-honesty-068: carries mcpSigsNext IN + } ); + // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) + // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare + // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between + // the two dialects, the way every other --for header fragment in this function already is. + if( mcpDroppedPositive > 0 ) + { + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + char nb[ 40 ]; + rw::formatTo( nb, sizeof( nb ), " dropped_positive=\"{}\"", mcpDroppedPositive ); + headerStr.insert( closeAt, nb ); // else: unexpected shape, header left as-is + } + } + // budget_bytes= — the CLI twin's disclosure (verbs_for.h, where the full argument lives), on this + // surface for the §P8 reason every other fragment in this function is: one element name, one attribute + // order, both surfaces. This dialect is budgeted by DEFAULT exactly as the CLI is (forBudgetBytes above + // is kForPayloadBudgetBytes when the caller passed no budget_tokens), and it disclosed the cut + // () without naming what did the cutting. Same two conditions as the CLI: the ladder + // actually fired, and the caller named no ceiling of their own — so exactly one ceiling rides a trimmed + // bundle. Same clause text, so the two dialects say the same sentence. Spliced AFTER the sigs render, + // like dropped_positive= above, so sigsBudget and therefore the served row set are untouched; est_tokens + // is spliced onto the finished document further down and so prices these bytes. + if( mcpSigsCapped && budgetTokens == 0 ) + { + const std::size_t rootCloseAt = headerStr.find( ">" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, rw::kForBudgetBytesNote ); + } + } + // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same + // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). + if( const std::string cutNotes = rw::sigsCutReportLegend( mcpSigsCut ); + !cutNotes.empty() ) + { + const std::size_t closeAt = headerStr.rfind( " -->" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, cutNotes ); + } + } + // L3 follow-up (CodeRabbit 4053600616): same splice shape as dropped_positive=/budget_bytes= above — absent + // entirely on a clean read. `noteIndex.degraded` is read here (not through notesPtr, which is nulled on an + // EMPTY index even when the read itself was not clean — see notesPtr's own comment above). + if( noteIndex.degraded ) + { + const std::size_t rootCloseAt = headerStr.find( ">" ); + if( closeAt != std::string::npos ) + { + headerStr.insert( closeAt, " [" + std::string( rw::notes::kNotesDegradedReading ) + "]" ); + } + } + std::fwrite( headerStr.data(), 1, headerStr.size(), mem ); + // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule + // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this + // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver + // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. + if( !mcpForHdrRows.empty() ) + { + const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); + const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string + { + return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); + }; + std::vector mcpHdrEsc; + std::string mcpHdrXml; + for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) + { + // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver + // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. + ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); + mcpHdrXml += ""; + } + std::fwrite( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); + } + // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs + // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr + // directly now rather than re-slicing it back out of the flushed memstream buffer. + if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr ) ) + { + legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount ); } ); // R-R: the re-render dropped the root its first render (above) passed + } + // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution + // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= + // is already CLI-flag-shaped everywhere else, e.g. the r=1 row's --expand=FILE:NAME, so the two + // surfaces do not need two spellings), and the SAME shared size gate (rw::priceSectionStub, + // serialize.h). This surface has no buffered/degrade-path split (renderToString always renders whole), + // so hasRenderedBytes is always true here and the size gate always has a true length to price. + // + // R2-L2p (independent review, 2026-09-19): mcpLegoWillStub/mcpComposeWillStub decide the SAME collapse + // the CLI twin discloses via kForSectionStubLegend (verbs_for.h finishForLensHeaderPriced), but this + // block used to just swap legoStr/composeStr for their stubs and never carried that clause anywhere — + // an MCP caller got a stubbed section with no legend explaining the cut. mcpSectionsWillStub survives + // this block's scope so the splice below (after `out` is assembled, mirroring priceForTaskRoot's own + // late-insertion technique for kOverCeilingLegend) knows whether to add it. + bool mcpSectionsWillStub = false; + { + const bool mcpWantLego = sectionsWant( sections, "lego" ); + const bool mcpWantCompose = sectionsWant( sections, "compose" ); + const bool mcpLegoCandidate = !legoStr.empty() && !mcpWantLego; + const bool mcpComposeCandidate = !composeStr.empty() && !mcpWantCompose; + if( mcpLegoCandidate || mcpComposeCandidate ) + { + std::string sectionsNextMcp = nextFlag( "--for=", task ); + sectionsNextMcp += ' '; + sectionsNextMcp += nextFlag( "--sections=", "lego,compose" ); + const SectionStubPricing legoPricing = mcpLegoCandidate + ? priceSectionStub( "lego", legoPreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, legoStr.size() ) + : SectionStubPricing{}; + const SectionStubPricing composePricing = mcpComposeCandidate + ? priceSectionStub( "compose", composePreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, composeStr.size() ) + : SectionStubPricing{}; + const bool mcpLegoWillStub = mcpLegoCandidate && legoPricing.collapse; + const bool mcpComposeWillStub = mcpComposeCandidate && composePricing.collapse; + // postcondition of the rule itself, checked BEFORE the swap below discards the original size: + // whichever section actually collapses is, by construction, smaller than what it replaced. + ENSURES( !mcpLegoWillStub || legoPricing.stubXml.size() < legoStr.size(), + "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); + ENSURES( !mcpComposeWillStub || composePricing.stubXml.size() < composeStr.size(), + "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); + if( mcpLegoWillStub ) + { + legoStr = legoPricing.stubXml; + } + if( mcpComposeWillStub ) + { + composeStr = composePricing.stubXml; + } + // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` + // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. + mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; + } + } + std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); + std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); + std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); + std::fwrite( routeStr.data(), 1, routeStr.size(), mem ); // B6.3 + // DEEP-TAIL d2, MCP twin: the same shared walk + renderer the CLI --for uses (serialize.h), from the + // same resolved surface — the fixed default budget regime, so the tail rides on top (default row cap) + // and the ranked head above stays byte-identical to a tail-less bundle. + { + std::vector tailEsc; + const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), + kForFileTailShownCap, tailEsc ); + std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); + } + rw::emitRaw( mem, "" ); + std::optional answer = mcpAnswerText( stream ); + if( !answer ) + { + return McpForAssembly{}; // the buffer lost bytes: the same internal error as the failed open above + } + std::string out = std::move( *answer ); + // R2-L2p: the legend clause disclosing a stubbed lego/compose section (kForSectionStubLegend) — the + // whole contract lives in spliceForSectionStubLegend above; `stubbed` false (the overwhelming common + // path) returns immediately and costs this function nothing but the call. Done BEFORE priceForTaskRoot + // so the clause's bytes are part of the est_tokens price exactly as the CLI's own version is. + spliceForSectionStubLegend( out, mcpSectionsWillStub ); + // F5 (terminality round A 2026-09-05): PRICE the bundle instead of declaring it unpriced. The document is + // complete here, so this is the same measurement the CLI twin makes over its own (deliberately different) + // bytes: pricedRootAttr's ≤4-pass fixpoint at kBytesPerTokenDefault, spliced onto the root by the + // shared splicer — no second estimator, and no number that could disagree with the CLI's for the same + // reason the CLI's could disagree with itself. Charged AFTER the sigs budget on purpose: a disclosure's + // contract is disclosure only, and charging its ~18 bytes against the ranked head would drop a row to pay + // for the attribute that describes the head — the same exemption mcpConfidenceExemptBytes already makes. + // r2-LO: a completeness attribute this bundle carries and its own legend never spells (at=, ccx=, next=) gets its compact + // reading — BEFORE pricing, so est_tokens= prices the bytes served, and after the sigs budget, so the rows are unchanged. + closeRosterGaps( out ); + mcpSigsCut.continuationRequest = nullptr; // it pointed at this pass's mcpSigsNext, which ends here + return McpForAssembly{ std::move( out ), mcpSigsCut }; +} + inline std::optional forTaskText( const std::string& root, const std::string& task, RedactCounts* redact = nullptr, std::size_t budgetTokens = 0, bool noRoute = false, McpPageArgs page = {}, // L-W: limit/offset select the FILE PAGE (forpage.h), the CLI --for --limit twin @@ -2188,238 +2463,10 @@ inline std::optional forTaskText( const std::string& root, const st const notes::NoteIndex noteIndex = notes::loadNoteIndex( root ); const notes::NoteIndex* const notesPtr = noteIndex.empty() ? nullptr : ¬eIndex; - // knob-honesty-068 (orchestrator ruling C3, 2026-10-07): everything from the sigs render to the finished document is ONE - // ASSEMBLY PASS, run once or twice — the ranking above runs once. The pass takes its own copies of what it edits (the header, - // the lego/compose strings, the lego scope and its pre-cap count), so a second pass starts from the same state as the first. - // It returns the document UNPRICED (est_tokens= / over_ceiling= are spliced once, on the chosen one, below). - struct McpForAssembly - { - std::optional doc; // nullopt: the answer buffer lost bytes (internal error) - rw::SigsCutReport cut; - }; - const auto assembleMcpFor = [ & ]( bool payFromRows, std::string headerStr, std::string legoStr, std::string composeStr, - std::vector> legoScoped, std::size_t legoPreCapCount ) -> McpForAssembly - { - rw::MemoryStream stream; - std::FILE* const mem = stream.open(); - if( !mem ) - { - return McpForAssembly{}; // the answer buffer could not be opened: an internal error, never "not found" - } - const rw::SigsCutContinuation mcpSigsNext{ task, 0, std::string_view(), fixedBytes, - /*charged=*/budgetTokens > 0, /*json=*/false, /*pasteHandle=*/false, - /*ledgerGapBytes=*/0u, payFromRows }; - // A2 (survey card, 2026-09-03): sigs render into their OWN buffer (rather than streaming straight into - // `mem` the way this call used to) so droppedPositive is known BEFORE headerStr is written — headerStr, - // once flushed to `mem`, cannot be edited retroactively (the same reason the CLI twin's degrade path - // never gets the attribute). Byte-for-byte the same content this call always produced. - std::size_t mcpDroppedPositive = 0; - bool mcpSigsCapped = false; // did the H1 ladder trim ? — decides the budget_bytes= disclosure below - rw::SigsCutReport mcpSigsCut; // cut-fix lane A: the tag's cut readings, spliced below as the CLI twin does - mcpSigsCut.continuationRequest = &mcpSigsNext; // knob-honesty-068: IN — the capped block's machine continuation - std::vector mcpShownIds; // lane 2: the sigs rows actually emitted — the tail excludes these files, not the whole surface - std::string sigsStr = renderToString( [ & ]( std::FILE* m2 ) - { - packSignatures( m2, ing, lensRank, forTopN, 0 /* no byte budget in MCP (0 = unlimited) */, true, &fanIn, &impure, redact, - nullptr, nullptr, nullptr, nullptr, // Q3 lens vectors — the MCP verb has no git/clone pass (as before) - /*rankAdaptivePayload=*/true, // B0.3: same rank-adaptive payload rule as the CLI --for lens - sigsBudget, // H1: global payload budget (trim ladder; payload="capped" marker) - notesPtr, // L3: field-notes surfacing (inert when null) - flRootArg, // R-E: root-relative p=, same argument the CLI twin passes - /*hasRelevanceFloor=*/true, // LB-A: shrink past the zero-score tail, never pad - &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) - &mcpShownIds, // lane 2: see verbs_for.h shownSigIds - &mcpSigsCapped, // the ladder's own verdict — see the budget_bytes= splice below - mcpTopRowNext, // L-W: the widening page on a thin answer, else the body - &mcpSigsCut ); // cut-fix lane A: docs_dropped= / shrunk readings; - // knob-honesty-068: carries mcpSigsNext IN - } ); - // A2: same insert-before-"-->" splice as the CLI twin (verbs_for.h) — absent entirely on the (overwhelming) - // no-drop path, so headerStr's bytes are unchanged there (byte-identical to the pre-A2 output). Bare - // spelling (no bracket note), same economy and same reasoning as the CLI twin — byte-consistent between - // the two dialects, the way every other --for header fragment in this function already is. - if( mcpDroppedPositive > 0 ) - { - const std::size_t closeAt = headerStr.rfind( " -->" ); - if( closeAt != std::string::npos ) - { - char nb[ 40 ]; - rw::formatTo( nb, sizeof( nb ), " dropped_positive=\"{}\"", mcpDroppedPositive ); - headerStr.insert( closeAt, nb ); // else: unexpected shape, header left as-is - } - } - // budget_bytes= — the CLI twin's disclosure (verbs_for.h, where the full argument lives), on this - // surface for the §P8 reason every other fragment in this function is: one element name, one attribute - // order, both surfaces. This dialect is budgeted by DEFAULT exactly as the CLI is (forBudgetBytes above - // is kForPayloadBudgetBytes when the caller passed no budget_tokens), and it disclosed the cut - // () without naming what did the cutting. Same two conditions as the CLI: the ladder - // actually fired, and the caller named no ceiling of their own — so exactly one ceiling rides a trimmed - // bundle. Same clause text, so the two dialects say the same sentence. Spliced AFTER the sigs render, - // like dropped_positive= above, so sigsBudget and therefore the served row set are untouched; est_tokens - // is spliced onto the finished document further down and so prices these bytes. - if( mcpSigsCapped && budgetTokens == 0 ) - { - const std::size_t rootCloseAt = headerStr.find( ">" ); - if( closeAt != std::string::npos ) - { - headerStr.insert( closeAt, rw::kForBudgetBytesNote ); - } - } - // cut-fix lane A: the tag's cut readings (docs_dropped=, shrunk-not-dropped) — the CLI twin's clauses, same - // text, same splice point, present only when the tag carries the case (serialize.h sigsCutLegendNotes). - if( const std::string cutNotes = rw::sigsCutReportLegend( mcpSigsCut ); - !cutNotes.empty() ) - { - const std::size_t closeAt = headerStr.rfind( " -->" ); - if( closeAt != std::string::npos ) - { - headerStr.insert( closeAt, cutNotes ); - } - } - // L3 follow-up (CodeRabbit 4053600616): same splice shape as dropped_positive=/budget_bytes= above — absent - // entirely on a clean read. `noteIndex.degraded` is read here (not through notesPtr, which is nulled on an - // EMPTY index even when the read itself was not clean — see notesPtr's own comment above). - if( noteIndex.degraded ) - { - const std::size_t rootCloseAt = headerStr.find( ">" ); - if( closeAt != std::string::npos ) - { - headerStr.insert( closeAt, " [" + std::string( rw::notes::kNotesDegradedReading ) + "]" ); - } - } - std::fwrite( headerStr.data(), 1, headerStr.size(), mem ); - // R2-AF (round 2, S4): first rows inside the root, right after the legend — the CLI twin's exact rule - // (verbs_for.h renderForHdrRowsXml), duplicated here rather than shared because main.cpp includes this - // file (mcp.h, line 48) before verbs_for.h (line 525) — the render is ~10 lines over the SAME resolver - // (rw::forNamedHeaderRows, mention.h), which is the part a divergence would actually cost. - if( !mcpForHdrRows.empty() ) - { - const std::string mcpHdrRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); - const auto mcpHdrRel = [ & ]( std::uint32_t f ) -> std::string - { - return flRootArg.empty() ? std::string( ing.files[f] ) : std::string( rw::sarif::rootRelativeUri( ing.files[f], mcpHdrRootPrefix ) ); - }; - std::vector mcpHdrEsc; - std::string mcpHdrXml; - for( const rw::ForNamedHeaderRow& row : mcpForHdrRows ) - { - // same local invariant as the CLI twin's renderForHdrRowsXml (verbs_for.h): the resolver - // (rw::forNamedHeaderRows, mention.h) is the only producer of these ids. - ASSUME( row.partnerFile < ing.files.size() && row.namedFile < ing.files.size() ); - mcpHdrXml += ""; - } - std::fwrite( mcpHdrXml.data(), 1, mcpHdrXml.size(), mem ); - } - // §P3 × §P4 (parity with the CLI --for): narrow the lego block to the files the budget-trimmed sigs - // actually kept and re-render (a byte-subset of what the budget already charged for) — reads sigsStr - // directly now rather than re-slicing it back out of the flushed memstream buffer. - if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsStr ) ) - { - legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount ); } ); // R-R: the re-render dropped the root its first render (above) passed - } - // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution - // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= - // is already CLI-flag-shaped everywhere else, e.g. the r=1 row's --expand=FILE:NAME, so the two - // surfaces do not need two spellings), and the SAME shared size gate (rw::priceSectionStub, - // serialize.h). This surface has no buffered/degrade-path split (renderToString always renders whole), - // so hasRenderedBytes is always true here and the size gate always has a true length to price. - // - // R2-L2p (independent review, 2026-09-19): mcpLegoWillStub/mcpComposeWillStub decide the SAME collapse - // the CLI twin discloses via kForSectionStubLegend (verbs_for.h finishForLensHeaderPriced), but this - // block used to just swap legoStr/composeStr for their stubs and never carried that clause anywhere — - // an MCP caller got a stubbed section with no legend explaining the cut. mcpSectionsWillStub survives - // this block's scope so the splice below (after `out` is assembled, mirroring priceForTaskRoot's own - // late-insertion technique for kOverCeilingLegend) knows whether to add it. - bool mcpSectionsWillStub = false; - { - const bool mcpWantLego = sectionsWant( sections, "lego" ); - const bool mcpWantCompose = sectionsWant( sections, "compose" ); - const bool mcpLegoCandidate = !legoStr.empty() && !mcpWantLego; - const bool mcpComposeCandidate = !composeStr.empty() && !mcpWantCompose; - if( mcpLegoCandidate || mcpComposeCandidate ) - { - std::string sectionsNextMcp = nextFlag( "--for=", task ); - sectionsNextMcp += ' '; - sectionsNextMcp += nextFlag( "--sections=", "lego,compose" ); - const SectionStubPricing legoPricing = mcpLegoCandidate - ? priceSectionStub( "lego", legoPreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, legoStr.size() ) - : SectionStubPricing{}; - const SectionStubPricing composePricing = mcpComposeCandidate - ? priceSectionStub( "compose", composePreCapCount, sectionsNextMcp, /*hasRenderedBytes=*/true, composeStr.size() ) - : SectionStubPricing{}; - const bool mcpLegoWillStub = mcpLegoCandidate && legoPricing.collapse; - const bool mcpComposeWillStub = mcpComposeCandidate && composePricing.collapse; - // postcondition of the rule itself, checked BEFORE the swap below discards the original size: - // whichever section actually collapses is, by construction, smaller than what it replaced. - ENSURES( !mcpLegoWillStub || legoPricing.stubXml.size() < legoStr.size(), - "R2-L2' MCP twin: a lego stub collapsed without being smaller than the section it replaced" ); - ENSURES( !mcpComposeWillStub || composePricing.stubXml.size() < composeStr.size(), - "R2-L2' MCP twin: a compose stub collapsed without being smaller than the section it replaced" ); - if( mcpLegoWillStub ) - { - legoStr = legoPricing.stubXml; - } - if( mcpComposeWillStub ) - { - composeStr = composePricing.stubXml; - } - // R2-L2p: same truth table as the CLI's `if( legoWillStub || composeWillStub ) sectionsStubNote = …` - // (verbs_for.h ~2858): the legend rides iff at least one section actually collapsed. - mcpSectionsWillStub = mcpLegoWillStub || mcpComposeWillStub; - } - } - std::fwrite( sigsStr.data(), 1, sigsStr.size(), mem ); - std::fwrite( legoStr.data(), 1, legoStr.size(), mem ); - std::fwrite( composeStr.data(), 1, composeStr.size(), mem ); - std::fwrite( routeStr.data(), 1, routeStr.size(), mem ); // B6.3 - // DEEP-TAIL d2, MCP twin: the same shared walk + renderer the CLI --for uses (serialize.h), from the - // same resolved surface — the fixed default budget regime, so the tail rides on top (default row cap) - // and the ranked head above stays byte-identical to a tail-less bundle. - { - std::vector tailEsc; - const std::string tailStr = renderFileTailXml( computeFileTail( ing, lensRank, mcpShownIds, flRootArg ), - kForFileTailShownCap, tailEsc ); - std::fwrite( tailStr.data(), 1, tailStr.size(), mem ); - } - rw::emitRaw( mem, "" ); - std::optional answer = mcpAnswerText( stream ); - if( !answer ) - { - return McpForAssembly{}; // the buffer lost bytes: the same internal error as the failed open above - } - std::string out = std::move( *answer ); - // R2-L2p: the legend clause disclosing a stubbed lego/compose section (kForSectionStubLegend) — the - // whole contract lives in spliceForSectionStubLegend above; `stubbed` false (the overwhelming common - // path) returns immediately and costs this function nothing but the call. Done BEFORE priceForTaskRoot - // so the clause's bytes are part of the est_tokens price exactly as the CLI's own version is. - spliceForSectionStubLegend( out, mcpSectionsWillStub ); - // F5 (terminality round A 2026-09-05): PRICE the bundle instead of declaring it unpriced. The document is - // complete here, so this is the same measurement the CLI twin makes over its own (deliberately different) - // bytes: pricedRootAttr's ≤4-pass fixpoint at kBytesPerTokenDefault, spliced onto the root by the - // shared splicer — no second estimator, and no number that could disagree with the CLI's for the same - // reason the CLI's could disagree with itself. Charged AFTER the sigs budget on purpose: a disclosure's - // contract is disclosure only, and charging its ~18 bytes against the ranked head would drop a row to pay - // for the attribute that describes the head — the same exemption mcpConfidenceExemptBytes already makes. - // r2-LO: a completeness attribute this bundle carries and its own legend never spells (at=, ccx=, next=) gets its compact - // reading — BEFORE pricing, so est_tokens= prices the bytes served, and after the sigs budget, so the rows are unchanged. - closeRosterGaps( out ); - mcpSigsCut.continuationRequest = nullptr; // it pointed at this pass's mcpSigsNext, which ends here - return McpForAssembly{ std::move( out ), mcpSigsCut }; - }; + // knob-honesty-068 (orchestrator ruling C3): the assembly is one pass (assembleMcpFor above), run once or twice below. + const McpForAssemblyInputs assemblyIn{ ing, lensRank, forTopN, fanIn, impure, redact, sigsBudget, fixedBytes, budgetTokens, + noteIndex, notesPtr, flRootArg, task, mcpTopRowNext, mcpForHdrRows, routeStr, + composePreCapCount, sections }; // C3 — pay for the handle in rows ONLY where that makes the answer fit (the CLI twin's rule, runForLens). The PAID pass runs // first, so the payable case is the answer it always was; when that answer still lands past budget_tokens — and this surface // overshoots on its own (the header bytes its ledger exempts), so a capped answer under a budget nearly always does — the @@ -2432,7 +2479,7 @@ inline std::optional forTaskText( const std::string& root, const st return est; }; const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; - McpForAssembly chosen = assembleMcpFor( /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + McpForAssembly chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); if( !chosen.doc ) { return std::nullopt; @@ -2443,7 +2490,7 @@ inline std::optional forTaskText( const std::string& root, const st { *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served } - chosen = assembleMcpFor( /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); if( !chosen.doc ) { return std::nullopt; From 6381b84757f1269c492274bc416dedb2f0e81978 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:26:10 -0400 Subject: [PATCH 162/229] =?UTF-8?q?defect-shape:=20three=20sanitizer=20fin?= =?UTF-8?q?ds=20=E2=80=94=20an=20out-of-range=20token=20read,=20two=20unsi?= =?UTF-8?q?gned=20wraps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A case pattern alone on its line (`'( f ) + 1 ) ); + std::string id( defectshape::facetName( f ) ); + id.push_back( '\0' ); + for( int b = 0; b < 8; ++b ) + { + id.push_back( static_cast( ( anchor >> ( 8 * b ) ) & 0xFFu ) ); + } + return fnv1a64( id ); } // One current-side site with what the delta needs to print it: the anchoring definition (kNoNode for a file diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh index 58781c0bb..4e7e723de 100644 --- a/test/defectshapecheck.sh +++ b/test/defectshapecheck.sh @@ -676,6 +676,7 @@ std::string replaced( const std::string& s, std::size_t n ) { return s; } std::string packname( const std::string& text, bool pack ) { return text; } std::string wordloop( const std::vector& words ) { std::string out; return out; } std::string capminus( std::string s ) { return s; } +std::string guardanywhere( std::string s ) { return s; } std::u32string wide( std::u32string s, std::size_t n ) { return s; } std::string legacy( std::string s ) { if( s.size() > 40 ) { s.resize( 40 ); s += "..."; } return s; } int drive() { return 0; } @@ -778,6 +779,20 @@ std::u32string wide( std::u32string s, std::size_t n ) } return s; } +std::string guardanywhere( std::string s ) +{ + if( s.size() > 117 ) + { + std::size_t n = 117; + while( n > 0 && ( static_cast( s[n] ) & 0xC0 ) == 0x80 ) + { + --n; + } + s.resize( 117 ); + s += "..."; + } + return s; +} std::string capminus( std::string s ) { if( s.size() > 120 ) @@ -803,6 +818,7 @@ ds_has "$L" utf8-cut freshcut new-symbol ds_wasnow "$L" utf8-cut excerpt 0 1 ds_none "$L" guarded "the cut backs off continuation bytes (0xC0/0x80)" ds_none "$L" helper "a utf8 truncation helper does the cut" +ds_none "$L" guardanywhere "a back-off anywhere in the function exempts it, even one that does not cover the cut (a precision choice, stated in the legend)" ds_none "$L" noellipsis "a cut with no ellipsis is not judged display text (stated floor)" ds_none "$L" othervar "the ellipsis goes on a different string than the one cut" ds_none "$L" replaced "the ellipsis replaces the value, nothing is cut" @@ -984,7 +1000,7 @@ EOF )" VHDR="$VHDR " -for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn t_setelocal t_setepipe t_derived n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok n_posafter n_nestif n_case n_sete n_seteuo n_filepipe n_wrapper n_derivedguard n_bracegroup n_elif; do +for f in t_var t_pipe t_pipefail t_if t_cont t_neg t_names t_fn t_setelocal t_setepipe t_derived n_rc n_orno n_presence n_positive n_polarity n_literal n_comment n_legacy n_ifok n_posafter n_nestif n_case n_casealone n_sete n_seteuo n_filepipe n_wrapper n_derivedguard n_bracegroup n_elif; do printf '%s' "$VHDR" >"$WORK/vac/test/$f.sh" done for f in t_setepipe n_sete; do printf 'set -e\n' >>"$WORK/vac/test/$f.sh"; done @@ -1151,6 +1167,15 @@ case "$OUT" in *) no "no list root" ;; esac EOF +cat >>"$WORK/vac/test/n_casealone.sh" <<'EOF' +OUT="$( "$BIN" --list 2>/dev/null )" +case "$OUT" in + '>"$WORK/vac/test/$f.sh" <<'EOF' OUT="$( "$BIN" --list 2>/dev/null )" @@ -1215,6 +1240,7 @@ ds_none "$L" absent_checked "a NEW function whose capture failure is handl ds_none_p "$L" test/n_posafter.sh "a positive assertion on the same capture AFTER the absence fails on a crash" ds_none_p "$L" test/n_nestif.sh "the absence is nested in an if that requires content of the same capture" ds_none_p "$L" test/n_case.sh "the absence is nested in a case arm that requires content of the same capture" +ds_none_p "$L" test/n_casealone.sh "a case pattern alone on its line (the scanner must not read past it), the absence on the next line" ds_none_p "$L" test/n_sete.sh "set -e: a failed plain capture aborts the script" ds_none_p "$L" test/n_seteuo.sh "set -euo pipefail: a failed plain capture aborts the script" ds_none_p "$L" test/n_filepipe.sh "a text utility over a file is not a run under test (its input's provenance is not on the line)" From 6e579475a146d0764a945b16232ab1ded8c4b264 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:48:47 -0400 Subject: [PATCH 163/229] test: crossrefcheck sources the hermetic git helper gitenvhermeticcheck (D) requires every gate that initialises a git repository to source test/lib/clean-env.sh. The new L13 tie fixture spells `git -C DIR init`, which made crossrefcheck visible to that sweep; its other fixtures were already built through helper functions the sweep does not see. It now sources the helper once, before any fixture is built. Co-Authored-By: Claude Opus 5.5 --- test/crossrefcheck.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index 32c335e43..92844cf33 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -21,6 +21,9 @@ ROOT="$( cd "$( dirname "$0" )/.." && pwd )" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" # BOTH seams: positional and RIPWIRE_BIN [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +# Every fixture repo below is built in a hermetic git environment (gitenvhermeticcheck (D): a gate that initialises a +# repository sources the shared helper, so an inherited GIT_DIR/GIT_WORK_TREE or agent home cannot leak into it). +. "$ROOT/test/lib/clean-env.sh" fail=0 ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; } From 219aaaacd982c3074585fad3e7b08f654b7569ca Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 7 Oct 2026 23:48:48 -0400 Subject: [PATCH 164/229] lean answers: the surfaces and pins the full suite found behind the lane The first full CI run of this lane (it had deferred its suite) was red on eight gates, each a surface or pin the lane's own changes moved: - ripwire-mcp skill: its legend section still said answers carry their legend until the client reads ripwire://legend-dict. It now describes the session default and names --mcp-legend=inline (agentsurfacecheck, skillinstallcheck/releaseinstallcheck: the flag had no skill home). - the deck: 185 -> 187 long flags (deckclaimcheck), and 162 -> 167 recorded invocations, the count docs/COMMANDS.md now carries. - fixedbufsweep: sigRowTail's zero-elision branch adds cxAttr/ccxAttr rows, a second inAttr site and a third tail site; classified, enumeration re-derived (351/241/241/105 -> 355/245/245/107). - anchorcheck / routecheck goldens: re-pinned for the --for zero elision only. The base binary reproduces each previous golden byte for byte, and restoring the zeros and removing the one legend clause gives it back exactly (3557 -> 3533 B, 3776 -> 3752 B). - mcptranchecheck M4: its whereis limit= arm keeps its assertion on listing:"all"; a twin holds limit= on the default's listed rows (on a single-branch checkout runMcp's defs page has one row). Co-Authored-By: Claude Opus 5.5 --- present/deck5_ripwire_build.js | 6 +++--- skills/ripwire-mcp/SKILL.md | 20 +++++++++++--------- test/anchorcheck.sh | 5 +++++ test/anchorfix/golden_for.xml | 2 +- test/fixedbufsweep.sh | 12 +++++++++--- test/mcptranchecheck.sh | 19 ++++++++++++++++++- test/routecheck.sh | 5 +++++ test/routefix/golden_for.xml | 2 +- 8 files changed, 53 insertions(+), 18 deletions(-) diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index e2186599c..c70195e61 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -195,7 +195,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["byte-identical", "two runs, same bytes — a gate on every push, not a tendency; warm equals cold"], ["zero runtime deps", "CMake + a C++23 compiler; builds with the network off — vendored everything"], ["the languages", "Rust · C++ · ObjC/C++ · C · Metal · CUDA · Python · Go · Swift · TypeScript · JavaScript · Java · Kotlin · Ruby · PHP · Lua · Elixir · Dart · GDScript · Bash · C# · JSON · TOML · YAML · Markdown — 25 vendored grammars, Astro frontmatter on the TypeScript one; markdown headings are real symbols"], // GDScript: CHANGELOG [0.6.2] (#233); Astro: CHANGELOG [0.6.4] (#320) - ["agent-native", "an MCP server and 185 long flags behind one `--help` that is always the authority"], + ["agent-native", "an MCP server and 187 long flags behind one `--help` that is always the authority"], ]; // Row height carries the LONGEST body (the language line, which wraps to three at this width), // not the shortest — a fixed 0.68 clipped its last line off the bottom of the card. @@ -230,7 +230,7 @@ function storyCards(s, { kick, head, stories, footText }){ s.addText(flags, { x: MX+6.3, y: y+0.04, w: 5.65, h: 0.58, fontFace: MONO, fontSize: 9, color: CYAN, valign: "middle", margin: 0 }); y += 0.74; } - foot(s, "--help is generated from the binary's own flag table — 185 long flags; docs/COMMANDS.md carries an entry for every one of them, 162 with a recorded invocation and its output"); + foot(s, "--help is generated from the binary's own flag table — 187 long flags; docs/COMMANDS.md carries an entry for every one of them, 167 with a recorded invocation and its output"); } /* ── S5 · the moments ───────────────────────────────────────────────────── */ @@ -1537,7 +1537,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// do not take any of it on trust", AMBER); title(s, "Every claim, and the command that re-derives it"); const claims = [ - ["185 long flags · 36 slides", "bash test/deckclaimcheck.sh"], + ["187 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"], ["664 gate scripts", "bash test/manifestcheck.sh"], // gatecount diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index 9b7ed1441..8b2bb2aab 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -188,15 +188,17 @@ the kit-style default-lean posture (measured ~90% cut on comparable extract-symb copied out of a prior session or a doc is almost certainly stale — call `find_symbol`/ `find_referencing_symbols` fresh each session and read the `handle` it hands back. -## The per-session legend: read `ripwire://legend-dict` once - -Every answer defines its own attributes until the session reads the MCP resource `ripwire://legend-dict` -(`initialize` names it). After that read, the verbs that take `legend` (and `for`) answer rows first and -end with ``, which carries the root attributes; a definition arrives once -per session, in a comment after the rows of the first answer that needs it. Lost the definitions (a -compacted context)? Read `ripwire://legend-dict/full` for all of them, or print them with -`ripwire --legend-dict`. `legend:"compact"` on one call keeps that answer's legend inline. The HTTP -transport (`--listen`) keeps every legend inline. +## The per-session legend: each definition once per session + +On the stdio server the legend session opens with the first answer that carries a legend (served inline) or +with a read of the MCP resource `ripwire://legend-dict` (`initialize` names it). After that, the verbs that take +`legend` (and `for`) answer rows first and end with ``, which carries the +root attributes; a definition arrives once per session, in a comment after the rows of the first answer that +needs it (an answer whose ref form would be longer stays inline). Lost the definitions (a compacted context)? +Read `ripwire://legend-dict/full` for all of them, or print them with `ripwire --legend-dict`. +`legend:"compact"` on one call keeps that answer's legend inline; a server started with +`--mcp --mcp-legend=inline` keeps every answer's legend inline. The HTTP transport (`--listen`) keeps every +legend inline. ## The 3 MCP edit verbs — the warm-server counterpart to the preferred CLI diff --git a/test/anchorcheck.sh b/test/anchorcheck.sh index 932ab6cb5..907ea5365 100755 --- a/test/anchorcheck.sh +++ b/test/anchorcheck.sh @@ -108,6 +108,11 @@ QUERY="frobnicate widget cache" # ladder trimmed used to appear in neither section), and the clause defining the tail says so. Verified before # re-pinning: with every comment and est_tokens= normalized out, old and new documents are byte-identical — # this fixture's head covers every file, so its tail is unchanged and every ranking byte is unmoved. +# RE-PIN 2026-10-07 (lane/lean-answers-068, --for zero elision): 3557 -> 3533 B, est_tokens="1349" -> "1339". ONE +# identified change: a lens row omits cx=/ccx=/in= when 0 and the full legend says ", each absent when 0;". Verified +# before re-pinning: the base binary (255dc199) reproduces the previous golden byte for byte, and putting cx="0"/ccx="0"/ +# in="0" back on every r= row, deleting that clause and masking est_tokens= gives the previous golden exactly — no +# ranking, body or anchor byte moved (anchor-neutrality, this golden's purpose, untouched). # L1 (2026-09-19): the CLI default legend is compact; this arm compares against a golden recorded from the full default, so it asks for it. "$BIN" anchorfix --no-cache --for="$QUERY" --no-route --legend=full >"$TMP/plain_full.xml" 2>/dev/null diff -q "$TMP/plain_full.xml" "$ROOT/test/anchorfix/golden_for.xml" >/dev/null \ diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index 896989756..45df03416 100644 --- a/test/anchorfix/golden_for.xml +++ b/test/anchorfix/golden_for.xml @@ -1,4 +1,4 @@ -frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit() row tail. Widest 34 lit+digits + inAttr(<=23) + lens(qbuf, <=79) + pure(9) = 145 B." ), + ( "src/serialize.h", "tail" ): ( 3, "safe", "tail[192] x3: the row tail. Widest 34 lit+digits + inAttr(<=23) + lens(qbuf, <=79) + pure(9) = 145 B. The third (lane/lean-answers-068, sigRowTail's zero-elision branch) prints cxAttr/ccxAttr/inAttr pre-formatted, each omitted at 0 — never wider than the second." ), ( "src/serialize.h", "hdr" ): ( 15, "safe", "hdr[64] x2 (packBodies/packOutline): ' plus pageDisclosure's own capped buffer, sized against that cap by construction." ), ( "src/serialize.h", "hb" ): ( 3, "latent", "hb[176]: ; shape= is a fixed vocabulary but acd/nccd are %.1f/%.2f on DOUBLES, formally unbounded. Realistic worst case 66 lit + 60 digits + 24 float + 10 shape = 160 B, ~16 B of margin. Truncation drops the '/>' and orphans the element." ), @@ -260,12 +260,14 @@ NUMERIC_ONLY = { ( "src/serialize.h", "...)" ): 1, ( "src/serialize.h", "callsHdr" ): 2, ( "src/serialize.h", "cb" ): 1, + ( "src/serialize.h", "ccxAttr" ): 1, # lane/lean-answers-068: sigRowTail's zero elision, " ccx=\"{}\"" of one count (<= 17 B of 23) ( "src/serialize.h", "changedAttr" ): 1, + ( "src/serialize.h", "cxAttr" ): 1, # lane/lean-answers-068: the same, " cx=\"{}\"" (<= 16 B of 23) ( "src/serialize.h", "eb" ): 1, ( "src/serialize.h", "estAttr" ): 1, ( "src/serialize.h", "gb" ): 1, ( "src/serialize.h", "gfb" ): 1, - ( "src/serialize.h", "inAttr" ): 1, + ( "src/serialize.h", "inAttr" ): 2, # +1 lane/lean-answers-068: the zero-elision branch's own " in=\"{}\"" (<= 16 B of 23) ( "src/serialize.h", "kbuf" ): 1, ( "src/serialize.h", "lb" ): 4, # row 6 (2026-09-12): collectCalleeNameRow's line buffer (the merged row) joined the two # …and a FOURTH (2026-09-13, PR #215 item 8): appendMergedCalleeNameRows joins the row's @@ -594,7 +596,11 @@ if not bad: # 238 -> 239 calls/sites, 104 -> 105 rows). noteCacheReject's new `char detail[192]` names both version # numbers on a refused cache blob; one formatTo of a literal and two std::uint32_t, rowed 'not-markup' # (a stderr notice). Re-derived on the train 20 merged tree; no other train lane moves the population. -EXPECTED = { "mentions": 351, "calls": 241, "sites": 241, "rows": 105, "widthforms": 0 } +# 2026-10-07 (lane/lean-answers-068, --for zero elision in serialize.h sigRowTail): +4 calls/+4 mentions/+4 +# sites/+2 rows (351 -> 355 mentions, 241 -> 245 calls/sites, 105 -> 107 rows): new cxAttr[24] and ccxAttr[24] +# rows, a second inAttr[24] site and a third tail[192] site, all in the branch that omits a zero cx=/ccx=/in=. +# Each formats one integer or pre-formatted numeric attributes; classified above. Re-derived, not summed. +EXPECTED = { "mentions": 355, "calls": 245, "sites": 245, "rows": 107, "widthforms": 0 } # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the diff --git a/test/mcptranchecheck.sh b/test/mcptranchecheck.sh index 1dc0bb776..add2882ab 100755 --- a/test/mcptranchecheck.sh +++ b/test/mcptranchecheck.sh @@ -186,9 +186,26 @@ IMP_CLI="$( "$BIN" "$ROOT" --impact=escapeXml --limit=3 --offset=2 --legend=full [ "$IMP_BAT" = "$IMP_CLI" ] \ && ok "M4 [batch]: paged is byte-identical to the CLI page" \ || no "M4 [batch]: CLI [$IMP_CLI] vs batch [$IMP_BAT]" -WH="$( mcp_text "$( call whereis '{"path":"'"$ROOT"'","symbol":"runMcp","limit":2}' )" | tr '<' '\n' | grep '^whereis ' | head -1 )" +# lane/lean-answers-068: the whereis default may list only the definitions (listing="defs", references counted), and on a +# single-branch checkout runMcp has ONE def row, so limit=2 there cannot show 2. The arm keeps its exact assertion on the +# whole list (listing:"all", the pre-listing answer); the twin below holds limit= on the default's LISTED rows. +WH="$( mcp_text "$( call whereis '{"path":"'"$ROOT"'","symbol":"runMcp","limit":2,"listing":"all"}' )" | tr '<' '\n' | grep '^whereis ' | head -1 )" case "$WH" in *'shown="2"'*'has_more="1"'*'next_offset="2"'*) ok "M4 [live]: whereis honors limit= and discloses has_more/next_offset";; *) no "M4 [live]: whereis still ignores limit= — $WH";; esac +WHD="$( mcp_text "$( call whereis '{"path":"'"$ROOT"'","symbol":"runMcp","limit":1}' )" | tr '<' '\n' | grep '^whereis ' | head -1 )" +WHD_TOTAL="$( printf '%s' "$WHD" | grep -oE ' total="[0-9]+"' | grep -oE '[0-9]+' )" +WHD_SHOWN="$( printf '%s' "$WHD" | grep -oE ' shown="[0-9]+"' | grep -oE '[0-9]+' )" +if [ -n "$WHD_TOTAL" ] && [ -n "$WHD_SHOWN" ] && [ "$WHD_TOTAL" -ge 1 ] && [ "$WHD_SHOWN" = "1" ]; then + if [ "$WHD_TOTAL" -gt 1 ]; then + case "$WHD" in *'has_more="1"'*'next_offset="1"'*) ok "M4 [live, default listing twin]: limit=1 shows 1 of the $WHD_TOTAL listed rows and discloses has_more/next_offset";; + *) no "M4 [live, default listing twin]: $WHD_TOTAL listed rows, limit=1, no has_more/next_offset — $WHD";; esac + else + case "$WHD" in *'has_more="0"'*) ok "M4 [live, default listing twin]: limit=1 shows the 1 listed row, has_more=0";; + *) no "M4 [live, default listing twin]: 1 listed row but has_more is not 0 — $WHD";; esac + fi +else + no "M4 [live, default listing twin]: limit=1 on the default listing — shown='${WHD_SHOWN:-none}' total='${WHD_TOTAL:-none}' — $WHD" +fi echo echo "=== M5 — quality_delta speaks the CLI's key vocabulary ===" diff --git a/test/routecheck.sh b/test/routecheck.sh index 9adfb8357..54a63bcf5 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -111,6 +111,11 @@ CONCEPT="how does resolution work" # ladder trimmed used to appear in neither section), and the clause defining the tail says so. Verified before # re-pinning: with every comment and est_tokens= normalized out, old and new documents are byte-identical — # this fixture's head covers every file, so its tail is unchanged and every ranking byte is unmoved. +# RE-PIN 2026-10-07 (lane/lean-answers-068, --for zero elision): 3776 -> 3752 B, est_tokens="1433" -> "1424". ONE +# identified change: a lens row omits cx=/ccx=/in= when 0 and the full legend says ", each absent when 0;". Verified +# before re-pinning: the base binary (255dc199) reproduces the previous golden byte for byte, and putting the zeros back +# on every r= row, deleting that clause and masking est_tokens= gives the previous golden exactly — no ranking, body or +# route byte moved (route-neutrality, this golden's purpose, untouched). # L1 (2026-09-19): the CLI default legend is compact; golden_for.xml was recorded from the full default and the --query arm # reads the full legend's "routed:" comment, so those two runs ask for the full legend. "$BIN" routefix --no-cache --for="$CONCEPT" --no-route --legend=full >"$TMP/concept_noroute.xml" 2>/dev/null diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index 48407f2b1..1930319f2 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Thu, 8 Oct 2026 00:15:14 -0400 Subject: [PATCH 165/229] fix: CI reds of fix round 1 (gate readers for x=, identity, a throwing-API lookalike) - rubyrecvnarrowcheck edgesTo and rubyscopecheck's Facade arm count a merged via="name" row x="N" as N edges (assertions unchanged), as lpincheck and resolverhonestycheck already do. - legenddriftcheck: "x" joins its placeholder exclusions BY NAME (the list's own rule, as "d" did): x=N is an output attribute, no --x flag exists. - graph.h: the CSR-level hedge lookup is edgeNameOnlyInCsr, not an overload of edgeNameOnly: two overloads collapsed onto one in_id in selectorhonestycheck's --uses vs --callers set comparison (1444 vs 1445). - GoModReads::at -> read: hazardpatterncheck (C) reads `.at(` as the throwing std::map::at. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 10 +++++----- src/serialize.h | 2 +- test/legenddriftcheck.py | 5 ++++- test/rubyrecvnarrowcheck.sh | 3 ++- test/rubyscopecheck.sh | 4 +++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/graph.h b/src/graph.h index 5e5069c4f..1d08290fe 100644 --- a/src/graph.h +++ b/src/graph.h @@ -179,8 +179,8 @@ struct Graph // FE-B: is the call edge from → to NAME-ONLY, over the raw out-CSR and its parallel hedge bits — bound by name alone at // every site that bound it, so every surface renders it via="name". false for an edge that does not exist and for empty -// hedge bits (a graph with no name-only edge at all). The Graph overload below and serialize.h's block share it. -inline bool edgeNameOnly( const std::vector& outOff, const std::vector& outTargets, +// hedge bits (a graph with no name-only edge at all). edgeNameOnly( Graph ) below and serialize.h's block share it. +inline bool edgeNameOnlyInCsr( const std::vector& outOff, const std::vector& outTargets, const std::vector& nameOnly, NodeId from, NodeId to ) noexcept { if( nameOnly.empty() || std::size_t( from ) + 1 >= outOff.size() ) @@ -196,7 +196,7 @@ inline bool edgeNameOnly( const std::vector& outOff, const std::v inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept { EXPECTS( g.outNameOnly.empty() || g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits per edge, or none" ); - return edgeNameOnly( g.outOff, g.outTargets, g.outNameOnly, from, to ); + return edgeNameOnlyInCsr( g.outOff, g.outTargets, g.outNameOnly, from, to ); } // FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) @@ -5268,7 +5268,7 @@ struct GoModReads std::vector> replacedDirs; // the tree paths of the go.mod in disk directory `disk` (root-relative `modDir`), read on first use - const std::vector& at( const std::string& disk, std::string_view modDir ) + const std::vector& read( const std::string& disk, std::string_view modDir ) { auto [ it, fresh ] = treePaths.try_emplace( disk, std::vector{} ); if( fresh ) @@ -5357,7 +5357,7 @@ inline void collectGoModules( const IngestResult& ing, FalseEdgeRules& rules ) } disk.resize( diskCut ); const std::string_view modDir = relCut == std::string_view::npos ? std::string_view{} : rel.substr( 0, relCut ); - const std::vector& paths = reads.at( disk, modDir ); + const std::vector& paths = reads.read( disk, modDir ); if( !paths.empty() ) { rules.goUnderModule[ s.fileId ] = 1; diff --git a/src/serialize.h b/src/serialize.h index 64686c448..f527250d5 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5846,7 +5846,7 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& outOff, const std::vector& outTargets, NodeId id, NodeId cid, std::size_t& used, const CalleeCallsSink& sink ) { - const bool via = sink.nameOnly != nullptr && edgeNameOnly( outOff, outTargets, *sink.nameOnly, id, cid ); + const bool via = sink.nameOnly != nullptr && edgeNameOnlyInCsr( outOff, outTargets, *sink.nameOnly, id, cid ); if( via && sink.viaLegendCharged != nullptr && !*sink.viaLegendCharged ) { used += viaNameLegendComment().size(); diff --git a/test/legenddriftcheck.py b/test/legenddriftcheck.py index b27b3762d..8be1f8d3e 100755 --- a/test/legenddriftcheck.py +++ b/test/legenddriftcheck.py @@ -188,7 +188,10 @@ def extract_flag_tokens_from_legend(legend_text): # depth-labelled --impact (0.6.5): "d" joins for the same documented reason. src/graphlegend.h kImpactDepthLegend # defines the row's OUTPUT ATTRIBUTE as "d=N on : hop depth", and there is no --d flag (single-letter flags # do not exist in this CLI). Added by NAME, per this list's own rule. - placeholder_exclude = {"bodies", "bodyless", "overloads", "files", "hits", "toks", "noedge", "total", "shown", "d"} + # lane FE-B fix round 1: "x" joins for the same documented reason. The map's via legend (src/graphlegend.h + # kMapViaNameLegend) and the compact row define a merged map row's OUTPUT ATTRIBUTE as "x=N"; there is no --x + # flag. Added by NAME, per this list's own rule. + placeholder_exclude = {"bodies", "bodyless", "overloads", "files", "hits", "toks", "noedge", "total", "shown", "d", "x"} for match in re.finditer(r'\b([a-z][a-z0-9\-]*)=[NM]\b', legend_text): word = match.group(1) if word not in placeholder_exclude: diff --git a/test/rubyrecvnarrowcheck.sh b/test/rubyrecvnarrowcheck.sh index a18956d8c..5fa38a370 100755 --- a/test/rubyrecvnarrowcheck.sh +++ b/test/rubyrecvnarrowcheck.sh @@ -213,7 +213,8 @@ command -v xmllint >/dev/null 2>&1 && { if xmllint --noout "$MAP"; then ok "xmll SPLIT="$DIR/split"; sed 's/>\n"$SPLIT" rowOf(){ awk -v pat="$1" '$0 ~ pat{f=1;print;next} /^ row + its children -edgesTo(){ echo "$1" | grep -c " rows of that name +# how many edges of that name; FE-B: a merged via="name" row stands for N edges (serialize.h writeMapCalleeRows) +edgesTo(){ echo "$1" | grep -o "]*>" | awk '{ n = 1; if ( match( $0, / x="[0-9]+"/ ) ) n = substr( $0, RSTART + 4, RLENGTH - 5 ) + 0; s += n } END { print s + 0 }'; } echo "=== the fixture parsed the way this gate assumes ===" for want in 'n="add" sc="Calc"' 'n="add" sc="Tally"' 'n="run" sc="Engine"' 'n="run" sc="Motor"' \ diff --git a/test/rubyscopecheck.sh b/test/rubyscopecheck.sh index cde09aa66..922af8f07 100755 --- a/test/rubyscopecheck.sh +++ b/test/rubyscopecheck.sh @@ -164,7 +164,9 @@ DMAP="$( "$BIN" "$DEL" --no-cache 2>/dev/null | sed 's/>\n]*edges=[0-9]*' | grep -o 'edges=[0-9]*' | cut -d= -f2 )" if [ "${DEDGES:-0}" -eq 2 ]; then ok "facade delegation keeps both real targets (edges=2)"; else no "facade delegation: expected edges=2, got ${DEDGES:-0} — the caller won its own locality tie-break?"; fi FAC="$( echo "$DMAP" | awk '/n="publish_event" sc="Facade"/{f=1;print;next} /^ stands for N callee rows (serialize.h writeMapCalleeRows) +FAC_ROWS="$( echo "$FAC" | grep -o ']*>' | awk '{ n = 1; if ( match( $0, / x="[0-9]+"/ ) ) n = substr( $0, RSTART + 4, RLENGTH - 5 ) + 0; s += n } END { print s + 0 }' )" +if [ "$FAC_ROWS" -eq 2 ]; then ok "Facade::publish_event → two publish_event callee rows (Fanout, Subscriber)"; else no "Facade::publish_event callee rows: $FAC_ROWS"; fi if echo "$FAC" | grep -q 'amb="1"'; then ok "…disclosed as an honest split (amb=\"1\"), not a locality guess"; else no "Facade::publish_event is not marked amb=\"1\": $( echo "$FAC" | head -1 )"; fi echo "$FAC" | grep -q 'lpin=' && no "Facade::publish_event carries lpin= — the tie-break picked ONE target from a two-way tie" || ok "…and no lpin= (a two-way tie is left a tie)" From 45a2eeba5b7eae327be4c71020ee82dfacd6585d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 00:30:10 -0400 Subject: [PATCH 166/229] fix(for): the retry self-check reads the optional without a call (selfcheckcheck C) `ASSUME( !paidRc.has_value() )` put a non-accessor call inside a promise; `ASSUME( !paidRc )` states the same invariant (the paid run returned before writing anything, so it handed back no exit code). Co-Authored-By: Claude Opus 5.5 --- src/verbs_for.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/verbs_for.h b/src/verbs_for.h index c3c6a10cd..13c933193 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -3679,7 +3679,7 @@ std::optional runForLens( const MainDispatch& d ) { return paidRc; } - ASSUME( !paidRc.has_value() ); // the paid run returned before writing anything + ASSUME( !paidRc ); // the paid run returned before writing anything d.redactCounts = tallyAtEntry; return runForLensPass( d, /*sigsNextPayFromRows=*/false, nullptr ); } From 7d2f2e24b4f77863ecdc979a7b30fecc45ce76c8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 00:43:46 -0400 Subject: [PATCH 167/229] defect-shape: reuse the house helpers and keep touched functions under their bars (quality-delta clean) --quality-delta against the merge-base gated 19 rows on this lane's own code: clones of existing helpers and three preexisting functions over the ccx bar. Fixed, not acked: - defectshape.h reuses namesplit's ASCII identifier pair and stripQuotePair; isSpace is std::isspace; the compare-operator and needle checks are table lookups; skipQuoted is a single forward loop; the Bash entry point returns its scanner's vector. - quality.h: staleWhyForKind takes computeStaleAcks' per-kind dispatch, writeDefectSiteLines the sidecar's defect records, healDefectSites remapSnapshotIdentity's defect re-filing; computeSnapshot projects the site records with std::ranges::transform (tripwire re-pinned, refactor only). - The new-symbol= compact entry is shorter so the quality-delta compact legend stays under its 940 B pin. Co-Authored-By: Claude Opus 5.5 --- src/compactlegend.h | 2 +- src/defectshape.h | 69 ++++++++--------- src/quality.h | 158 +++++++++++++++++++++------------------ test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 3 + 5 files changed, 123 insertions(+), 111 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 64764d55e..df60903a5 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -708,7 +708,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // quality-delta: src/verbs_quality.h (root emit) + src/quality.h identityDisclosure { "stale", "stale=N: ack ledger rows whose target no longer applies (sa rows); never gating", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "preexisting-worse", "preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, - { "new-symbol", "new-symbol=N: regressions on NEW code; new-symbol rows never gate, except defect-shape format-arity; read them", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, + { "new-symbol", "new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "register-macro-excluded", "register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "declined-call-excluded", "declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, { "api-new-surface", "api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/defectshape.h b/src/defectshape.h index 26279c26e..9a644bc38 100644 --- a/src/defectshape.h +++ b/src/defectshape.h @@ -37,7 +37,10 @@ #include #include +#include + #include "infra/Diagnostics.h" +#include "infra/namesplit.h" // the ONE ASCII identifier-character pair and the quote-pair strip namespace rw::defectshape { @@ -83,16 +86,10 @@ struct Span namespace detail { -inline bool isIdentStart( char c ) noexcept -{ - const unsigned char u = static_cast( c ); - return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || c == '_' || u >= 0x80; -} - -inline bool isIdentChar( char c ) noexcept -{ - return isIdentStart( c ) || ( c >= '0' && c <= '9' ); -} +// identifiers are read ASCII-only, the house rule (namesplit.h): a non-ASCII byte outside a literal becomes a +// one-byte punctuator, which no shape below matches. +using rw::namesplit::isIdentChar; +using rw::namesplit::isIdentStart; inline bool isDigit( char c ) noexcept { @@ -101,7 +98,7 @@ inline bool isDigit( char c ) noexcept inline bool isSpace( char c ) noexcept { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; + return std::isspace( static_cast( c ) ) != 0; } enum class TokKind : std::uint8_t @@ -161,12 +158,20 @@ inline std::uint32_t punctLen( std::string_view s, std::size_t i ) noexcept // end of the line for an unterminated literal). Backslash escapes the next byte. inline std::size_t skipQuoted( std::string_view s, std::size_t i, char q ) noexcept { - std::size_t j = i + 1; - while( j < s.size() && s[j] != q && s[j] != '\n' ) + bool escaped = false; + for( std::size_t j = i + 1; j < s.size(); ++j ) { - j += ( s[j] == '\\' && j + 1 < s.size() ) ? 2 : 1; + if( s[j] == '\n' ) + { + return j; // unterminated on its line: stop there rather than swallow the file + } + if( !escaped && s[j] == q ) + { + return j + 1; + } + escaped = !escaped && s[j] == '\\'; } - return ( j < s.size() && s[j] == q ) ? j + 1 : j; + return s.size(); } // C++ tokens with comments and preprocessor directives dropped. A directive is skipped whole (continuation @@ -988,8 +993,8 @@ inline std::string_view sizeCallReceiver( std::string_view src, const std::vecto inline bool isCompareOp( std::string_view src, const Tok& t ) noexcept { - const std::string_view w = tokText( src, t ); - return w == ">" || w == ">=" || w == "<" || w == "<=" || w == "=="; + static constexpr std::array kCompare = { ">", ">=", "<", "<=", "==" }; + return std::find( kCompare.begin(), kCompare.end(), tokText( src, t ) ) != kCompare.end(); } // Name tokens of an identifier (camelCase and snake_case boundaries), lower-cased. @@ -1640,11 +1645,7 @@ using ShLineToks = std::vector>; // every logical line's to inline std::string_view unquoted( std::string_view w ) noexcept { - if( w.size() >= 2 && ( w.front() == '"' || w.front() == '\'' ) && w.back() == w.front() ) - { - return w.substr( 1, w.size() - 2 ); - } - return w; + return rw::namesplit::stripQuotePair( w ); } // "$VAR" / "${VAR}" / $VAR → VAR, else empty. @@ -2460,12 +2461,13 @@ inline std::string collapseSpaces( std::string_view s ) return out; } -inline void scanVacuousAssertBash( std::string_view src, const std::vector& spans, std::vector& out ) +inline std::vector scanVacuousAssertBash( std::string_view src, const std::vector& spans ) { + std::vector out; const std::vector lines = shLogicalLines( src ); if( lines.empty() ) { - return; + return out; } std::vector scope( lines.size() ); ShLineToks lt( lines.size() ); @@ -2543,6 +2545,7 @@ inline void scanVacuousAssertBash( std::string_view src, const std::vector out.push_back( { Facet::VacuousAssert, lines[a.line].startByte, collapseSpaces( lines[a.line].text ) } ); } } + return out; } } // namespace detail @@ -2586,24 +2589,16 @@ inline std::vector scanPython( std::string_view src ) // definitions, the scopes a capture and its guards are matched in. inline std::vector scanBashTestScript( std::string_view src, const std::vector& fnSpans ) { - std::vector out; - detail::scanVacuousAssertBash( src, fnSpans, out ); - return out; + return detail::scanVacuousAssertBash( src, fnSpans ); } // A cheap byte prefilter: can this C++ file hold any of the three C++ shapes at all? inline bool cppMayHoldShapes( std::string_view src ) noexcept { - for( std::string_view needle : { std::string_view( "format" ), std::string_view( "print" ), std::string_view( "emitTo" ), std::string_view( "formatTo" ), - std::string_view( "unique" ), std::string_view( "..." ), std::string_view( "\xE2\x80\xA6" ), std::string_view( "xE2" ), - std::string_view( "u2026" ) } ) - { - if( src.find( needle ) != std::string_view::npos ) - { - return true; - } - } - return false; + static constexpr std::array kNeedles = { "format", "print", "emitTo", "unique", "...", "\xE2\x80\xA6", "u2026" }; + // "format" also covers formatTo; the escaped ellipsis "\xE2\x80\xA6" is found through its "xE2" bytes below + return std::any_of( kNeedles.begin(), kNeedles.end(), [ & ]( std::string_view n ) { return src.find( n ) != std::string_view::npos; } ) + || src.find( "xE2" ) != std::string_view::npos; } } // namespace rw::defectshape diff --git a/src/quality.h b/src/quality.h index 19eac908c..a375b41f6 100644 --- a/src/quality.h +++ b/src/quality.h @@ -4729,17 +4729,6 @@ inline std::vector defectSitesOf( const IngestResult& ing, std::st return out; } -inline std::vector defectSiteKeys( const std::vector& sites ) -{ - std::vector out; - out.reserve( sites.size() ); - for( const DefectSiteAt& s : sites ) - { - out.push_back( s.key ); - } - return out; -} - inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root, const std::vector* fileInBaseline ) { @@ -4798,7 +4787,11 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // the defect-shape baseline: every offending site (a sorted multiset of anchor + site identity) and each // scanned file's presence record. Filtered to the baseline files inside defectSitesOf (IDENTITY BASIS). - snap.defectSites = defectSiteKeys( defectSitesOf( ing, root, fileInBaseline ) ); + { + const std::vector sites = defectSitesOf( ing, root, fileInBaseline ); + snap.defectSites.resize( sites.size() ); + std::ranges::transform( sites, snap.defectSites.begin(), &DefectSiteAt::key ); + } // §D#4 short-horizon-churn baseline: per-canonId RAW-body hash so the delta detects a rewrite that moved no // metric (a literal-only edit). Compared, never bar-checked — presence-or-difference IS the rewrite signal. @@ -5107,6 +5100,15 @@ inline int openBaselineSidecar( const std::string& path ) // than absorb — and it is written as two records the snapshot reader skips as unknown kinds (`dirty 1`, // `absorbed N`), exactly like the `head` stamp above: the fact has to outlive the process that knew it, // because the report it changes the meaning of is every LATER --quality-delta, not this run. +// The defect-shape records of a baseline sidecar: "defect ", both hex, one per record. +inline void writeDefectSiteLines( std::ostream& f, const std::vector& sites ) +{ + for( const DefectSiteKey& d : sites ) + { + f << "defect " << std::hex << d.anchor << ' ' << d.site << std::dec << '\n'; + } +} + inline bool writeBaseline( const Snapshot& s, const std::string& path, std::string_view headSha = {}, std::size_t absorbedGating = 0 ) { @@ -5209,10 +5211,7 @@ inline bool writeBaseline( const Snapshot& s, const std::string& path, std::stri { f << "api " << std::hex << h << std::dec << '\n'; } - for( const DefectSiteKey& d : s.defectSites ) - { - f << "defect " << std::hex << d.anchor << ' ' << d.site << std::dec << '\n'; // the defect-shape kind: anchor + site identity (both hex) - } + writeDefectSiteLines( f, s.defectSites ); // Was an unconditional `return true`: a stream that failed to flush still reported a written baseline. // The descriptor answers for the bytes, so a full disk is now a failure the caller can report. return rw::pathguard::writeAllAndClose( fd, f.str() ); @@ -7386,6 +7385,35 @@ inline void addSchemeAliases( IdentityAliases& al, const IngestResult& ing, cons std::sort( al.schemeAmbiguousKeys.begin(), al.schemeAmbiguousKeys.end() ); } +// The defect-shape records: a renamed definition's sites are re-filed under its current anchor, add-never- +// overwrite like the per-symbol maps (an anchor the baseline already holds keeps its own records). Whether a site +// is NEW never depends on this — that is decided by its text, repo-wide — only the row's was= count does. +inline std::size_t healDefectSites( Snapshot& base, const IdentityAliases& al ) +{ + std::size_t healed = 0; + std::vector addSites; + for( const auto& [ from, to ] : al.toCurrent ) + { + const auto lo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ from, 0 } ); + const auto toLo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ to, 0 } ); + if( toLo != base.defectSites.end() && toLo->anchor == to ) + { + continue; + } + for( auto it = lo; it != base.defectSites.end() && it->anchor == from; ++it ) + { + addSites.push_back( { to, it->site } ); + ++healed; + } + } + if( !addSites.empty() ) + { + base.defectSites.insert( base.defectSites.end(), addSites.begin(), addSites.end() ); + std::sort( base.defectSites.begin(), base.defectSites.end() ); + } + return healed; +} + // Heal a BASELINE snapshot forward: an entry recorded under a pre-rename identity is re-filed under the // identity the current tree uses, so computeDelta's `was` lookups, its origin oracle and its churn join all // find it without a single edit to any of them. @@ -7446,29 +7474,7 @@ inline std::size_t remapSnapshotIdentity( Snapshot& base, const IdentityAliases& healSet( base.publicApi ); // base.cloneGroups is deliberately untouched — see the WHAT IS NOT REMAPPED note above. - // the defect-shape records: a renamed definition's sites are re-filed under its current anchor, same - // add-never-overwrite rule (an anchor the baseline already holds keeps its own records). Whether a site is - // NEW never depends on this — that is decided by its text, repo-wide — only the row's was= count does. - std::vector addSites; - for( const auto& [ from, to ] : al.toCurrent ) - { - const auto lo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ from, 0 } ); - const auto toLo = std::lower_bound( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ to, 0 } ); - if( toLo != base.defectSites.end() && toLo->anchor == to ) - { - continue; - } - for( auto it = lo; it != base.defectSites.end() && it->anchor == from; ++it ) - { - addSites.push_back( { to, it->site } ); - ++moved; - } - } - if( !addSites.empty() ) - { - base.defectSites.insert( base.defectSites.end(), addSites.begin(), addSites.end() ); - std::sort( base.defectSites.begin(), base.defectSites.end() ); - } + moved += healDefectSites( base, al ); return moved; } @@ -7898,6 +7904,46 @@ inline std::optional staleForDefectShape( std::uint64_t key, const return StaleAckWhy::FindingGone; } +// Which oracle answers whether an ack of kind `base` is stale — one dispatch per finding kind. An unrecognized +// kind (a future addition, or a hand-edited line) answers nullopt: left unclassified, never guessed. +inline std::optional staleWhyForKind( std::string_view base, std::uint64_t key, const Snapshot& snap ) +{ + std::optional why; + if( base == "complexity" || base == "verbosity" || base == "nesting" || base == "params" ) + { + why = staleForMetricKind( base, key, snap ); + } + else if( base == "dead-code" ) + { + why = staleForSetMembership( key, snap, snap.dead ); + } + else if( base == "api-surface" ) + { + why = staleForSetMembership( key, snap, snap.publicApi ); + } + else if( base == "error-masking" ) + { + why = staleForConstructCount( key, snap, snap.maskBySym ); + } + else if( base == "placeholder" ) + { + why = staleForConstructCount( key, snap, snap.placeholderBySym ); + } + else if( base == "duplication" || base == "new-clone-of-reused-helper" ) + { + why = staleForCloneKind( key, snap ); + } + else if( base == "short-horizon-churn" ) + { + why = staleForChurn( key, snap ); + } + else if( base == "defect-shape" ) + { + why = staleForDefectShape( key, snap ); + } + return why; +} + inline std::vector computeStaleAcks( const gtl::btree_map& acks, const Snapshot& snap ) { std::vector out; @@ -7911,39 +7957,7 @@ inline std::vector computeStaleAcks( const gtl::btree_map why; - if( base == "complexity" || base == "verbosity" || base == "nesting" || base == "params" ) - { - why = staleForMetricKind( base, rec.key, snap ); - } - else if( base == "dead-code" ) - { - why = staleForSetMembership( rec.key, snap, snap.dead ); - } - else if( base == "api-surface" ) - { - why = staleForSetMembership( rec.key, snap, snap.publicApi ); - } - else if( base == "error-masking" ) - { - why = staleForConstructCount( rec.key, snap, snap.maskBySym ); - } - else if( base == "placeholder" ) - { - why = staleForConstructCount( rec.key, snap, snap.placeholderBySym ); - } - else if( base == "duplication" || base == "new-clone-of-reused-helper" ) - { - why = staleForCloneKind( rec.key, snap ); - } - else if( base == "short-horizon-churn" ) - { - why = staleForChurn( rec.key, snap ); - } - else if( base == "defect-shape" ) - { - why = staleForDefectShape( rec.key, snap ); - } + const std::optional why = staleWhyForKind( base, rec.key, snap ); // an unrecognized kind (a future addition, or a hand-edited line) is left unclassified rather than // guessed — same "degrade, do not fabricate" rule readAckRecords already applies to a malformed line. if( why.has_value() ) diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 7d67600f6..4a415573b 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -2244e7390c2f69c695c9e1f51d39a2e296bcb8418b238c34c4ca89ccc6a55ef7 +2fdb42dc0a53edccb8084f04799fd51505467aaaf689763afd579ae01c3c1b8d diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index c0991de03..227d25979 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,9 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-08, lane/cr-qd-kinds-068 cleanup: RE-PINNED with UPDATE_GOLDEN=1 (hash 2fdb42dc0a…3c1b8d). Refactor only: +# computeSnapshot projects defectSitesOf's records with std::ranges::transform instead of a helper loop (the same +# records in the same order); kQSnapCacheScheme stays 18. # 2026-10-07, lane/cr-qd-kinds-068 (the defect-shape kind): RE-DERIVED with UPDATE_GOLDEN=1 (hash 2244e7390c…a55ef7). # kQSnapCacheScheme 17 -> 18: the blob gained the defect-shape (anchor, site) records after publicApi (serializeSnapshot, # deserializeSnapshot and computeSnapshot all moved), a BLOB SHAPE change. The manifest gains defectSitesOf (the site From 1f7f6356594adb7e3ebb85c29f5cbf85a2fdcc2b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 00:46:30 -0400 Subject: [PATCH 168/229] quality: computeDelta hands the defect-shape rows to appendDefectShapeRows The twelfth kind's block moves out of computeDelta into its own function, called with computeDelta's origin oracle and locator; computeDelta stays at its pre-lane size. No behaviour change. Co-Authored-By: Claude Opus 5.5 --- src/quality.h | 260 ++++++++++++++++++++++++++------------------------ 1 file changed, 133 insertions(+), 127 deletions(-) diff --git a/src/quality.h b/src/quality.h index a375b41f6..03401b101 100644 --- a/src/quality.h +++ b/src/quality.h @@ -8242,6 +8242,138 @@ inline std::pair scopeDisclosure( const Scope& scope, return out; } +// ── defect-shape (the twelfth kind) ─────────────────────────────────────────────────────────────────── +// A site is NEW when its identity (facet + normalized text) occurs more often in the current tree than in +// the baseline, REPO-WIDE: an untouched, moved or renamed defect is not new, an edited one is, and a swap +// (one site fixed and another added in the same definition) still is. The excess of each identity is +// credited to the anchors that hold more copies of it now than at the baseline, in anchor-key order +// (deterministic). An anchor credited with a new site of a facet gets ONE row for that facet; was= and now= +// count the anchor's sites of that facet on each side. format-arity gates on ANY origin — a placeholder / +// argument mismatch is a defect, not debt; utf8-cut, dedup-first and vacuous-assert are report-only: +// sev="minor" by facet, not by size. A file the baseline could not read judges none of its sites. +// Called from computeDelta with its origin oracle (existedAtBaseline) and its locator (stampLoc). +template +inline void appendDefectShapeRows( std::vector& regs, const IngestResult& ing, const Graph& g, const Snapshot& base, + std::string_view root, ExistedFn&& existedAtBaseline, StampFn&& stampLoc ) +{ + const std::vector nowSites = defectSitesOf( ing, root, nullptr ); + const auto isRealSite = []( std::uint64_t v ) { return v != kDefectFilePresent && v != kDefectFileUnreadable; }; + std::vector baseVals, nowVals, baseUnreadable; + for( const DefectSiteKey& d : base.defectSites ) + { + if( isRealSite( d.site ) ) + { + baseVals.push_back( d.site ); + } + else if( d.site == kDefectFileUnreadable ) + { + baseUnreadable.push_back( d.anchor ); + } + } + for( const DefectSiteAt& d : nowSites ) + { + if( isRealSite( d.key.site ) ) + { + nowVals.push_back( d.key.site ); + } + } + std::sort( baseVals.begin(), baseVals.end() ); + std::sort( nowVals.begin(), nowVals.end() ); + std::sort( baseUnreadable.begin(), baseUnreadable.end() ); + const auto countOf = []( const std::vector& v, std::uint64_t x ) + { + const auto r = std::equal_range( v.begin(), v.end(), x ); + return static_cast( r.second - r.first ); + }; + // the repo-wide excess still to credit, per identity + std::vector> excess; + for( std::size_t k = 0; k < nowVals.size(); ) + { + const std::uint64_t v = nowVals[k]; + const std::uint32_t nN = countOf( nowVals, v ); + const std::uint32_t nB = countOf( baseVals, v ); + if( nN > nB ) + { + excess.push_back( { v, nN - nB } ); + } + k += nN; + } + const auto baseAnchorCount = [ & ]( std::uint64_t anchor, std::uint64_t site ) + { + const auto r = std::equal_range( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ anchor, site } ); + return static_cast( r.second - r.first ); + }; + const auto facetCount = []( const auto& sites, std::uint64_t anchor, defectshape::Facet f, auto keyOf ) + { + std::uint32_t n = 0; + for( const auto& s : sites ) + { + const DefectSiteKey& k = keyOf( s ); + n += ( k.anchor == anchor && k.site != kDefectFilePresent && k.site != kDefectFileUnreadable && defectFacetOf( k.site ) == f ) ? 1u : 0u; + } + return n; + }; + struct Credited + { + std::uint64_t anchor = 0; + defectshape::Facet facet = defectshape::Facet::FormatArity; + const DefectSiteAt* first = nullptr; + }; + std::vector credited; + for( std::size_t k = 0; k < nowSites.size(); ) + { + const DefectSiteKey key = nowSites[k].key; + std::size_t e = k; + while( e < nowSites.size() && nowSites[e].key == key ) + { + ++e; + } + const std::uint32_t headA = static_cast( e - k ); + const DefectSiteAt& at = nowSites[k]; + k = e; + if( !isRealSite( key.site ) || at.fileId >= ing.files.size() + || std::binary_search( baseUnreadable.begin(), baseUnreadable.end(), defectFileAnchor( relForHash( ing.files[ at.fileId ], root ) ) ) ) + { + continue; + } + const std::uint32_t baseA = baseAnchorCount( key.anchor, key.site ); + const auto ex = std::lower_bound( excess.begin(), excess.end(), std::pair{ key.site, 0 } ); + if( headA <= baseA || ex == excess.end() || ex->first != key.site || ex->second == 0 ) + { + continue; + } + ex->second -= std::min( ex->second, headA - baseA ); + const defectshape::Facet f = defectFacetOf( key.site ); + const bool seen = std::any_of( credited.begin(), credited.end(), [ & ]( const Credited& c ) { return c.anchor == key.anchor && c.facet == f; } ); + if( !seen ) + { + credited.push_back( { key.anchor, f, &at } ); + } + } + for( const Credited& c : credited ) + { + const std::uint32_t was = facetCount( base.defectSites, c.anchor, c.facet, []( const DefectSiteKey& d ) -> const DefectSiteKey& { return d; } ); + const std::uint32_t now = facetCount( nowSites, c.anchor, c.facet, []( const DefectSiteAt& d ) -> const DefectSiteKey& { return d.key; } ); + const NodeId owner = c.first->owner; + const std::string rel( relForHash( ing.files[ c.first->fileId ], root ) ); + const bool isNew = ( owner != kNoNode ) + ? !existedAtBaseline( c.anchor ) + : !std::binary_search( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ c.anchor, kDefectFilePresent } ); + const std::string sym = ( owner != kNoNode && owner < g.canonId.size() && !g.canonId[owner].empty() ) ? g.canonId[owner] : rel; + regs.push_back( { "defect-shape", sym, was, now, defectRowKey( c.anchor, c.facet ), !defectshape::facetGatesOnAnyOrigin( c.facet ), + std::string( defectshape::facetName( c.facet ) ), isNew } ); + if( owner != kNoNode ) + { + stampLoc( owner ); + } + else + { + regs.back().path = rel; + regs.back().line = c.first->line; + } + } +} + // current state vs baseline → only what got worse. // complexity/verbosity/nesting/params: a symbol the change pushed/kept OVER the bar (now > baseline AND // now > BAR; a NEW symbol counts with baseline 0). Each is a per-symbol metric whose overloads share a @@ -8917,133 +9049,7 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap } } - // ── defect-shape (the twelfth kind) ─────────────────────────────────────────────────────────────────── - // A site is NEW when its identity (facet + normalized text) occurs more often in the current tree than in - // the baseline, REPO-WIDE: an untouched, moved or renamed defect is not new, an edited one is, and a swap - // (one site fixed and another added in the same definition) still is. The excess of each identity is - // credited to the anchors that hold more copies of it now than at the baseline, in anchor-key order - // (deterministic). An anchor credited with a new site of a facet gets ONE row for that facet; was= and now= - // count the anchor's sites of that facet on each side. format-arity gates on ANY origin — a placeholder / - // argument mismatch is a defect, not debt; utf8-cut, dedup-first and vacuous-assert are report-only: - // sev="minor" by facet, not by size. A file the baseline could not read judges none of its sites. - { - const std::vector nowSites = defectSitesOf( ing, root, nullptr ); - const auto isRealSite = []( std::uint64_t v ) { return v != kDefectFilePresent && v != kDefectFileUnreadable; }; - std::vector baseVals, nowVals, baseUnreadable; - for( const DefectSiteKey& d : base.defectSites ) - { - if( isRealSite( d.site ) ) - { - baseVals.push_back( d.site ); - } - else if( d.site == kDefectFileUnreadable ) - { - baseUnreadable.push_back( d.anchor ); - } - } - for( const DefectSiteAt& d : nowSites ) - { - if( isRealSite( d.key.site ) ) - { - nowVals.push_back( d.key.site ); - } - } - std::sort( baseVals.begin(), baseVals.end() ); - std::sort( nowVals.begin(), nowVals.end() ); - std::sort( baseUnreadable.begin(), baseUnreadable.end() ); - const auto countOf = []( const std::vector& v, std::uint64_t x ) - { - const auto r = std::equal_range( v.begin(), v.end(), x ); - return static_cast( r.second - r.first ); - }; - // the repo-wide excess still to credit, per identity - std::vector> excess; - for( std::size_t k = 0; k < nowVals.size(); ) - { - const std::uint64_t v = nowVals[k]; - const std::uint32_t nN = countOf( nowVals, v ); - const std::uint32_t nB = countOf( baseVals, v ); - if( nN > nB ) - { - excess.push_back( { v, nN - nB } ); - } - k += nN; - } - const auto baseAnchorCount = [ & ]( std::uint64_t anchor, std::uint64_t site ) - { - const auto r = std::equal_range( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ anchor, site } ); - return static_cast( r.second - r.first ); - }; - const auto facetCount = []( const auto& sites, std::uint64_t anchor, defectshape::Facet f, auto keyOf ) - { - std::uint32_t n = 0; - for( const auto& s : sites ) - { - const DefectSiteKey& k = keyOf( s ); - n += ( k.anchor == anchor && k.site != kDefectFilePresent && k.site != kDefectFileUnreadable && defectFacetOf( k.site ) == f ) ? 1u : 0u; - } - return n; - }; - struct Credited - { - std::uint64_t anchor = 0; - defectshape::Facet facet = defectshape::Facet::FormatArity; - const DefectSiteAt* first = nullptr; - }; - std::vector credited; - for( std::size_t k = 0; k < nowSites.size(); ) - { - const DefectSiteKey key = nowSites[k].key; - std::size_t e = k; - while( e < nowSites.size() && nowSites[e].key == key ) - { - ++e; - } - const std::uint32_t headA = static_cast( e - k ); - const DefectSiteAt& at = nowSites[k]; - k = e; - if( !isRealSite( key.site ) || at.fileId >= ing.files.size() - || std::binary_search( baseUnreadable.begin(), baseUnreadable.end(), defectFileAnchor( relForHash( ing.files[ at.fileId ], root ) ) ) ) - { - continue; - } - const std::uint32_t baseA = baseAnchorCount( key.anchor, key.site ); - const auto ex = std::lower_bound( excess.begin(), excess.end(), std::pair{ key.site, 0 } ); - if( headA <= baseA || ex == excess.end() || ex->first != key.site || ex->second == 0 ) - { - continue; - } - ex->second -= std::min( ex->second, headA - baseA ); - const defectshape::Facet f = defectFacetOf( key.site ); - const bool seen = std::any_of( credited.begin(), credited.end(), [ & ]( const Credited& c ) { return c.anchor == key.anchor && c.facet == f; } ); - if( !seen ) - { - credited.push_back( { key.anchor, f, &at } ); - } - } - for( const Credited& c : credited ) - { - const std::uint32_t was = facetCount( base.defectSites, c.anchor, c.facet, []( const DefectSiteKey& d ) -> const DefectSiteKey& { return d; } ); - const std::uint32_t now = facetCount( nowSites, c.anchor, c.facet, []( const DefectSiteAt& d ) -> const DefectSiteKey& { return d.key; } ); - const NodeId owner = c.first->owner; - const std::string rel( relForHash( ing.files[ c.first->fileId ], root ) ); - const bool isNew = ( owner != kNoNode ) - ? !existedAtBaseline( c.anchor ) - : !std::binary_search( base.defectSites.begin(), base.defectSites.end(), DefectSiteKey{ c.anchor, kDefectFilePresent } ); - const std::string sym = ( owner != kNoNode && owner < g.canonId.size() && !g.canonId[owner].empty() ) ? g.canonId[owner] : rel; - regs.push_back( { "defect-shape", sym, was, now, defectRowKey( c.anchor, c.facet ), !defectshape::facetGatesOnAnyOrigin( c.facet ), - std::string( defectshape::facetName( c.facet ) ), isNew } ); - if( owner != kNoNode ) - { - stampLoc( owner ); - } - else - { - regs.back().path = rel; - regs.back().line = c.first->line; - } - } - } + appendDefectShapeRows( regs, ing, g, base, root, existedAtBaseline, stampLoc ); // the twelfth kind (see its doc) // ── §D#4-2 short-horizon churn (GitClear +15% "new code rewritten within two weeks") ─────────────────── // A symbol flags iff THREE independent gates all hold (signal-to-noise round, 2026-07-13): From a584b8b94c56f1131ad13c61c6219ab3813581ee Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 00:54:17 -0400 Subject: [PATCH 169/229] --for: no code-above-docs reorder (or its note) under a body ceiling either explicitCeilingTighterThanDefault now takes the body-ceiling case (--max-tokens with --detail) beside the token-budget half, so e= and the reorder read ONE predicate; docsAfterCodeFitsCeiling passes it through. On the ts fixture at --max-tokens=1600 --detail=1 the uncharged docs_after_code note had pushed est_tokens 1534 -> 1634 with over_ceiling; e= already stayed off there, so only the reorder leaked. forsigspancheck (T) gains body-ceiling arms (ts and a new pydocs fixture), a --detail-alone near miss, and the JSON/MCP refusals. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 ++++--- src/filter.h | 2 +- src/mcpverbs.h | 2 +- src/serialize.h | 23 +++++++++------- src/verbs_for.h | 4 +-- test/forcompletefix/pydocs/docs/cache.md | 5 ++++ test/forcompletefix/pydocs/docs/pump.md | 15 +++++++++++ .../pydocs/src/pump/__init__.py | 0 test/forcompletefix/pydocs/src/pump/app.py | 16 ++++++++++++ test/forcompletefix/pydocs/src/pump/cache.py | 20 ++++++++++++++ test/forcompletefix/pydocs/src/pump/loop.py | 21 +++++++++++++++ .../pydocs/src/pump/resolver.py | 15 +++++++++++ test/forsigspancheck.sh | 26 +++++++++++++++++++ 13 files changed, 140 insertions(+), 18 deletions(-) create mode 100644 test/forcompletefix/pydocs/docs/cache.md create mode 100644 test/forcompletefix/pydocs/docs/pump.md create mode 100644 test/forcompletefix/pydocs/src/pump/__init__.py create mode 100644 test/forcompletefix/pydocs/src/pump/app.py create mode 100644 test/forcompletefix/pydocs/src/pump/cache.py create mode 100644 test/forcompletefix/pydocs/src/pump/loop.py create mode 100644 test/forcompletefix/pydocs/src/pump/resolver.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d5b21d3..97cecc00d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,10 +38,11 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli choose which rows are shown, so no doc row is ever evicted by it; the shown rows' `r=` values are kept as a set and reassigned in the new order (so for a moved row `r=` is no longer its score rank), and `` (JSON `"docs_after_code"`) says how many doc rows moved. A question that names docs (`doc…`, `readme`, `guide`, `tutorial`, `manual`, `markdown`, `wiki`, `faq`, `howto`, a - change or translation cue, or a named `.md`/`.rst` file) keeps the plain score order, and so does `--no-route`; - An answer under an explicit ceiling TIGHTER than the default signature budget - (`--token-budget` below it, MCP `budget_tokens` likewise) is not reordered and carries no `docs_after_code`: it is the answer - it was before the reorder, so its est_tokens promise stays what it was (the reading would be uncharged bytes). + change or translation cue, or a named `.md`/`.rst` file) keeps the plain score order, and so does `--no-route`. + An answer under an explicit ceiling TIGHTER than the default signature budget (`--token-budget` below it, MCP + `budget_tokens` below it, or a body ceiling, `--max-tokens` with `--detail`) is not reordered and carries no + `docs_after_code`: it is the answer it was before the reorder, so its est_tokens promise stays what it was (the reading + would be uncharged bytes). These are the same three ceilings that drop `e=`, decided by one predicate. `RIPWIRE_NO_DOCS_AFTER_CODE`, set to any value (even empty or `0`), turns the reorder off everywhere (CLI and MCP `for`). Gate: `forsigspancheck` (fixture `test/forcompletefix`, C/JS/Python/TS). diff --git a/src/filter.h b/src/filter.h index 4851b59af..65ae45dff 100644 --- a/src/filter.h +++ b/src/filter.h @@ -888,7 +888,7 @@ inline std::vector docNoiseSymbolMultipliers( const IngestResult& ing, st // row. A question that ASKS about docs keeps the plain score order: a docs cue (kDocsQuestionCues), a change or // translation cue (the doc-noise lists above: those questions are about prose too), or a named doc file. A false cue // is the cheap side to be wrong on — it restores the old order. Under an explicit ceiling TIGHTER than the default signature -// share (serialize.h explicitCeilingTighterThanDefault) the reorder and its uncharged note do not apply: the answer is the plain +// share, or a body ceiling (serialize.h explicitCeilingTighterThanDefault) the reorder and its uncharged note do not apply: the answer is the plain // score-order one, so est_tokens <= the budget keeps holding. RIPWIRE_NO_DOCS_AFTER_CODE=1 is the A/B handle (the // RIPWIRE_NO_DOC_MENTION precedent): the same answer in plain score order, which the gate compares the row SET against. inline constexpr DocNoiseCue kDocsQuestionCues[] = { { "doc", true }, { "readme", true }, { "guide", true }, { "tutorial", true }, diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 122c494f4..ba659f4de 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2218,7 +2218,7 @@ inline std::optional forTaskText( const std::string& root, const st notesPtr, // L3: field-notes surfacing (inert when null) flRootArg, // R-E: root-relative p=, same argument the CLI twin passes rw::forLensRules( !noRoute && !taskAsksAboutDocs( task ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) - && rw::docsAfterCodeFitsCeiling( budgetTokens ), + && rw::docsAfterCodeFitsCeiling( budgetTokens, /*bodyCeiling=*/false ), rw::endLinesFitCeiling( budgetTokens, /*bodyCeiling=*/false ) ), // LB-A, e=, docs reorder &mcpDroppedPositive, // A2: exact count, see droppedPositiveCount (serialize.h) &mcpShownIds, // lane 2: see verbs_for.h shownSigIds diff --git a/src/serialize.h b/src/serialize.h index 94e59328a..46fb1d7e5 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4399,20 +4399,22 @@ inline ForEndLinesMode forEndLinesMode() }(); return mode; } -// THE tight-explicit-ceiling predicate (--token-budget / MCP budget_tokens set, and below the default signature share -// kForPayloadBudgetBytes). One definition for the two decisions that must not add an uncharged byte there: e= (above) and the -// code-above-docs reorder with its reading (docsAfterCodeFitsCeiling). 0 = no explicit ceiling. -inline constexpr bool explicitCeilingTighterThanDefault( std::size_t budgetTokens ) noexcept +// THE tight-explicit-ceiling predicate: the answer carries an explicit ceiling it promises (est_tokens <= it) and that ceiling is +// tighter than the default signature share — --token-budget / MCP budget_tokens set and below kForPayloadBudgetBytes, OR a body +// ceiling (--max-tokens with --detail; `bodyCeiling`, which has no default share to compare with). One definition for the two +// decisions that must not add an uncharged byte there: e= (below) and the code-above-docs reorder with its reading +// (docsAfterCodeFitsCeiling). budgetTokens 0 = no token budget; MCP has no body ceiling and passes false. +inline constexpr bool explicitCeilingTighterThanDefault( std::size_t budgetTokens, bool bodyCeiling ) noexcept { - return budgetTokens != 0 && budgetBytesForTokens( budgetTokens ) < kForPayloadBudgetBytes; + return bodyCeiling || ( budgetTokens != 0 && budgetBytesForTokens( budgetTokens ) < kForPayloadBudgetBytes ); } // May the code-above-docs reorder (reorderDocsAfterCode) apply? Its `docs_after_code=` attribute and reading are UNCHARGED -// (a disclosure never costs a row), so under a tight explicit ceiling — where the answer promises est_tokens <= the budget +// (a disclosure never costs a row), so under a tight explicit ceiling — where the answer promises est_tokens <= the ceiling // and every row — neither the reorder nor its note applies: the answer is the one it was before the reorder existed. At or // above the default share, or with no explicit ceiling, it applies. Independent of RIPWIRE_FOR_ENDLINES (that switch moves e= only). -inline constexpr bool docsAfterCodeFitsCeiling( std::size_t budgetTokens ) noexcept +inline constexpr bool docsAfterCodeFitsCeiling( std::size_t budgetTokens, bool bodyCeiling ) noexcept { - return !explicitCeilingTighterThanDefault( budgetTokens ); + return !explicitCeilingTighterThanDefault( budgetTokens, bodyCeiling ); } inline constexpr bool endLinesFitCeilingFor( ForEndLinesMode mode, std::size_t budgetTokens, bool bodyCeiling ) noexcept { @@ -4422,10 +4424,11 @@ inline constexpr bool endLinesFitCeilingFor( ForEndLinesMode mode, std::size_t b case ForEndLinesMode::Never: return false; case ForEndLinesMode::Auto: break; } - return !bodyCeiling && !explicitCeilingTighterThanDefault( budgetTokens ); + return !explicitCeilingTighterThanDefault( budgetTokens, bodyCeiling ); } static_assert( endLinesFitCeilingFor( ForEndLinesMode::Auto, 0, false ) && !endLinesFitCeilingFor( ForEndLinesMode::Auto, 0, true ) ); -static_assert( docsAfterCodeFitsCeiling( 0 ) && !docsAfterCodeFitsCeiling( 1200 ) && docsAfterCodeFitsCeiling( 8000 ) ); +static_assert( docsAfterCodeFitsCeiling( 0, false ) && !docsAfterCodeFitsCeiling( 1200, false ) && docsAfterCodeFitsCeiling( 8000, false ) ); +static_assert( !docsAfterCodeFitsCeiling( 0, true ) && !docsAfterCodeFitsCeiling( 8000, true ) ); // a body ceiling is tight at any (or no) token budget static_assert( endLinesFitCeilingFor( ForEndLinesMode::Always, 1, true ) && !endLinesFitCeilingFor( ForEndLinesMode::Never, 0, false ) ); inline bool endLinesFitCeiling( std::size_t budgetTokens, bool bodyCeiling ) { diff --git a/src/verbs_for.h b/src/verbs_for.h index 026f8e2fd..2f0b1583b 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2295,9 +2295,10 @@ std::optional runForLens( const MainDispatch& d ) LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning std::vector lensRank = std::move( lr.rank ); + const bool forBodyCeiling = cfg.maxTokens > 0 && cfg.detail > 0; // the kShapingVerbs carve-out, read once (e= and the docs reorder share it via serialize.h explicitCeilingTighterThanDefault) // code above docs (filter.h): the routed path on a question that does not ask about docs — the shown set's REORDER const bool forDocsAfterCode = !cfg.noRoute && !taskAsksAboutDocs( cfg.forTask ) && !std::getenv( "RIPWIRE_NO_DOCS_AFTER_CODE" ) - && rw::docsAfterCodeFitsCeiling( cfg.tokenBudget > 0 ? std::size_t( cfg.tokenBudget ) : 0u ); // not under a tight explicit ceiling (uncharged note) + && rw::docsAfterCodeFitsCeiling( cfg.tokenBudget > 0 ? std::size_t( cfg.tokenBudget ) : 0u, forBodyCeiling ); // not under a tight explicit ceiling (uncharged note) const std::string routeNoteRaw = std::move( lr.routeNote ); // verbatim; lands ONLY in route= (attribute-escaped) + the JSON twin — L1: the comment no longer echoes it const std::string mentionNote( std::move( lr.mentionNote ) ); const std::string boostNote( std::move( lr.boostNote ) ); @@ -2510,7 +2511,6 @@ std::optional runForLens( const MainDispatch& d ) // header has a MEASURED hard byte constraint (fornotesbudgetcheck), where 150 bytes of prose is // 2% of a 1550-token ceiling. Gate: test/budgetpolicycheck.sh arm (B). const bool forGateBudget = cfg.tokenBudget > 0; - const bool forBodyCeiling = cfg.maxTokens > 0 && cfg.detail > 0; // the kShapingVerbs carve-out, read once const bool forEndLines = rw::endLinesFitCeiling( forGateBudget ? std::size_t( cfg.tokenBudget ) : 0u, forBodyCeiling ); // e= rides? if( forGateBudget ) { diff --git a/test/forcompletefix/pydocs/docs/cache.md b/test/forcompletefix/pydocs/docs/cache.md new file mode 100644 index 000000000..4fdef4767 --- /dev/null +++ b/test/forcompletefix/pydocs/docs/cache.md @@ -0,0 +1,5 @@ +# Cache + +## How the message cache resolves entries + +The resolver delivers cached message entries; a miss posts a message to the pump. diff --git a/test/forcompletefix/pydocs/docs/pump.md b/test/forcompletefix/pydocs/docs/pump.md new file mode 100644 index 000000000..7ab09d293 --- /dev/null +++ b/test/forcompletefix/pydocs/docs/pump.md @@ -0,0 +1,15 @@ +# Message pump + +## How a message is delivered + +The message pump drains pending messages and delivers each message to its handler. +`MessagePump.post` queues a message; `run_forever` drains the pending queue. + +## Delivering a message to a handler + +`deliver` looks up the `on_` handler for each message and calls it. +A message without a handler is dropped. + +## Draining pending messages + +`_drain` pops messages in arrival order until the pending queue is empty. diff --git a/test/forcompletefix/pydocs/src/pump/__init__.py b/test/forcompletefix/pydocs/src/pump/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/forcompletefix/pydocs/src/pump/app.py b/test/forcompletefix/pydocs/src/pump/app.py new file mode 100644 index 000000000..c9e32aa93 --- /dev/null +++ b/test/forcompletefix/pydocs/src/pump/app.py @@ -0,0 +1,16 @@ +from pump.loop import MessagePump +from pump.resolver import Resolver + + +class App(MessagePump): + def __init__(self): + super().__init__() + self.resolver = Resolver() + + def on_lookup(self, message): + return self.resolver.resolve(message.name) + + +def main(): + app = App() + app.run_forever() diff --git a/test/forcompletefix/pydocs/src/pump/cache.py b/test/forcompletefix/pydocs/src/pump/cache.py new file mode 100644 index 000000000..0bf39eada --- /dev/null +++ b/test/forcompletefix/pydocs/src/pump/cache.py @@ -0,0 +1,20 @@ +class LookupCache: + def __init__(self): + self._slots = {} + + def fetch_entry(self, key): + """Return the cached entry for key, computing and storing it on a miss.""" + if key not in self._slots: + self._slots[key] = self._compute(key) + return self._slots[key] + + @staticmethod + def _compute(key): + return key.upper() + + def evict_entries( + self, + keys, + ): + for key in keys: + self._slots.pop(key, None) diff --git a/test/forcompletefix/pydocs/src/pump/loop.py b/test/forcompletefix/pydocs/src/pump/loop.py new file mode 100644 index 000000000..161554cc3 --- /dev/null +++ b/test/forcompletefix/pydocs/src/pump/loop.py @@ -0,0 +1,21 @@ +class MessagePump: + def __init__(self): + self._pending = [] + self._running = True + + def post(self, message): + self._pending.append(message) + + def run_forever(self): + while self._running: + self._drain() + + def _drain(self): + while self._pending: + message = self._pending.pop(0) + self.deliver(message) + + def deliver(self, message): + handler = getattr(self, "on_" + message.kind, None) + if handler is not None: + handler(message) diff --git a/test/forcompletefix/pydocs/src/pump/resolver.py b/test/forcompletefix/pydocs/src/pump/resolver.py new file mode 100644 index 000000000..4a3191548 --- /dev/null +++ b/test/forcompletefix/pydocs/src/pump/resolver.py @@ -0,0 +1,15 @@ +from pump.cache import LookupCache + + +class Resolver: + def __init__(self): + self.cache = LookupCache() + + def resolve(self, name): + return self.cache.fetch_entry(name) + + +class Downloader: + def pull(self, source, url): + # `source` is an untyped parameter: nothing proves it is a LookupCache. + return source.fetch_entry(url) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 36e63687b..68e06dbed 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -60,6 +60,9 @@ # MCP est_tokens is the twin's (already above 1200 on base; not this rule's to fix). Near misses: no ceiling and # --token-budget=8000 still reorder and carry docs_after_code= with its reading, XML, JSON and MCP; a question that # names docs keeps score order at 1200 too. +# A BODY ceiling (--max-tokens=N --detail=K, XML only: --json refuses the pair, MCP `for` has no max_tokens) is the third tight +# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1600 and the Python+docs fixture (pydocs) at 1550 +# (RED at f4253d45: est 1634 > 1600 / 1622 > 1550, over_ceiling="1"); near miss --detail alone still reorders. # (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). # (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. # (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's @@ -449,6 +452,29 @@ for b in 700 1200; do tcheck "T/mcp budget_tokens=$b: byte-equal to the plain-order twin, no docs_after_code= (est_tokens not gated: base MCP is already over)" tight "$TMP/t$b.on.mcp.json" "$TMP/t$b.off.mcp.json" fi done +# (T) body ceiling (--max-tokens=N --detail=K): the third tight explicit ceiling (B1' of the delta review of f4253d45). +# est_tokens must stay <= N exactly as it did before the reorder: the answer is byte-equal to the plain-order twin. Both fixtures +# sit in the window where the uncharged note alone pushed a within-ceiling answer over (RED at f4253d45: ts 1534 -> 1634 with +# over_ceiling="1" at 1600; pydocs 1523 -> 1622 at 1550). JSON refuses the pair and MCP `for` has no body-ceiling input. +QP='How is a message delivered to its handler?' +for spec in "ts|$Q1|1600" "pydocs|$QP|1550"; do + r="${spec%%|*}"; rest="${spec#*|}"; q="${rest%|*}"; mt="${rest##*|}" + ON="$( onr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )"; OFF="$( offr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )" + if ran_ok "$ON" "T/body/$r" && ran_ok "$OFF" "T/body/$r (twin)"; then + tcheck "T/body $r --max-tokens=$mt --detail=1: byte-equal to the plain-order twin, no docs_after_code=, est_tokens <= $mt" tight "$ON" "$OFF" "$mt" + fi + # near miss: --detail alone is not a ceiling (no --max-tokens), so the reorder and its note still apply + ON="$( onr "$r" "bd" "$q" --detail=1 )"; OFF="$( offr "$r" "bd" "$q" --detail=1 )" + if ran_ok "$ON" "T/body/near/$r" && ran_ok "$OFF" "T/body/near/$r (twin)"; then + tcheck "T/body/near $r --detail=1 without --max-tokens: the reorder and its note still apply" reorders "$ON" "$OFF" + fi +done +# --json refuses the pair (the arm above is XML-only), and MCP `for` takes no max_tokens: if either ever accepts it, this arm must be +# replaced by a byte-equal one. +( cd "$CORPUS/ts" && "$BIN" . --no-cache "--for=$Q1" --max-tokens=1600 --detail=1 --json >"$TMP/bcj.out" 2>"$TMP/bcj.err" ); jrc=$? +if [ "$jrc" -ne 0 ] && grep -q 'two output SHAPES' "$TMP/bcj.err"; then ok "T/body --json: refuses --max-tokens with --detail (no JSON body-ceiling answer to gate)"; else no "T/body --json: expected the two-shapes refusal, rc=$jrc"; fi +mcpfor "$TMP/bcm.mcp.json" 0 "$Q1" ',"max_tokens":1600,"detail":1' +if grep -q "unknown field: 'max_tokens'" "$TMP/bcm.mcp.json"; then ok "T/body mcp: for refuses max_tokens (no MCP body-ceiling input to gate)"; else no "T/body mcp: for no longer refuses max_tokens — add a byte-equal arm"; fi # near misses: the SAME question with no explicit ceiling, and a ceiling at/above the default share, still reorder for b in 0 8000; do for spec in "xml|" "json|--json"; do From 3644a7689fa3a284baeee5014bebf40d38ff1d29 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 01:32:36 -0400 Subject: [PATCH 170/229] defect-shape: what the full CI suite found outside the quality-delta gates - skilldescbudgetcheck: the quality-bar trigger text carries the format-arity exception and still fits the 5,400-character set ceiling (the rest of the sentence is tighter; the routing gates stay green). - selfcheckcheck: defectSiteValue's ENSURES reads a plain bool, not a function call. - jsonwalkcheck: the C++ lexer's directive skip is not a second copy of jsonesc.h's escape walk. - hazardpatterncheck: the cut-length comparison parses with std::from_chars, not std::stol. - fixedbufsweep: +5 lines that name rw::formatTo without calling it (351 -> 356 mentions; no call, site or row). - showcasecapturecheck (I): the capture's --legend-dict head line and size marker are re-read from this binary (dictv=6360eea4a2deea49, 785 entries; the 30 displayed lines are otherwise byte-identical); COMMANDS.md regenerated; CHANGELOG's dictionary figure follows. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- docs/COMMANDS.md | 2 +- docs/captures/COMMANDS_showcase_2026-09-14.md | 4 ++-- skills/ripwire-quality-bar/SKILL.md | 8 +++---- src/defectshape.h | 21 ++++++++++++------- src/quality.h | 5 +++-- test/fixedbufsweep.sh | 5 ++++- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d8427de..099c8bcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1304,7 +1304,7 @@ shadow and the value-reference slot text take 143, above 141's full-use file tag binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 17 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions held as values). Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=04d7833c60d2d5bd entries=784`. +cached quality snapshot is recomputed. The session legend dictionary is `dictv=6360eea4a2deea49 entries=785` (the defect-shape entry `r defect=` is the 785th). ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index d5ce3c1eb..7b3a218fb 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -4398,7 +4398,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=6360eea4a2deea49 entries=785 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md index 65e566861..3052e71c0 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-14.md +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -5351,7 +5351,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=04d7833c60d2d5bd entries=784 +ripwire legend dictionary ripwire.dict/v1 dictv=6360eea4a2deea49 entries=785 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5381,7 +5381,7 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [755 more display lines; full output is 77894 bytes on 785 raw line(s)] +… [756 more display lines; full output is 78014 bytes on 786 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` diff --git a/skills/ripwire-quality-bar/SKILL.md b/skills/ripwire-quality-bar/SKILL.md index d11635eb0..7eb4ed4e1 100644 --- a/skills/ripwire-quality-bar/SKILL.md +++ b/skills/ripwire-quality-bar/SKILL.md @@ -1,10 +1,10 @@ --- name: ripwire-quality-bar description: > - Code QUALITY of what YOU just wrote, before you commit or say 'done', or verifying a cleanup: - --quality-delta lists what got WORSE in 12 kinds and exits 2 only when pre-existing code got - materially worse (new-symbol rows never gate, except defect-shape format-arity); which restructuring a measured shape (humps/deep, a tangle) calls for. Merge - safety → change-check. Even a single-line leaf fix runs it. + Code QUALITY of what YOU just wrote, before commit or 'done', or a cleanup: --quality-delta + lists what got WORSE in 12 kinds, exit 2 if old code got materially worse (new-symbol rows + never gate, except defect-shape format-arity); which fix a measured shape (humps/deep, tangle) + calls for. Merge safety → change-check. Even one-line fixes. allowed-tools: Bash, Read --- diff --git a/src/defectshape.h b/src/defectshape.h index 9a644bc38..3fc5c75d4 100644 --- a/src/defectshape.h +++ b/src/defectshape.h @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include @@ -212,9 +214,10 @@ inline std::vector lexCpp( std::string_view s ) } if( c == '#' && lineStart ) { - while( i < s.size() && s[i] != '\n' ) + // the directive runs to the first newline a backslash does not continue + while( i < s.size() && !( s[i] == '\n' && ( i == 0 || s[i - 1] != '\\' ) ) ) { - i += ( s[i] == '\\' && i + 1 < s.size() ) ? 2 : 1; + ++i; } continue; } @@ -1061,13 +1064,17 @@ inline bool lengthIsSizeBound( std::string_view src, const std::vector& tok { return true; } - if( m.kind != TokKind::Number || n.empty() || !std::all_of( n.begin(), n.end(), isDigit ) || n.size() > 9 - || !std::all_of( mt.begin(), mt.end(), isDigit ) || mt.size() > 9 ) + long nv = 0; + long mv = 0; + const auto plain = []( std::string_view d, long& out ) + { + const auto r = std::from_chars( d.data(), d.data() + d.size(), out ); + return !d.empty() && d.size() <= 9 && r.ec == std::errc() && r.ptr == d.data() + d.size(); + }; + if( m.kind != TokKind::Number || !plain( n, nv ) || !plain( mt, mv ) ) { - return false; + return false; // not two plain decimal lengths } - const long nv = std::stol( n ); - const long mv = std::stol( std::string( mt ) ); return mv >= nv && mv - nv <= 3; }; if( k + 6 < to && isCompareOp( src, toks[k + 5] ) && boundMatches( toks[k + 6] ) ) diff --git a/src/quality.h b/src/quality.h index 03401b101..273f836c1 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1024,8 +1024,9 @@ inline std::uint64_t defectSiteValue( defectshape::Facet f, std::string_view tex { h = 4; } - const std::uint64_t v = h | static_cast( f ); - ENSURES( v != kDefectFilePresent && v != kDefectFileUnreadable && defectFacetOf( v ) == f, "a site value is never a reserved file record and carries its facet" ); + const std::uint64_t v = h | static_cast( f ); + const bool keepsFacet = ( v & 3u ) == static_cast( f ); + ENSURES( v != kDefectFilePresent && v != kDefectFileUnreadable && keepsFacet, "a site value is never a reserved file record and carries its facet" ); return v; } diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index ddf94a920..b56ba2e96 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -594,7 +594,10 @@ if not bad: # 238 -> 239 calls/sites, 104 -> 105 rows). noteCacheReject's new `char detail[192]` names both version # numbers on a refused cache blob; one formatTo of a literal and two std::uint32_t, rowed 'not-markup' # (a stderr notice). Re-derived on the train 20 merged tree; no other train lane moves the population. -EXPECTED = { "mentions": 351, "calls": 241, "sites": 241, "rows": 105, "widthforms": 0 } +# 2026-10-08 (lane/cr-qd-kinds-068, the defect-shape kind): +5 mentions, no call/site/row (351 -> 356). Five +# lines NAME rw::formatTo without calling it: the format-arity family table and two comments in +# src/defectshape.h, the defect-shape legend sentence (verbs_quality.h) and the --help paragraph (cli.h). +EXPECTED = { "mentions": 356, "calls": 241, "sites": 241, "rows": 105, "widthforms": 0 } # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the From 639165805016ca61f877b45756101f9e40b2c3b6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 01:38:51 -0400 Subject: [PATCH 171/229] test: forsigspancheck (T) body ceiling: pydocs ceiling 1660 (the committed fixture's window) The fixture's est_tokens moves once it is a committed tree, so the body-ceiling arm's window is re-picked on the committed fixture: twin 1626 (inside 1660), head-before-fix 1725 (over_ceiling). Co-Authored-By: Claude Sonnet 5.5 --- test/forsigspancheck.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/forsigspancheck.sh b/test/forsigspancheck.sh index 68e06dbed..7fe51c441 100755 --- a/test/forsigspancheck.sh +++ b/test/forsigspancheck.sh @@ -61,8 +61,8 @@ # --token-budget=8000 still reorder and carry docs_after_code= with its reading, XML, JSON and MCP; a question that # names docs keeps score order at 1200 too. # A BODY ceiling (--max-tokens=N --detail=K, XML only: --json refuses the pair, MCP `for` has no max_tokens) is the third tight -# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1600 and the Python+docs fixture (pydocs) at 1550 -# (RED at f4253d45: est 1634 > 1600 / 1622 > 1550, over_ceiling="1"); near miss --detail alone still reorders. +# ceiling: the same twin equality, est_tokens <= N, on the TS fixture at 1600 and the Python+docs fixture (pydocs) at 1660 +# (RED at f4253d45: est 1634 > 1600 / 1725 > 1660, over_ceiling="1"); near miss --detail alone still reorders. # (J) dialect parity: --for --json "sigs" entries carry "e" equal to the XML rows' e= (and none where XML has none). # (M) the MCP `for` twin: the same e= on the C rows, none on the extent_suspect rows, and a legend clause for e=. # (L) legend: --legend=full defines e= and says what it does NOT mean (absent = unknown, not 0; l= is the name's @@ -455,9 +455,9 @@ done # (T) body ceiling (--max-tokens=N --detail=K): the third tight explicit ceiling (B1' of the delta review of f4253d45). # est_tokens must stay <= N exactly as it did before the reorder: the answer is byte-equal to the plain-order twin. Both fixtures # sit in the window where the uncharged note alone pushed a within-ceiling answer over (RED at f4253d45: ts 1534 -> 1634 with -# over_ceiling="1" at 1600; pydocs 1523 -> 1622 at 1550). JSON refuses the pair and MCP `for` has no body-ceiling input. +# over_ceiling="1" at 1600; pydocs 1626 -> 1725 at 1660). JSON refuses the pair and MCP `for` has no body-ceiling input. QP='How is a message delivered to its handler?' -for spec in "ts|$Q1|1600" "pydocs|$QP|1550"; do +for spec in "ts|$Q1|1600" "pydocs|$QP|1660"; do r="${spec%%|*}"; rest="${spec#*|}"; q="${rest%|*}"; mt="${rest##*|}" ON="$( onr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )"; OFF="$( offr "$r" "bc$mt" "$q" "--max-tokens=$mt" --detail=1 )" if ran_ok "$ON" "T/body/$r" && ran_ok "$OFF" "T/body/$r (twin)"; then From f890d6a46dbce87951bbb7ec4e76bff1b9fffecc Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 01:56:42 -0400 Subject: [PATCH 172/229] test(partitioncheck): pin P10's corpus to a fixed commit P10 compares the partitioned document's prose legend bytes against 1.3x the single bundle's. Both legends carry data clauses besides their fixed prose (the single bundle's doc-mentions bracket and its kept/far tail, the partitioned document's conditional truncation definition), so the ratio's denominator depends on whatever source the bundle ranks. The arm read the live checkout, which every branch edits: with one binary on one day the main tree measures 2443 vs 1916 B (pass) while a branch that edits 14 src/ headers, none of them partition code, measures 2443 vs 1872 B (fail). The clock plays no part: the same results hold with the wall clock moved a day or a year either way. The arm now measures a git archive of the tree at 60dd3b3f (third_party/ and bench/ excluded; byte-identical at the pin), run from inside the corpus with a relative root. The 1.3x bar is unchanged. A clone without the pinned commit fails the arm by name instead of measuring the live tree. Co-Authored-By: Claude Opus 5.5 --- test/partitioncheck.sh | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/test/partitioncheck.sh b/test/partitioncheck.sh index bcd65e356..2cf741ada 100755 --- a/test/partitioncheck.sh +++ b/test/partitioncheck.sh @@ -293,8 +293,28 @@ fi # legend DEFINES the partition envelope ( counts, each 's role/symbols/bytes/tokens=bytes/2.36, the # inner ctx attributes) that a single bundle does not carry, so its ratio measures those definitions, not repetition; the # default's own contract — ONE legend, none per slice — is asserted right after this arm. -"$BIN" "$ROOT" --pack-task="$TASK" --partition=3 --no-cache --legend=full >"$TMP/p10.part" 2>/dev/null -"$BIN" "$ROOT" --pack-task="$TASK" --no-cache --legend=full >"$TMP/p10.single" 2>/dev/null +# CORPUS PINNED 2026-10-08: the ratio is measured on this repo's tree AS OF P10_CORPUS_REV, not on the live checkout. +# Both legends carry DATA clauses besides their fixed prose — the single bundle's "[doc mentions: …]" and its +# "| bodies: kept X of Y | far: …" tail, the partitioned document's conditional 146 B b truncated= definition — so the +# DENOMINATOR is a function of whatever source the bundle ranks. Measured with ONE binary (cd87e30e) on the same day: +# the 60dd3b3f tree gives 2443 vs 1916 B (PASS); a lane tree that edits 14 src/ headers, none of them partition.h or a +# legend this arm reads, gives 2443 vs 1872 B (FAIL, 1.3x = 2433.6); src/ alone 2443 vs 1805; src/+docs/ 2297 vs 1772. Every lane edits the corpus this arm read, so the +# arm went red on lanes that never touched partition output. Pinned, the corpus moves only when this line does, and a +# change in either legend's prose — the thing the 1.3x bar is for — is the only thing left that can move the ratio. +# third_party/ and bench/ are left out of the archive: byte-identical result at the pin (2443/1916), 25 MB vs 107 MB. +# Run from INSIDE the corpus with a "." root, as the ceiling arms above do, so the checkout's path depth cannot enter. +# CI checks out full history (fetch-depth: 0), so the pinned commit is present on every lane; a shallow clone FAILs +# this arm by name rather than silently measuring the live tree. +P10_CORPUS_REV="60dd3b3f2246ce0284cfa4d4ed48694eafa69c2b" +P10_CORPUS="$TMP/p10corpus"; mkdir -p "$P10_CORPUS" +if git -C "$ROOT" cat-file -e "$P10_CORPUS_REV^{commit}" 2>/dev/null \ + && git -C "$ROOT" archive "$P10_CORPUS_REV" -- . ':(exclude)third_party' ':(exclude)bench' | tar -x -C "$P10_CORPUS"; then + ( cd "$P10_CORPUS" && "$BIN" . --pack-task="$TASK" --partition=3 --no-cache --legend=full ) >"$TMP/p10.part" 2>/dev/null + ( cd "$P10_CORPUS" && "$BIN" . --pack-task="$TASK" --no-cache --legend=full ) >"$TMP/p10.single" 2>/dev/null +else + no "P10: pinned corpus commit $P10_CORPUS_REV is not in this clone (shallow, or no .git) — run from a full clone" + : >"$TMP/p10.part"; : >"$TMP/p10.single" +fi read -r P10_PART P10_SINGLE P10_INNER < Date: Thu, 8 Oct 2026 02:00:22 -0400 Subject: [PATCH 173/229] fix(for): pay for a next= in rows only when that is what makes the answer fit (C3 completed) Orchestrator ruling (completes C3, 2026-10-08): drop a row to pay for next= ONLY when dropping it is what makes the answer fit. CLI, --json and MCP alike: the unpaid answer (every row the cut leaves + the handle) is rendered first and served when it fits; only when it lands past its ceiling is the paid answer rendered, and it is served only when IT fits; otherwise the unpaid answer ships with over_ceiling="1" + the unpaid clause. The served answer never has fewer rows than an alternative that also fits. The CLI ranking (computeLensRanking) now runs once in runForLens and every payment-mode render shares it, so no render repeats the ranking (round 1 re-ranked on its unpaid retry). MCP keeps the unpaid assembly and re-uses it when the paid one does not fit (at most two assemblies). Co-Authored-By: Claude Opus 5.5 --- src/mcpverbs.h | 28 +++++++---- src/serialize.h | 11 +++-- src/verbs_for.h | 127 +++++++++++++++++++++++++++++++----------------- 3 files changed, 108 insertions(+), 58 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 9907a64bb..d4544c3e2 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1804,7 +1804,7 @@ inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) // knob-honesty-068 (orchestrator ruling C3, 2026-10-07): MCP `for`'s ASSEMBLY — everything from the sigs render to the // finished, UNPRICED document (est_tokens= / over_ceiling= are spliced once, on the chosen one, by forTaskText) — as one pass -// forTaskText runs once or twice over a single ranking (paid, then unpaid when the paid answer still lands over). The pass +// forTaskText runs up to twice over a single ranking (unpaid; paid only when the unpaid answer lands over — ruling C3). The pass // takes its own copies of what it edits (the header, the lego/compose strings, the lego scope and its pre-cap count), so a // second pass starts from the same state as the first. A free function, not a lambda: forTaskText is already the largest // function in this file, and the assembly is a step of its own. @@ -2467,11 +2467,12 @@ inline std::optional forTaskText( const std::string& root, const st const McpForAssemblyInputs assemblyIn{ ing, lensRank, forTopN, fanIn, impure, redact, sigsBudget, fixedBytes, budgetTokens, noteIndex, notesPtr, flRootArg, task, mcpTopRowNext, mcpForHdrRows, routeStr, composePreCapCount, sections }; - // C3 — pay for the handle in rows ONLY where that makes the answer fit (the CLI twin's rule, runForLens). The PAID pass runs - // first, so the payable case is the answer it always was; when that answer still lands past budget_tokens — and this surface - // overshoots on its own (the header bytes its ledger exempts), so a capped answer under a budget nearly always does — the - // rows it dropped bought nothing, and the UNPAID pass is served instead: the rows the cut alone leaves, the handle riding - // unpaid, over_ceiling="1" (priceForTaskRoot). Both passes share the one ranking above; only the assembly re-runs. + // C3 (completed 2026-10-08) — drop a row to pay for the handle ONLY where dropping it is what makes the answer fit (the CLI + // twin's rule, runForLens). The UNPAID pass runs first: every row the cut leaves + the handle; when it fits (or nothing is + // capped under a budget) it is served and nothing is paid. Only when it lands past budget_tokens does the PAID pass run, and + // it is served only when IT fits — paying is what made it fit. Otherwise (this surface overshoots on its own: the header + // bytes its ledger exempts) the unpaid answer is served: the cut's rows, the handle riding unpaid, over_ceiling="1" + // (priceForTaskRoot). Both passes share the one ranking above; only the assembly re-runs. const auto estOf = []( std::size_t docBytes ) { std::size_t est = 0; @@ -2479,22 +2480,31 @@ inline std::optional forTaskText( const std::string& root, const st return est; }; const std::optional tallyAtEntry = redact != nullptr ? std::optional( *redact ) : std::nullopt; - McpForAssembly chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + McpForAssembly chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); if( !chosen.doc ) { return std::nullopt; } if( budgetTokens > 0 && chosen.cut.hasContinuation && estOf( chosen.doc->size() ) > budgetTokens ) { + const std::optional tallyUnpaid = redact != nullptr ? std::optional( *redact ) : std::nullopt; if( tallyAtEntry ) { *redact = *tallyAtEntry; // the summary counts the rows of the ONE answer served } - chosen = assembleMcpFor( assemblyIn, /*payFromRows=*/false, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); - if( !chosen.doc ) + McpForAssembly paid = assembleMcpFor( assemblyIn, /*payFromRows=*/true, headerStr, legoStr, composeStr, legoScoped, legoPreCapCount ); + if( !paid.doc ) { return std::nullopt; } + if( estOf( paid.doc->size() ) <= budgetTokens ) + { + chosen = std::move( paid ); + } + else if( tallyUnpaid ) + { + *redact = *tallyUnpaid; // the unpaid answer is served: its tally + } } std::string out = std::move( *chosen.doc ); priceForTaskRoot( out, budgetTokens ); // R1: est_tokens=, and over_ceiling="1" when it exceeds budgetTokens diff --git a/src/serialize.h b/src/serialize.h index 60c7a62f4..9e8e1cc31 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -4531,9 +4531,10 @@ struct SigsCutReport // one fitted (compactlegendcheck P4), and the rungs cannot shed a clause that is the cut's only disclosure. The e= // ruling's shape: under a tight ceiling the extras go, the one thing that must stay (here the call) stays and is paid // for. MCP keeps its clause (next_budget_tokens= has no other reading). -// …PAID ONLY WHERE PAYING MAKES THE ANSWER FIT (orchestrator ruling C3, 2026-10-07, CLI and MCP alike): an answer that -// lands past its ceiling paid or not loses rows for nothing, so its caller re-renders with payFromRows=false — the rows -// the cut alone leaves, the whole handle unpaid, over_ceiling="1" on the root (kForSigsUnpaidOverCeilingNote). +// …PAID ONLY WHERE PAYING IS WHAT MAKES THE ANSWER FIT (orchestrator ruling C3, 2026-10-07/08, CLI and MCP alike): the +// caller renders with payFromRows=false first (the rows the cut alone leaves, the whole handle unpaid) and serves that +// when it fits; it pays only when that answer lands past its ceiling and the paid one does not. Past it either way, the +// unpaid answer ships with over_ceiling="1" on the root (kForSigsUnpaidOverCeilingNote). // THE RESUME INDEX, next_offset= (present when rows were DROPPED, not only shrunk): the candidate index the cut starts at — // the last printed row's 1-based rank in the (score desc, id asc) candidate order, so a pseudo-symbol slot that prints no // row cannot skew it the way a printed-row count would. The root's at= stamps the index it was cut from; a continuation @@ -4748,8 +4749,8 @@ inline std::size_t sigsCutPlanReserve( const SigsCutContinuation* req ) noexcept // up `cost` bytes, so block + continuation never exceeds the block the cut alone left — in either dialect, whatever the plan's // slack (reserving it in the plan let that slack absorb it and the bundle grow). Returns the bytes the rank 1..4 floor could // NOT give up (0 = paid in full); the continuation rides regardless (orchestrator ruling 2026-10-07) and the caller labels it. -// Called only where the caller lets rows pay (sigsCutPaysFromRows): a caller whose answer lands past its ceiling paid or not -// re-renders with payFromRows=false (ruling C3), so no row is dropped for a handle that cannot make the answer fit. +// Called only where the caller lets rows pay (sigsCutPaysFromRows): its callers render that way only after the unpaid answer +// landed past its ceiling, and serve the result only when it fits (ruling C3), so no row is dropped unless that buys the fit. template inline std::size_t paySigsContinuationFromRows( std::vector& entries, std::vector& files, std::size_t& total, std::size_t cost, CostFn entryCost ) diff --git a/src/verbs_for.h b/src/verbs_for.h index 13c933193..e386acc92 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1423,11 +1423,11 @@ inline std::string forLensJsonTailStanza( const rw::FileTail& tail, std::size_t return stanza; } -// knob-honesty-068 (orchestrator ruling C3, 2026-10-07): one RENDER of the --json bundle. `sigsNextPayFromRows` is the charged -// "sigs_next" payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextRetryUnpaid` set, a paid render whose -// bundle still lands past its ceiling writes NOTHING and asks for the unpaid one (emitForLensJson below). +// knob-honesty-068 (orchestrator ruling C3, completed 2026-10-08): one RENDER of the --json bundle. `sigsNextPayFromRows` is +// the charged "sigs_next" payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextTryOther` set, a render +// whose capped, charged bundle lands past its ceiling writes NOTHING and asks for the other mode (emitForLensJson below). inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const ForLensJsonInputs& in, bool sigsNextPayFromRows, - bool* sigsNextRetryUnpaid ) + bool* sigsNextTryOther ) { using namespace rw; @@ -1667,11 +1667,11 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const overCeiling = ",\"over_ceiling\":true"; } ASSUME( overCeiling.size() == 0 || overCeiling.size() == 20 ); // the 20 the charge above reserved for it - // knob-honesty-068 (ruling C3): rows paid for "sigs_next" and the bundle STILL lands past its ceiling — dropping them bought - // nothing. Write nothing; the caller re-renders unpaid (the cut's own rows, the handle riding unpaid, over_ceiling disclosed). - if( sigsNextRetryUnpaid != nullptr && sigsNextPayFromRows && cutNext.charged && sigsCut.hasContinuation && !overCeiling.empty() ) + // knob-honesty-068 (ruling C3, completed): a capped, charged bundle past its ceiling in THIS payment mode is not served from + // this render. Write nothing; the caller tries the other mode (emitForLensJson: unpaid, then paid, then unpaid served). + if( sigsNextTryOther != nullptr && cutNext.charged && sigsCut.hasContinuation && !overCeiling.empty() ) { - *sigsNextRetryUnpaid = true; + *sigsNextTryOther = true; return 0; } std::fputs( header.c_str(), out ); @@ -1690,23 +1690,39 @@ inline int emitForLensJsonPass( std::FILE* out, const std::string& header, const return 0; } -// The --json bundle (orchestrator ruling C3, 2026-10-07): a charged "sigs_next" is paid for in rows only where that makes the -// bundle fit. The paid render runs first (the payable case is unchanged); when it lands past its ceiling anyway it wrote nothing, -// and the bundle is rendered again with the rows the cut alone leaves and the handle unpaid. The redaction tally is put back -// to its entry value first, so the rows the summary counts are the ones written. Rendering only — the ranking is the caller's. +// The --json bundle (orchestrator ruling C3, completed 2026-10-08): a charged "sigs_next" is paid for in rows ONLY where that is +// what makes the bundle fit — runForLens's three modes over one ranking: unpaid (served when it fits), paid (served when IT +// fits), unpaid again (served over its ceiling, over_ceiling disclosed: no row dropped for a handle that cannot make it fit). +// A render that hands back wrote nothing; the redaction tally is put back to its entry value before each later render, so the +// rows the summary counts are the ones written. Rendering only — the ranking is the caller's. inline int emitForLensJson( std::FILE* out, const std::string& header, const ForLensJsonInputs& in ) { + if( in.tokenBudget == 0 ) // only an explicit --token-budget charges the handle, so only then can a render hand back + { + return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, nullptr ); + } const std::optional tallyAtEntry = in.redact != nullptr ? std::optional( *in.redact ) : std::nullopt; - bool retryUnpaid = false; - const int paidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, &retryUnpaid ); - if( !retryUnpaid ) + const auto restoreTally = [ & ] { - return paidRc; + if( tallyAtEntry ) + { + *in.redact = *tallyAtEntry; + } + }; + bool unpaidOver = false; + const int unpaidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, &unpaidOver ); + if( !unpaidOver ) + { + return unpaidRc; } - if( tallyAtEntry ) + restoreTally(); + bool paidOver = false; + const int paidRc = emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/true, &paidOver ); + if( !paidOver ) { - *in.redact = *tallyAtEntry; + return paidRc; } + restoreTally(); return emitForLensJsonPass( out, header, in, /*sigsNextPayFromRows=*/false, nullptr ); } @@ -2285,10 +2301,11 @@ inline std::string renderForHdrRowsXml( const rw::IngestResult& ing, const std:: return x; } -// knob-honesty-068 (orchestrator ruling C3, 2026-10-07): one RUN of the --for lens. `sigsNextPayFromRows` is the charged -// continuation's payment mode (serialize.h SigsCutContinuation::payFromRows); with `sigsNextRetryUnpaid` set, a paid run whose -// document still lands past its ceiling writes NOTHING to stdout and asks for the unpaid run (runForLens below). -std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRows, bool* sigsNextRetryUnpaid ) +// knob-honesty-068 (orchestrator ruling C3, completed 2026-10-08): one RENDER of the --for lens over the ranking runForLens +// computed once (`lr`). `sigsNextPayFromRows` is the charged continuation's payment mode (serialize.h +// SigsCutContinuation::payFromRows); with `sigsNextTryOther` set, a run whose capped, charged document lands past its ceiling +// writes NOTHING to stdout and asks for the run in the other payment mode (runForLens below decides which answer ships). +std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bool sigsNextPayFromRows, bool* sigsNextTryOther ) { using namespace rw; const Config& cfg = d.cfg; @@ -2332,8 +2349,7 @@ std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRo // ROUTING + anchoring + the B8 mention anchor + the opt-in B3 co-change prior all live in // computeLensRanking (shared with runPackTask so the ranking is defined once). Compose order with // --anchor: ROUTE picks the base lens rank, then ANCHOR expands it; mention/co-change run after. - LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), - /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning + // (the ranking itself — computeLensRanking — runs ONCE, in runForLens: every payment-mode render shares it) std::vector lensRank = std::move( lr.rank ); const std::string routeNoteRaw = std::move( lr.routeNote ); // verbatim; lands ONLY in route= (attribute-escaped) + the JSON twin — L1: the comment no longer echoes it const std::string mentionNote( std::move( lr.mentionNote ) ); @@ -3063,8 +3079,9 @@ std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRo // already trims pays it: a capped compact answer serves the rows the full one does and lands no further over its // ceiling (compactlegendcheck P4). An uncapped answer is untouched. AT THE FLOOR (rank 1..4, nothing left to shed) the // handle still ships and an overshoot is labelled (rootFinish.sigsUnpaidOver below; orchestrator ruling 2026-10-07). - // AND ONLY WHERE PAYING MAKES IT FIT (ruling C3): a paid run that lands past its ceiling anyway is re-run unpaid - // (sigsNextPayFromRows=false: no ledger gap, no payment — the rows the cut alone leaves; runForLens). + // AND ONLY WHERE PAYING IS WHAT MAKES IT FIT (ruling C3, completed 2026-10-08): runForLens renders unpaid first + // (sigsNextPayFromRows=false: no ledger gap, no payment — the rows the cut alone leaves) and pays only when that + // answer lands past its ceiling and the paid one does not. const bool forSigsNextCharged = cfg.tokenBudget > 0 && sigSideCeiling == bundleBudget; const std::string forRankFlags = forRankShapingFlags( cfg ); const rw::SigsCutContinuation forSigsNext{ cfg.forTask, cfg.packTopN, forRankFlags, fixedBytes, forSigsNextCharged, /*json=*/false, @@ -3560,12 +3577,12 @@ std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRo const ForLensPricedHeader asChosen = finishForLensHeaderPriced( headerStr, rootFinish ); const bool landsOver = asChosen.header.size() + emittedNonHeaderBytes > ladderCeiling || asChosen.estTokens > cfg.tokenBudget; - // C3: rows paid for the handle and the answer STILL lands past its ceiling — they bought nothing. Nothing has - // reached stdout yet: hand back and let runForLens re-run unpaid (the rows the cut alone leaves). - if( sigsNextRetryUnpaid != nullptr && forSigsNext.payFromRows && forSigsNextCharged && forSigsCut.hasContinuation - && ( landsOver || rootFinish.lastRungFired ) ) + // C3 (completed): a capped, charged answer that lands past its ceiling in THIS payment mode is not served from this + // run — nothing has reached stdout yet: hand back and let runForLens try the other mode (unpaid → paid: does paying + // make it fit? paid → unpaid: it did not, so no row is dropped for the handle). + if( sigsNextTryOther != nullptr && forSigsNextCharged && forSigsCut.hasContinuation && ( landsOver || rootFinish.lastRungFired ) ) { - *sigsNextRetryUnpaid = true; + *sigsNextTryOther = true; return std::nullopt; } // the handle rode unpaid (the floor could not give its bytes up, or this is the unpaid run) and the document lands @@ -3663,25 +3680,47 @@ std::optional runForLensPass( const MainDispatch& d, bool sigsNextPayFromRo return std::nullopt; } -// The --for lens (orchestrator ruling C3, 2026-10-07: never drop rows to pay for next= when the answer is over its ceiling -// anyway — dropping them does not make it fit). A charged continuation is paid for in rows first, so the payable case is -// the run it always was; a paid run whose finished document still lands past its ceiling wrote nothing (the decision is taken -// before the first byte reaches stdout) and the lens runs again with payFromRows=false: the rows the cut alone leaves, the -// handle unpaid, over_ceiling="1" + kForSigsUnpaidOverCeilingNote. The redaction tally is put back to its entry value first, -// so the summary counts the rows written once. The re-run repeats the ranking: it happens only on a capped, charged answer -// that cannot fit (the tight-ceiling tail), never on the default ceiling. +// The --for lens (orchestrator ruling C3, completed 2026-10-08: drop a row to pay for a charged next= ONLY when dropping +// it is what makes the answer fit). The ranking runs once; the render runs in up to three payment modes over it, each deciding +// on its FINISHED document before the first byte reaches stdout: +// 1. unpaid: every row the cut leaves + the handle. Fits (or nothing is capped and charged): served, no row dropped. +// 2. paid: rows dropped to pay for the handle. Fits: served, because paying is what made it fit. +// 3. unpaid again, served whatever its size: it cannot fit either way, so it keeps the cut's rows, the handle rides unpaid, +// over_ceiling="1" + kForSigsUnpaidOverCeilingNote. +// So the served answer never has fewer rows than an alternative that also fits. The redaction tally is put back to its entry +// value before every render, so the summary counts the rows written once. A render that hands back wrote nothing. std::optional runForLens( const MainDispatch& d ) { + const rw::Config& cfg = d.cfg; + if( cfg.forTask.empty() ) + { + return std::nullopt; + } + rw::LensRanking lr = computeLensRanking( d, cfg.forTask, forCompactPosture( cfg ), + /*fullDistribution=*/!cfg.candidates ); // deep-tail: the bundle serves the file-grain tail; candidates has no tail and keeps the H2 pruning + // only an explicit --token-budget charges the handle (forSigsNextCharged), so only then can a render hand back + if( cfg.tokenBudget == 0 ) + { + return runForLensPass( d, std::move( lr ), /*sigsNextPayFromRows=*/true, nullptr ); + } const rw::RedactCounts tallyAtEntry = d.redactCounts; - bool retryUnpaid = false; - std::optional paidRc = runForLensPass( d, /*sigsNextPayFromRows=*/true, &retryUnpaid ); - if( !retryUnpaid ) + bool unpaidOver = false; + std::optional rc = runForLensPass( d, lr, /*sigsNextPayFromRows=*/false, &unpaidOver ); + if( !unpaidOver ) { - return paidRc; + return rc; + } + ASSUME( !rc ); // the unpaid render that handed back wrote nothing + d.redactCounts = tallyAtEntry; + bool paidOver = false; + rc = runForLensPass( d, lr, /*sigsNextPayFromRows=*/true, &paidOver ); + if( !paidOver ) + { + return rc; } - ASSUME( !paidRc ); // the paid run returned before writing anything + ASSUME( !rc ); // the paid render that handed back wrote nothing d.redactCounts = tallyAtEntry; - return runForLensPass( d, /*sigsNextPayFromRows=*/false, nullptr ); + return runForLensPass( d, std::move( lr ), /*sigsNextPayFromRows=*/false, nullptr ); } std::optional runTargetedViews( const MainDispatch& d ) From c7920353a6d41f95f6ad61193358a10a3b255acf Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:35:12 -0400 Subject: [PATCH 174/229] whereis: the default serves the page that lists more definitions, then the shorter Fix round 2 (delta review D1, orchestrator ruling: completeness first, bytes break ties). Under the shared 60-row cap the listing=all page can list FEWER definitions than the defs page (HEAD's references fill the cap before the branch definitions arrive), and the bytes-only default then served the page with fewer definitions: --whereis=rankGraphTeleport served 1 definition + 59 references (13,695 B) over 60 definitions (18,430 B). whereisServedListing now counts the definition rows each page SHOWS under maxHits; the defs page is served whenever it shows more, whatever its bytes. Equal definitions fall to the existing rule (strictly shorter as written and compact-delivered; a tie serves all). One code path for the CLI and MCP. Arms: crossrefcheck L15 (capped, longer defs page lists more definitions -> defs; red at 219aaaac) + L15-MCP; L16 (61 definitions on HEAD, both pages show the same 60 -> the shorter all page) + L16-MCP. Surfaces made exact: the full LISTING legend, the compact listing= reading, --help, the MCP listing field, CHANGELOG, the change-check and ripwire-mcp skills, the showcase capture's contrast pair. Re-pins, each attributed in-file or here: - compactlegendcheck ripwire.whereis/v1 pin 1040 -> 1050, measured 1023 -> 1037 (+14 B, the listing= reading). - mcpmanifestcheck ceiling 46,850 -> 46,890, measured 46,846 -> 46,869 (+23 B, the listing field's description; whereis description unchanged at 701 B), with twin (1c) pinning both. - printf_parity.manifest help / help_all: the whereis LISTING and --whereis-listing help text. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 25 ++++++----- skills/ripwire-change-check/SKILL.md | 5 ++- skills/ripwire-mcp/SKILL.md | 4 +- src/cli.h | 13 +++--- src/compactlegend.h | 2 +- src/crossref.h | 46 ++++++++++++++----- src/mcprefusal.h | 2 +- src/mcpverbs.h | 2 +- test/compactlegendcheck.sh | 8 +++- test/crossrefcheck.sh | 66 +++++++++++++++++++++++++++- test/mcpmanifestcheck.sh | 17 ++++++- test/printf_parity.manifest | 4 +- test/showcase_capture.py | 4 +- 13 files changed, 157 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a17ec14..22a9fedd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,13 +22,16 @@ build: 631,732 → 611,785 B for the lossless changes alone (−3.2%), 568,600 B Every gold item the base answers supplied is still supplied (502 of 502), and decoding the lossless spellings gives the base answer back element for element on all 90. -- **`--whereis=SYM` lists the definitions and counts the references, when that answer is shorter.** Every `kind="def"` - row is listed; the `kind="ref"` rows are counted in one `` element whose `next=` lists exactly those rows. The default serves this page only when it - is strictly shorter, in bytes, than the `--whereis-listing=all` page under the same row cap (compared as written and - in the compact legend); otherwise it lists every hit. On a symbol with few references (one to three on a 60-symbol - sample) the count and its legend reading cost more than the rows they replace, and the default never serves a page - that lists fewer rows in more bytes (an explicit `--whereis-listing=defs` is served as asked). +- **`--whereis=SYM` lists the definitions and counts the references, when that answer is more complete or shorter.** Every + `kind="def"` row is listed; the `kind="ref"` rows are counted in one `` element whose `next=` lists exactly those rows. The default serves this page when it + lists MORE definitions than the `--whereis-listing=all` page does under the same row cap (a capped all page can list + fewer: HEAD's references fill the cap before the branch definitions arrive), whatever its bytes. When both pages list + the same definitions, it serves this page only if it is strictly shorter in bytes (compared as written and in the + compact legend), and a tie lists every hit. On a symbol with few references (one to three on a 60-symbol sample) the + count and its legend reading cost more than the rows they replace, so the default never serves a page that lists + fewer definitions, nor, for the same definitions, one that lists fewer rows in more bytes (an explicit + `--whereis-listing=defs` is served as asked). `--whereis-listing=defs|refs|all` (MCP `listing`) picks the rows; `all` is the whole hit list. An answer with no reference row, or no definition row (the mentions are then the answer), lists every hit. `kind="def"` is the parser's label: a definition it does not model (a Ruby `define_method`, a `setattr`, a name bound by assignment) is @@ -44,10 +47,12 @@ the base answer back element for element on all 90. posture (rows first, each definition sent once per session, the core carried by the first ref answer). `--mcp-legend=inline` keeps the previous posture; `legend:"compact"`/`"full"` still keep one answer inline; the CLI and the HTTP transport are unchanged. A ten-call session on one repo: 45,463 → 32,622 B (inline) → 29,679 B. -- **Manifest.** The `tools/list` manifest grows 46,732 → 46,846 B: whereis declares `listing`. +- **Manifest.** The `tools/list` manifest grows 46,732 → 46,869 B: whereis declares `listing`. -Gates: `crossrefcheck` (LEAN L1–L14 — L11–L14 the shorter-page default: one ref, 73 refs, the exact tie and one byte -either side, the MCP twin — and listing=all twins), `completecheck`, `legendrefcheck` (J), `mcptwinclaimscheck` +Gates: `crossrefcheck` (LEAN L1–L16 — L11–L14 the same-definitions bytes rule: one ref, 73 refs, the exact tie and one +byte either side, the MCP twin; L15 and L16 the definitions-first rule: a capped answer whose longer defs page lists more +definitions is served, and one whose pages list the same definitions keeps the shorter all page, each with its MCP twin +— and listing=all twins), `completecheck`, `legendrefcheck` (J), `mcptwinclaimscheck` (A-default), `compactlegendcheck` (re-pinned with the measured bytes; a listing=all twin pin), `mcpmanifestcheck`. diff --git a/skills/ripwire-change-check/SKILL.md b/skills/ripwire-change-check/SKILL.md index fd79e3ffd..23c1de830 100644 --- a/skills/ripwire-change-check/SKILL.md +++ b/skills/ripwire-change-check/SKILL.md @@ -148,8 +148,9 @@ emits a flat `` top-K — identity + score + signatu distinct blob is read once (git is content-addressed), so 30 branches cost about one tree. `kind="def"` on a branch row is a lexical heuristic — branch blobs are raw text, never ingested; for HEAD's parsed answer use `--expand`/`--callers`. By default only the `kind="def"` rows are listed and the mentions are - counted in one `` row (its `next=` lists them) when that page is the shorter one; - otherwise, and with `--whereis-listing=all`, every row is listed. + counted in one `` row (its `next=` lists them) when that page lists MORE definitions than + the all page does under the same row cap (a capped all page can list fewer), or the same ones in strictly fewer + bytes; otherwise, and with `--whereis-listing=all`, every row is listed. A tree scan only finds what some ref *still carries*, so `hits="0"` cannot by itself tell a name this repo never had from one it deleted. Add `--with-history` and a `` row says which: `v="never"`, or `v="removed" commit=… date=… p=…` naming the commit that took it out. One `git log` pass, memoized per diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index 8b2bb2aab..949c48c02 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -31,7 +31,9 @@ minimal joining subgraph over N task symbols — the L4 one-call/B11-parity trio pair `whereis` / `stray_content`, which answer "where does this content live?" across every branch: the question `git cherry` cannot, since it compares commit ancestry and every other verb indexes one worktree. `stray_content` marks a branch `superseded` when the live line re-implemented its work, which is exactly -the case `git cherry` calls unmerged forever) + the 3 span-addressed +the case `git cherry` calls unmerged forever; `whereis` lists the definitions and counts the references +(`listing:"defs"` on the root) when that page lists MORE definitions than the whole list would under the same row +cap, or the same ones in strictly fewer bytes — `listing:"all"` asks for every row) + the 3 span-addressed edit verbs (`replace_symbol_body`, `insert_before_symbol`, `insert_after_symbol`). Only `find_symbol` and `find_referencing_symbols` attach a stable `handle=` instead of the body (fetch it only when you need it, via `fetch_body`); the edit verbs enforce a safety contract (staleness refusal, ambiguity refusal, atomic diff --git a/src/cli.h b/src/cli.h index 4668972af..a30e0c670 100644 --- a/src/cli.h +++ b/src/cli.h @@ -2095,11 +2095,14 @@ inline constexpr char kHelpTail[] = " with the commit, date and file that removed it. Remote-tracking refs are excluded\n" " (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR.\n" " LISTING: by default only the kind=\"def\" rows are listed and the kind=\"ref\" rows\n" - " counted in one element, when that page is strictly shorter\n" - " than the --whereis-listing=all page; otherwise every hit is listed.\n" - " --whereis-listing=WHICH with --whereis: which rows to list, defs, refs or all (default: the shorter page).\n" - " The default lists defs when that page is strictly shorter in bytes than all under the\n" - " same row cap, else all (a tie lists all). defs lists every kind=\"def\" row and COUNTS\n" + " counted in one element, when that page lists MORE definitions\n" + " than the --whereis-listing=all page under the row cap, or the same ones in strictly\n" + " fewer bytes; otherwise every hit is listed.\n" + " --whereis-listing=WHICH with --whereis: which rows to list, defs, refs or all (default: more defs, else shorter).\n" + " The default lists defs when that page lists MORE definitions than all under the same\n" + " row cap (references can fill a capped all page before the branch definitions arrive),\n" + " whatever its bytes; when both list the same definitions, defs only when strictly shorter\n" + " in bytes, else all (a tie lists all). defs lists every kind=\"def\" row and COUNTS\n" " the kind=\"ref\" rows in one element whose next= lists exactly\n" " them (refs). kind=\"def\" is the parser's label: a definition it does not model\n" " (define_method, setattr, a name bound by assignment) is a counted ref. With no ref\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index b2cf60551..2e8602527 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -930,7 +930,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "worktree", "worktree=read|partial|unlisted: the checkout differs from HEAD (at= +dirty); each changed path is read from disk as ref=\"worktree\" rows replacing HEAD's, and on-head=/hits=/head_labels= count them; on-head= then reads the checkout, not HEAD's commit; partial = some changed path unreadable or a directory (nested repo, submodule), its HEAD rows may be stale; unlisted = git could not list the changes; either withholds complete=", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "hits", "more hits=N: rows after this page; page on with offset=next_offset", true, "more", MapHeaderRead::No, {}, "whereis" }, // lean-answers lane (crossref.h WhereisListing / the tip-date hoist): each rides only the answer that carries it. - { "listing", "listing=defs|refs: only those kind= rows listed; under defs counts the kind=ref rows and next= lists them; the window counts listed rows; default: defs only if strictly shorter than all; a def the parser does not model (define_method, setattr, assignment) is a counted ref", false, "whereis", MapHeaderRead::No, {}, "whereis" }, + { "listing", "listing=defs|refs: only those kind= rows listed; under defs counts the kind=ref rows and next= lists them; the window counts listed rows; default: defs if it lists more defs than all, else if shorter; a def the parser does not model (define_method, setattr, assignment) is a counted ref", false, "whereis", MapHeaderRead::No, {}, "whereis" }, { "head_date", "head_date=: a hit without tip= date= has tip= at=, date= this", false, "whereis", MapHeaderRead::No, {}, "whereis" }, // the GREY ZONE of the same sweep: attributes the compact prose named in passing ("in/out, cx/ccx", " groups") but never // DEFINED as name= — legendcoveragecheck's default rows hold the definitional predicate, so each gets its reading here. diff --git a/src/crossref.h b/src/crossref.h index 3e51700fd..adfc12efe 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -152,11 +152,13 @@ constexpr std::size_t kWhereisHits = 60; // list holds no ref row (nothing to elide) or no def row (the mentions ARE the answer then). // Refs the kind="ref" rows only, in the hit list's order: the page. // All every row. -// ShorterOfDefsAll the DEFAULT (no flag, no MCP `listing`): the Defs page when it is STRICTLY shorter in bytes than -// the All page, else the All page (review B1: on a symbol with one or two refs the Defs page pays more for its -// element, listing= and their legend readings than the ref rows it elides, and a lean answer that is -// longer while listing less is strictly worse). Measured, never guessed: whereisServedListing renders both. -// An explicit defs is served as asked. +// ShorterOfDefsAll the DEFAULT (no flag, no MCP `listing`): complete answers first. The Defs page when it SHOWS MORE +// definitions than the All page under the shared row cap (a capped All page can list fewer: HEAD's references fill +// the cap before the branch definitions arrive; review D1), whatever its bytes; when both pages show the same +// definitions, the Defs page only when it is STRICTLY shorter in bytes (review B1: on a symbol with one or two +// refs the Defs page pays more for its element, listing= and their legend readings than the ref rows it +// elides), else the All page. Measured, never guessed: whereisServedListing renders both. An explicit defs is +// served as asked. enum class WhereisListing : std::uint8_t { Defs, Refs, All, ShorterOfDefsAll }; // ── blob facts (the per-sha memo payload) ──────────────────────────────────────────────────────────────── @@ -2899,8 +2901,8 @@ inline bool whereisRowOnHeadCommit( const WhereHit& h, std::string_view headSha, return !head.empty() && !headDate.empty() && std::string_view( h.tip ).substr( 0, 9 ) == head && h.date == headDate; } -// The listing a validated --whereis-listing= / MCP `listing` value names; "" (absent) is the default, the shorter of -// defs and all (WhereisListing::ShorterOfDefsAll). Both surfaces refuse any other value before they get here. +// The listing a validated --whereis-listing= / MCP `listing` value names; "" (absent) is the default, defs when it +// lists more definitions than all, else the shorter of the two (WhereisListing::ShorterOfDefsAll). Both surfaces refuse any other value before they get here. inline WhereisListing whereisListingOf( std::string_view v ) noexcept { return v == "refs" ? WhereisListing::Refs : v == "all" ? WhereisListing::All @@ -3091,15 +3093,17 @@ inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std: "the other end (what this page did not print). Page with limit= and offset=; the more element is " "absent exactly when this page reached the end of the hit list. " // lean-answers lane — the default LISTING and the tip/date hoist, defined where they appear. - "LISTING: by default only the kind=\"def\" rows are listed (listing=\"defs\" on the root) when that page is " - "STRICTLY shorter in bytes than the listing=all page under the same row cap (both compared without limit= or " + "LISTING: by default only the kind=\"def\" rows are listed (listing=\"defs\" on the root) when that page lists " + "MORE definitions than the listing=all page lists under the same row cap (a capped all page can list fewer: " + "references can fill the cap before the branch definitions arrive), whatever its bytes, or, when both pages " + "list the same definitions, is STRICTLY shorter in bytes than the all page (both compared without limit= or " "offset=, as written and in the compact dialect, so every page of one answer lists the same rows); " "otherwise the default lists every hit. Under listing=\"defs\" the " "kind=\"ref\" rows are COUNTED by the trailing refs element: count= is exactly the number of kind=\"ref\" " "rows in the hit list, none of them printed here, and its next= lists exactly those rows (listing=\"refs\", " "the same rows byte for byte that listing=all prints). listing= is absent when every hit is listed: the hit " "list holds no kind=\"ref\" row, or no kind=\"def\" row (then the mentions are the answer), or the defs page " - "would not be shorter, or the whole list was asked for (the whereis-listing flag, value all; its value defs " + "would neither list more definitions nor be shorter, or the whole list was asked for (the whereis-listing flag, value all; its value defs " "lists the definitions whatever the size). kind=\"def\" on a HEAD row is the parser's label, not a proof: a " "definition the parser does not model (a Ruby define_method, a setattr, a name bound by assignment such as an " "alias in a class body) is a kind=\"ref\" row, so under listing=\"defs\" it is among the counted refs. " @@ -3139,7 +3143,9 @@ inline void writeWhereisListedPage( std::FILE* out, const WhereResult& res, std: rw::emitRaw( out, "" ); } -// THE DEFAULT, MEASURED (review B1). The Defs page pays a fixed overhead — listing=, the element +// THE DEFAULT, MEASURED (review B1, then D1). First the definitions: the Defs page is served whenever it SHOWS more +// definitions than the All page under the row cap (a capped All page can list fewer), whatever its bytes. Only when both +// pages show the same definitions do the bytes decide. The Defs page pays a fixed overhead — listing=, the element // and, under the compact legend, their readings — that eliding one or two short ref rows does not repay: on 24 of 58 // sampled symbols the Defs page was LONGER than the All page while listing fewer rows. So both pages are rendered and // Defs is served only when it is STRICTLY shorter both as written (the full legend, whose prose is the same text on @@ -3158,7 +3164,23 @@ inline ListedHits whereisServedListing( const WhereResult& res, std::size_t maxH { return defs; // Defs already degraded to every hit (no ref row, or no def row): nothing to compare } - ListedHits all = listedHits( res, WhereisListing::All ); + ListedHits all = listedHits( res, WhereisListing::All ); + // COMPLETE ANSWERS FIRST (orchestrator ruling 2026-10-08, review D1). Under the shared row cap the All page can list + // FEWER definitions than the Defs page: HEAD's ref rows fill the cap before the branch definitions arrive. Both + // lists are in hit-list order and Defs is the def subsequence, so the definitions the All page shows are a PREFIX + // of the Defs page's: comparing the counts compares the sets. More definitions wins whatever its bytes; only equal + // sets fall to the bytes rule below. + const auto defsShown = [ & ]( const ListedHits& l ) + { + const std::size_t n = std::min( l.rows.size(), maxHits ); + return std::size_t( std::count_if( l.rows.begin(), l.rows.begin() + std::ptrdiff_t( n ), + [ & ]( std::size_t i ) { return res.hits[ i ].isDef; } ) ); + }; + ENSURES( defsShown( defs ) >= defsShown( all ), "the All page's shown definitions are a prefix of the Defs page's" ); + if( defsShown( defs ) > defsShown( all ) ) + { + return defs; + } const Rendered defsPage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, 0, 0, defs ); } ); const Rendered allPage = renderToString( [ & ]( std::FILE* f ) { writeWhereisListedPage( f, res, maxHits, 0, 0, all ); } ); const bool defsShorter = defsPage.ok && allPage.ok && defsPage.text.size() < allPage.text.size() diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 0684d6313..075069dbd 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -338,7 +338,7 @@ inline constexpr McpValueSpec kMcpValueFields[] = { { "var", "a STRING variable name inside the resolved definition (omit it to list the sliceable locals)", "var=\"out\"" }, { "flow", "a STRING flow direction: back, fwd or both (omit it for the flat per-line rows)", "flow=\"back\"" }, // lean-answers lane: whereis's CLOSED listing set, the CLI --whereis-listing= values verbatim. - { "listing", "a STRING: defs (refs counted), refs or all; default: the shorter page", "listing=\"all\"" }, + { "listing", "a STRING: defs (refs counted), refs or all; default: more defs listed, else the shorter page", "listing=\"all\"" }, // lane/t10-mcp-coverage: rank_by's own CLOSED value set, the CLI --rank-by= spelling verbatim (cli.h's // parse arm). churn/churn-decay are valid CLI values that this row still names — the closed-set check // must accept them so the refusal can then say WHY they are unsupported HERE (a named gap, not an diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 565f9a5a0..63660ea5c 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1054,7 +1054,7 @@ inline std::string grepAuxJson( const std::vector& hits, const PageW // was false: mcprefusal.h already registers the field, and the batch surface refuses loudly. // · `in` reaches the batch arm at all. It previously took the defaulted GrepIn::Code with no hatch. // Absent reads as the default, as an OPTIONAL field must; only a PRESENT unknown spelling refuses. -// `listing` on whereis — the CLI --whereis-listing= twin, the same closed set (defs|refs|all; absent = the shorter of defs and all), refused on +// `listing` on whereis — the CLI --whereis-listing= twin, the same closed set (defs|refs|all; absent = the page listing more definitions, else the shorter of defs and all), refused on // any other value through the shared sentence so a typo never reads as the default listing. inline std::string whereisListingFromArg( std::string_view typed, bool isPresent, crossref::WhereisListing& out ) { diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index a39956f19..323e9007a 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -526,6 +526,12 @@ probeFor() # parser does not model (define_method, setattr, assignment) is a counted ref" (+87 B) — +136 B, nothing else moved. # Both ride only a page that carries listing=. The OLD path keeps its own pin: the whereis-listing=all twin after the # universe loop holds the whole-list page's legend at 700 B (measured 692), so it cannot grow inside this headroom. +# RE-PINNED 2026-10-08 (lane/lean-answers-068 fix round 2, review D1), measured on this gate's fixture: ripwire.whereis/v1 +# 1023->1037, pin 1040->1050 (measured + 10 rounded up to 10): the listing= reading "default: defs only if strictly shorter +# than all" becomes "default: defs if it lists more defs than all, else if shorter" (+14 B) because the default now serves +# the defs page whenever it lists MORE definitions than the all page under the row cap, whatever its bytes. Rides only a +# page that carries listing=; nothing else moved. The OLD path keeps its pin: the whereis-listing=all twin holds the +# whole-list page's legend at 700 B (measured 692, unchanged). PIN_TABLE=' ripwire.edit-check/v1 610 595 ripwire.map/v1 910 892 @@ -587,7 +593,7 @@ ripwire.at/v1 180 161 ripwire.from-trace/v1 1300 1281 ripwire.plan-lint/v1 570 551 ripwire.merge-scout/v1 570 558 -ripwire.whereis/v1 1040 1023 +ripwire.whereis/v1 1050 1037 ripwire.community/v1 730 719 ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index 92844cf33..3610da443 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -589,8 +589,10 @@ else fi # ── THE DEFAULT IS THE SHORTER PAGE (fix round 1, review B1; crossref.h whereisServedListing) ──────────────────── -# The default serves the defs page only when it is STRICTLY shorter, in bytes, than the listing=all page (as written -# AND in the compact dialect); otherwise it serves the all page. An explicit --whereis-listing=defs is served as asked, +# When the defs page and the listing=all page show the SAME definitions, the default serves the defs page only when it is +# STRICTLY shorter, in bytes, than the all page (as written AND in the compact dialect); otherwise it serves the all page. +# (Fix round 2, review D1: when the defs page shows MORE definitions under the row cap, the default serves it whatever its +# bytes — the L15/L16 arms below.) An explicit --whereis-listing=defs is served as asked, # so each arm reads the two explicit pages of the same binary and checks which one the default is, byte for byte. nbytes(){ printf '%s' "$1" | wc -c | tr -d ' '; } # (L11) a 1-ref symbol: the defs page is LONGER than the all page, so the default IS the all page (no listing=, no ) @@ -653,6 +655,66 @@ else printf ' SKIP LEAN (L14) MCP twin (no python3)\n' fi +# ── COMPLETE ANSWERS FIRST (fix round 2, review D1; crossref.h whereisServedListing) ─────────────────────────────── +# Under the shared row cap the all page can list FEWER definitions than the defs page (HEAD's references fill the cap +# before the branch definitions arrive). The default serves the page that SHOWS MORE definitions whatever its bytes; only +# when both show the same definitions do the bytes decide. The arms are fixture-built and compare the DEFAULT with the two +# explicit pages of the same binary, byte for byte. +defs_in(){ hits_of def | wc -l | tr -d ' '; } +dw(){ "$BIN" "$1" --whereis="$2" --no-cache "${@:3}" 2>/dev/null; } +# (L15) the cap bites: HEAD defines zqCap once and calls it 70 times; 64 branches sit at the previous commit (same files) +# and HEAD adds one unrelated commit, so every branch row carries tip=/date=. The all page lists HEAD's definition and 59 +# references (60 rows), the defs page 60 definitions, and the defs page is the LONGER one in bytes — the default is defs. +D15="$TMP/cap15"; mkdir -p "$D15"; git -C "$D15" init -q -b main >/dev/null 2>&1; git -C "$D15" config commit.gpgsign false +printf 'int zqCap( int x ) { return x; }\n' >"$D15/def.c" +{ printf 'int useAll( void )\n{\n int s = 0;\n'; i=0; while [ $i -lt 70 ]; do printf ' s += zqCap( %d );\n' $i; i=$((i+1)); done; printf ' return s;\n}\n'; } >"$D15/use.c" +printf '// head\n' >"$D15/other.c" +git -C "$D15" add -A >/dev/null 2>&1; git -C "$D15" commit -qm cap1 >/dev/null 2>&1 +i=0; while [ $i -lt 64 ]; do git -C "$D15" branch "b$i" >/dev/null 2>&1; i=$((i+1)); done +printf '// head, later\n' >"$D15/other.c"; git -C "$D15" add -A >/dev/null 2>&1 +GIT_COMMITTER_DATE="2026-02-02T00:00:00Z" GIT_AUTHOR_DATE="2026-02-02T00:00:00Z" git -C "$D15" commit -qm cap2 >/dev/null 2>&1 +C_D="$( dw "$D15" zqCap )"; C_A="$( dw "$D15" zqCap --whereis-listing=all )"; C_F="$( dw "$D15" zqCap --whereis-listing=defs )" +CD_A="$( printf '%s' "$C_A" | defs_in )"; CD_F="$( printf '%s' "$C_F" | defs_in )" +if [ "$CD_F" -gt "$CD_A" ] 2>/dev/null && [ "$( nbytes "$C_F" )" -gt "$( nbytes "$C_A" )" ] \ + && printf '%s' "$C_A" | lroot | grep -q ' capped="1"'; then + { [ "$C_D" = "$C_F" ] && printf '%s' "$C_D" | lroot | grep -q ' listing="defs"'; } \ + && ok "LEAN (L15): capped, the defs page is LONGER ($( nbytes "$C_F" ) B vs $( nbytes "$C_A" ) B) but lists more definitions ($CD_F vs $CD_A) — the default is the defs page" \ + || { no "LEAN (L15): the default ($( nbytes "$C_D" ) B) is not the defs page ($( nbytes "$C_F" ) B, $CD_F defs) over the all page ($( nbytes "$C_A" ) B, $CD_A defs)"; printf '%s\n' "$C_D" | lroot; } +else + no "LEAN (L15): premises not met: defs shown $CD_F vs all $CD_A, bytes $( nbytes "$C_F" ) vs $( nbytes "$C_A" ), all capped?" +fi +# (L15-MCP) the same symbol over MCP (no `listing`): byte-identical to the CLI defs page. +if command -v python3 >/dev/null 2>&1; then + { [ -n "$C_F" ] && [ "$( mcpq "$D15" zqCap '' )" = "$C_F" ] && [ "$( mcpq "$D15" zqCap ',"listing":"all"' )" = "$C_A" ]; } \ + && ok "LEAN (L15-MCP): MCP whereis serves the same defs page as the CLI default on the capped answer (listing:\"all\" still the all page)" \ + || no "LEAN (L15-MCP): the MCP default differs from the CLI's on the capped answer" +else + printf ' SKIP LEAN (L15-MCP) MCP twin (no python3)\n' +fi +# (L16) the negative for the bytes rule: 61 definitions on HEAD, one reference, a single branch. Both pages are capped at +# 60 rows and both show the SAME 60 definitions (definitions sort before references), and the all page is shorter (no +# element). The default is the all page: a fix that serves defs whenever the all page is capped, or that counts +# the TOTAL definitions instead of the SHOWN ones, goes red here. +D16="$TMP/cap16"; mkdir -p "$D16"; git -C "$D16" init -q -b main >/dev/null 2>&1; git -C "$D16" config commit.gpgsign false +i=0; while [ $i -lt 61 ]; do printf 'int zqMany( int );\n' >"$D16/h$i.h"; i=$((i+1)); done +printf 'int useMany( void ) { return zqMany( 1 ); }\n' >"$D16/use.c" +git -C "$D16" add -A >/dev/null 2>&1; git -C "$D16" commit -qm many >/dev/null 2>&1 +M16_D="$( dw "$D16" zqMany )"; M16_A="$( dw "$D16" zqMany --whereis-listing=all )"; M16_F="$( dw "$D16" zqMany --whereis-listing=defs )" +M16_DA="$( printf '%s' "$M16_A" | defs_in )"; M16_DF="$( printf '%s' "$M16_F" | defs_in )" +if [ "$M16_DA" = "$M16_DF" ] && [ "$M16_DA" -ge 60 ] 2>/dev/null && printf '%s' "$M16_A" | lroot | grep -q ' capped="1"' \ + && printf '%s' "$M16_F" | lroot | grep -q ' capped="1"' && [ "$( nbytes "$M16_A" )" -lt "$( nbytes "$M16_F" )" ]; then + { [ "$M16_D" = "$M16_A" ] && ! printf '%s' "$M16_D" | lroot | grep -q ' listing='; } \ + && ok "LEAN (L16): capped on both pages, the SAME $M16_DA definitions shown — the shorter all page ($( nbytes "$M16_A" ) B < $( nbytes "$M16_F" ) B) is the default" \ + || { no "LEAN (L16): the default ($( nbytes "$M16_D" ) B) is not the shorter all page ($( nbytes "$M16_A" ) B) when both show the same definitions"; printf '%s\n' "$M16_D" | lroot; } +else + no "LEAN (L16): premises not met: defs shown $M16_DA (all) vs $M16_DF (defs), bytes $( nbytes "$M16_A" ) vs $( nbytes "$M16_F" )" +fi +if command -v python3 >/dev/null 2>&1; then + [ "$( mcpq "$D16" zqMany '' )" = "$M16_A" ] \ + && ok "LEAN (L16-MCP): MCP whereis serves the same all page as the CLI when both pages show the same definitions" \ + || no "LEAN (L16-MCP): the MCP default differs from the CLI's on the capped, same-definitions answer" +fi + if command -v xmllint >/dev/null 2>&1; then if printf '%s' "$SW" | xmllint --noout - 2>/dev/null; then ok "capped stray-content still G4 clean"; else no "capped stray-content XML malformed"; fi if printf '%s' "$W1" | xmllint --noout - 2>/dev/null; then ok "paged whereis still G4 clean"; else no "paged whereis XML malformed"; fi diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index c84d12834..0b492b1e7 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -292,7 +292,14 @@ tools = json.loads( line )[ "result" ][ "tools" ] # field's description now says what its default does ("defs (refs counted), refs or all; default: the shorter page": # absent `listing` serves the defs page only when it is strictly shorter than the all page), +18 B in whereis's schema; # every other tool unchanged. Headroom after this line: 4 B. -CEILING = 46850 +# RE-ANCHORED 2026-10-08 (lane/lean-answers-068 fix round 2, review D1): 46,850 -> 46,890, measured 46,869 (from 46,846, +# 33 tools). The default `listing` is no longer "the shorter page": absent `listing` serves the defs page when it lists MORE +# definitions than the all page under the row cap (a capped all page can list fewer), whatever its bytes, and otherwise the +# strictly shorter of the two. The field's description follows the mechanism, "default: the shorter page" -> +# "default: more defs listed, else the shorter page" (+23 B, the minimum that is true). Attributed: whereis schema 828 -> 851 B +# (+23 B), whereis description unchanged at 701 B, every other tool unchanged. Headroom after this line: 21 B. The twin below +# (1c) pins what must not move with it: whereis's description stays 701 B and the listing field says the exact rule. +CEILING = 46890 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) @@ -301,6 +308,14 @@ print( " INFO %d tools, manifest %d B (~%d tokens): descriptions %d B, schemas check( manifest <= CEILING, "(1) tools/list is %d B, within the %d B per-session ceiling" % ( manifest, CEILING ) ) +# (1c) TWIN of the ceiling re-anchor (PROCESS rule 4): the growth is the listing field's description and nothing else. +whereisTool = [ t for t in tools if t[ "name" ] == "whereis" ] +check( len( whereisTool ) == 1 and len( whereisTool[ 0 ][ "description" ] ) == 701, + "(1c) whereis's own description is still 701 B (the re-anchor was spent in the listing field, not in prose)" ) +listingDesc = whereisTool[ 0 ][ "inputSchema" ].get( "properties", {} ).get( "listing", {} ).get( "description", "" ) if whereisTool else "" +check( listingDesc.endswith( "default: more defs listed, else the shorter page" ), + "(1c) whereis's listing field states the default rule exactly (got: %r)" % listingDesc ) + # ── (1b) the CHANGELOG's own copy of this figure does not silently rot ────────────────────────────── # train10.md's "manifest anchor" finding: the byte figure above lives in two places — this file's own # RE-ANCHORED/RE-MEASURED comment history (checked live, every run) and CHANGELOG.md's prose restating it diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 05d74770b..6698be518 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 b7a2467faab2b69e4fe8e3d8204f158329f6b349d644e5681af860ce6f8ba6ca e3b0c callers 0 31971e419a6df4d59c4464fcdd1afa56c6125e4fc76d096f0203c80cedd9a4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 c48ba09589d13cad6011bd6fadbb2898d68e2f5bd97a10bf44324901cfa08d49 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 a082f92ce84cd952ced8341e3faff33a23685729bfa4ab6f0eda0f7867e41d7c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 6d8f62342517d63043bc48883f9b420a17db647f10831195c4118fe6e7667870 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 99cf380f022bd78990be9e86c7adb125a453eaaf91b2a50134cb4363c960b726 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f3e60d37ab964c4772d794326420441e0b027d1970ea5fb0c60efe6c89f808b8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 cbd133fa53917bad05926d46615d4d8b4457ba9285513dc4b193fa57a1ef8b85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 ff0b962658da396e7bf3360584de21ab2f0e8bebcd8c0e4e997a77994b004a9f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 07a79ee85c5585fcedcf420c1a1f4c27f9a4852406e1d73b3b3d30cc18ea4d0f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 d5e4e99fd0b06848d0f4612645a7ccb2da40fca48583291dc7bf45612b5db94f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/showcase_capture.py b/test/showcase_capture.py index c1f712c03..0153596f3 100644 --- a/test/showcase_capture.py +++ b/test/showcase_capture.py @@ -488,8 +488,8 @@ def _strayScope( sub, kind ): add(S4, f"{BIN} . --stray-content={shlex.quote( _famB )} --plan", f"Select the genuinely-unmerged refs of {_strayScope( _famB, _famBKind )} and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).", timeout=900) add(S4, f"{BIN} . --stray-content=zzzz-no-such-ref --plan", "A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.") add(S4, f"{BIN} . --stray-content={shlex.quote( _famB )} --abi", f"Cross-branch ABI-break gate over {_strayScope( _famB, _famBKind )}: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.", timeout=600) -add(S4, f"{BIN} . --whereis=rankGraphTeleport", "Which ref's tree defines or mentions SYM — HEAD first, then every local branch.", timeout=600) -add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=defs", "The definitions-only page asked for by name: every kind=\"def\" row (here the first 60, most on branches, each carrying tip= and date=) and the references counted in . The default lists this page only when it is strictly shorter in bytes than the whole list (--whereis-listing=all) under the same row cap; on this symbol it is not, so the default above listed every hit.", timeout=600) +add(S4, f"{BIN} . --whereis=rankGraphTeleport", "Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind=\"def\" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.", timeout=600) +add(S4, f"{BIN} . --whereis=rankGraphTeleport --whereis-listing=all", "The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).", timeout=600) add(S4, f"{BIN} . --whereis=computeOnePairOverlap --with-history", "Same, plus a git-history row (never / removed-by-commit) for names no tree carries.", timeout=600) add(S4, f"{BIN} . --flags", "The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.") add(S4, f"{BIN} . --flags --flip=RIPWIRE_ASAN", "Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.") From 4acbcd2299fb0956ab728c960ff3e6034443f88b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:35:51 -0400 Subject: [PATCH 175/229] =?UTF-8?q?test(for):=20forrankordercheck=20(11)?= =?UTF-8?q?=20=E2=80=94=20no=20row=20is=20paid=20where=20every=20row=20+?= =?UTF-8?q?=20next=3D=20already=20fits=20(CLI,=20--json,=20MCP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arms for the completed C3 ruling, on (10)'s relative-path fixture: CLI --for @2160 (17 rows), --json @2000 (16 rows), MCP for @4340 (40 rows) each serve every row the cut leaves plus the unpaid handle, inside the budget, no over_ceiling. Red on 45a2eeba (16/15/39 rows) and on an always-pay mutant; (10)'s over-anyway and payable arms unchanged. CHANGELOG states the rule and the re-measured 20-query counts; one comment says the compact-from-full payment is the PAID case. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 19 ++++++++++--------- src/verbs_for.h | 2 +- test/forrankordercheck.sh | 30 ++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150fafb02..2343c33e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,14 +31,15 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal Present-only legend clauses define each. At the default ceiling, and at any explicit ceiling wide enough that the signature side is frozen at the default's, they are exempt from the signature trim — the rows shown are the ones they were. Under a tighter explicit ceiling (a hard one) - `next=` rides alone and is paid from the block's rows, and a capped compact answer is cut to the rows the full one - keeps, so the answer stays within the budget wherever it can fit; measured at `--token-budget=2000` on 20 queries, that - costs one row on 11 — a row the `next=` itself serves. Rows pay for it ONLY where that makes the answer fit: when the - answer lands past its ceiling paid or not (the rank 1..4 floor has nothing left to give, or the MCP `for` answer, whose - header overshoots `budget_tokens` on its own), no row is dropped for it — the answer keeps the rows the cut alone - leaves, `next=` still ships (the call that recovers a cut is never the thing cut), and the root says - `over_ceiling="1"`, with a clause naming the cause where the answer's own ceiling was otherwise met. Measured on MCP `for` - at `budget_tokens=2000` over 20 queries: the 19 capped answers over their ceiling keep every row they had before. + `next=` rides alone, and rows pay for it ONLY where paying is what makes the answer fit: the answer with every row the + cut leaves plus `next=` is served whenever it fits; only when it lands past its ceiling are rows dropped to pay (a + capped compact answer then serves the rows the full one keeps), and that paid answer is served only when it fits. Past + the ceiling either way (the rank 1..4 floor has nothing left to give, or the MCP `for` answer, whose header overshoots + `budget_tokens` on its own), no row is dropped for it — the answer keeps the rows the cut alone leaves, `next=` still + ships (the call that recovers a cut is never the thing cut), and the root says `over_ceiling="1"`, with a clause naming + the cause where the answer's own ceiling was otherwise met. Measured on 20 queries at a 2000-token budget: the CLI keeps + every row on 19 (the 20th was over its ceiling and now pays one row to fit), `--json` on all 20, MCP `for` on all 20 + (19 of them over their ceiling either way); at 4000, MCP `for` pays on the 6 answers that fit only by paying. No budget, ceiling or token conversion changed. - **`--lint` / `--lint-rules` and `--sarif`.** A rule that spends its per-rule match budget (`count_capped="1"`) now makes the root carry `findings_next=` (SARIF: `runs[0].properties.findingsNext`, which keeps `--sarif`): only the floored @@ -49,7 +50,7 @@ Three cuts were disclosed and still dead ends — counted, with no pasteable cal Also: the `ripwire-handoff` skill passed `--top-k=20` to `--for`, which does not read it; it now spells `--signatures-only`. Gates: `forrankordercheck` (9), `lintbudgetcheck` (5), `impactimportcheck` (#9d), `estchargecheck` -(#11 A7 unpaid twin), `compactlegendcheck` (P4 floor twin), `forrankordercheck` (10), `fordisclosurecheck` (#2c). +(#11 A7 unpaid twin), `compactlegendcheck` (P4 floor twin), `forrankordercheck` (10) and (11), `fordisclosurecheck` (#2c). ### Added — MCP `grep` rows carry the matched text and a `fetch_body` handle (CLI parity) diff --git a/src/verbs_for.h b/src/verbs_for.h index e386acc92..ca65f1819 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -3076,7 +3076,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, bo // grows past the bytes the cut alone left it. THE COMPACT DIALECT ALSO PAYS FROM FULL'S ROOM: its sig ledger charges // less header than it emits and leans on the header rungs to land inside the ceiling, so a charged one also reserves // the room the compact charge was given beyond full's (compactLedgerGapBytes) — in the plan, so only a block the ladder - // already trims pays it: a capped compact answer serves the rows the full one does and lands no further over its + // already trims pays it: a PAID capped compact answer serves the rows the full one does and lands no further over its // ceiling (compactlegendcheck P4). An uncapped answer is untouched. AT THE FLOOR (rank 1..4, nothing left to shed) the // handle still ships and an overshoot is labelled (rootFinish.sigsUnpaidOver below; orchestrator ruling 2026-10-07). // AND ONLY WHERE PAYING IS WHAT MAKES IT FIT (ruling C3, completed 2026-10-08): runForLens renders unpaid first diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index d6f6d3488..e5cc9ba30 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -581,5 +581,35 @@ else no "(10) MCP budget_tokens=3800: shown/capped/handle/over/est = ${*:-} — want a paid handle inside the budget" fi +# ── (11) C3 COMPLETED: PAY FOR next= ONLY WHEN PAYING IS WHAT MAKES THE ANSWER FIT (orchestrator ruling 2026-10-08) ───────────── +# The answer with every row the cut leaves PLUS the unpaid handle is the first candidate; it is served whenever it fits, and only +# when it lands past its ceiling are rows dropped to pay (and served only if THAT fits — (10)'s @3800). 45a2eeba paid first and +# kept the paid answer whenever it fit, so it dropped rows the unpaid answer had room for: the served answer had fewer rows than +# an alternative that also fit. Same fixture and relative path as (10). Pins are 255dc199's own rows at each budget (the cut's +# rows; re-pin from a pre-continuation binary if row bytes or the header change), budgets chosen with room on both sides: +# CLI XML @2160: 255dc199 17 rows (est 2046); 45a2eeba 16 (est 1998) — RED there; head 17 + next= (est 2078 <= 2160). +# --json @2000: 255dc199 16 rows (est 1657); 45a2eeba 15 — RED there; head 16 + "sigs_next" (est 1691). +# MCP @4340: 255dc199 40 rows (est 4257); 45a2eeba 39 — RED there; head 40 + next_budget_tokens= (est 4308 <= 4340). +# Each: rows >= the cut's own, the handle rides, est_tokens <= the budget, no over_ceiling. A binary that always pays when it +# can fit (the 45a2eeba order) is red on all three. +c11row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); h=d["next"] or d["next_budget_tokens"]; print(d["shown"], d["capped"], bool(h), d["over"], d["est_tokens"])' 2>/dev/null; } +c11check(){ # LABEL BUDGET WANT ROWS CAPPED HANDLE OVER EST + local label="$1" bt="$2" want="$3"; shift 3 + if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = False ] && [ "$5" -le "$bt" ]; then + ok "(11) $label @$bt fits with every row the cut leaves: $1 rows (>= $want, none dropped to pay), the handle unpaid, est $5 <= $bt, no over_ceiling=" + else + no "(11) $label @$bt: shown/capped/handle/over/est = ${*:-} — want >= $want rows, the handle, est <= $bt, no over_ceiling= (paid where it already fit?)" + fi +} +c11check "CLI --for" 2160 17 $( c11row "$( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=2160 --no-cache 2>/dev/null | cut_arm )" ) +c11check "MCP for" 4340 40 $( c11row "$( mcprel 4340 )" ) +set -- $( cd "$TMP" && "$BIN" fxcut --for="gadget assembler" --token-budget=2000 --json --no-cache 2>/dev/null \ + | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["sigs"]), "sigs_next" in d, bool(d.get("over_ceiling")), d["est_tokens"])' 2>/dev/null ) +if [ "$#" -eq 4 ] && [ "$1" -ge 16 ] && [ "$2" = True ] && [ "$3" = False ] && [ "$4" -le 2000 ]; then + ok "(11) --for --json @2000 fits with every row the cut leaves: $1 rows (>= 16), \"sigs_next\" unpaid, est $4 <= 2000, no over_ceiling" +else + no "(11) --for --json @2000: rows/sigs_next/over/est = ${*:-} — want >= 16 rows, \"sigs_next\", est <= 2000, no over_ceiling" +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" From afdcf5c8d19af920e231ec1820fd1d5b15c34068 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:44:57 -0400 Subject: [PATCH 176/229] resolve: a binding is read where its language scopes it; a type parameter names no class (review B4) The B3 capture recorded declarations only, so a binding that hides a field or an outer local of the same name (a loop, lambda, catch, resource, pattern, case, out var, destructuring, if/guard/while let, a Python rebinding, a Go range or type-switch variable) left no record and the call proved the field's class: one unhedged wrong edge where base hedged. A generic's type parameter spelled like an in-repo class () was proven as that class. - Every receiver-evidence local now carries the bytes it is visible in; a binding whose class is unknown is a tombstone that hides the outer name and proves nothing. ReceiverEvidence::visibleLocal picks the innermost binding holding the call site; a record with no span that disagrees makes the answer unknown. - Binding forms are captured for Java, C#, Kotlin, Swift and, where FE-B reads evidence, TS/JS, Python and Go. - Type parameter lists are recorded as the walk meets them; a written type that names one is no class. - receiverevidencecheck (B): 177 arms over 8 languages; CHANGELOG states what is wider than the language (hedged) and what is not read. - kParserVer 154 (153 is 152's rich tag); qschemetrip re-pinned (logged). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 19 +- src/ingest_binds.h | 1115 ++++++++++++++--- src/ingest_cache.h | 6 +- src/quality.h | 2 +- src/receiverevidence.h | 98 +- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 3 + test/receiverevidencecheck.sh | 64 +- test/receiverevidencefix/README.md | 7 + .../receiverevidencefix/scope/cs/App/Scope.cs | 34 + test/receiverevidencefix/scope/go/go.mod | 3 + test/receiverevidencefix/scope/go/scope.go | 42 + .../scope/java/src/app/Scope.java | 35 + test/receiverevidencefix/scope/js/scope.js | 8 + .../scope/kt/src/app/Scope.kt | 28 + test/receiverevidencefix/scope/py/scope.py | 50 + .../scope/swift/Sources/App/Scope.swift | 31 + test/receiverevidencefix/scope/ts/scope.ts | 18 + 18 files changed, 1337 insertions(+), 228 deletions(-) create mode 100644 test/receiverevidencefix/scope/cs/App/Scope.cs create mode 100644 test/receiverevidencefix/scope/go/go.mod create mode 100644 test/receiverevidencefix/scope/go/scope.go create mode 100644 test/receiverevidencefix/scope/java/src/app/Scope.java create mode 100644 test/receiverevidencefix/scope/js/scope.js create mode 100644 test/receiverevidencefix/scope/kt/src/app/Scope.kt create mode 100644 test/receiverevidencefix/scope/py/scope.py create mode 100644 test/receiverevidencefix/scope/swift/Sources/App/Scope.swift create mode 100644 test/receiverevidencefix/scope/ts/scope.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ce59256..aca070e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,20 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. (`this.bucket = new Schemas()`, Python `self.x = Foo()`, Go struct and embedded fields, Java/C# fields and properties, Kotlin/Swift properties and Kotlin `val` constructor properties, read through `this`/`self` or bare); Python `feed = parser.feed`. A Go module alias proves its package by the exact import path: the nearest `go.mod`'s - module path plus the directory below it, or the path a local `replace` gives a directory. An untyped local, a local - that hides a field, and an interface-typed receiver with several implementors stay name-only. A JS/TS call on the class object reaches its `static` members only, and a call on an - instance never one. A Ruby call's candidates that Ruby's own method lookup proves (the entries below) are its + module path plus the directory below it, or the path a local `replace` gives a directory (in a tree with no `go.mod` + at all, a directory that ends the import path). A local is read where its language scopes it: a block's declaration + from the declaration to the block's end, a parameter in its function or lambda, a Python name in its whole function. + Every other binding of a name also hides the field or the outer local it shadows there — a loop, lambda, catch, + resource or `with`/`except` variable, a pattern, `case`, `out var` or `match` capture, a destructuring entry, Swift + `if`/`guard`/`while let`, a Go range / type-switch / short variable, a JS/TS/Python/Go reassignment. Such a binding + names its own class only when it is written or constructed with one (a typed loop, lambda or resource variable); otherwise the call + through it is name-only, like a call through an untyped local or an interface-typed receiver with several + implementors. A written type that names a generic's type parameter (``, `[T any]`, `def f[T]`) names no class, + even when a class of that name exists. Wider than the language, and so name-only rather than resolved: a pattern + variable hides its field in its whole enclosing block (an `else` branch too), a Java local hides a field anywhere in + its method, and a Python rebinding anywhere in its function. Not read, the floor: a nested function or class + declaration that reuses a typed binding's name (JS/TS/Python), and Kotlin's implicit `it` beside a field named `it`. A JS/TS call on the class object reaches its `static` members only, + and a call on an instance never one. A Ruby call's candidates that Ruby's own method lookup proves (the entries below) are its answer, never `via="name"`. - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an implicit receiver's class and bases, a free function in scope, the module a receiver alias names). With none @@ -54,7 +65,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. change); the `--max-tokens=3000` maps of the eight repositories keep 337 rows (331 before the change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses for every evidence rule). Ingest records the receiver chain and the Java/C#/Kotlin/Swift declarations - (`kParserVer` 152, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. + (`kParserVer` 154, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 2f4a76508..a69abf79a 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -4081,12 +4081,22 @@ inline void resolvePendingFnBindDecls( std::uint32_t fileId, Lang lang, FnBindGa // stream (streamSideCaptures below) — the pass used to own an identical walk of its own, which is what the // fusion removed. Its state outlives a single node (the L3 clobber sweep needs the whole file's positives), // so it rides in a context the driver holds by reference; bindsFinalize spends it when the stream ends. +// FE-B (review B4b): one type parameter a generic declares (``, `[T any]`, `[T]`) and the declaration it belongs to +// (its owner's bytes). A written type that spells one of these inside the owner is the parameter, never a same-named class. +struct TypeParamScope +{ + std::string_view name; + std::uint32_t start = 0; + std::uint32_t end = 0; +}; + struct BindCtx { std::uint32_t fileId = 0; Lang lang {}; std::string_view src; std::vector* binds = nullptr; + std::vector typeParams; // FE-B: filled as the pre-order walk meets each parameter list // L3 fn-pointer buffers. Positives collect here (not straight into binds) so the end-of-walk clobber // sweep can ask "does this var have a fn binding in this file?" — a clobbering assignment @@ -4213,41 +4223,249 @@ inline std::string_view constructedBy( TSNode value, Lang lang, std::string_view return constructedClass( value, lang, src ); } -// FE-B: Java's `var` is inference, not a class: the written type, unless it is `var`, else what the initializer constructs -inline std::string_view declaredOrConstructed( TSNode type, TSNode value, Lang lang, std::string_view src ) +inline bool identifierKind( const char* t ) noexcept { - std::string_view cls = annotatedClass( type, src ); - if( cls == "var" && lang == Lang::Java ) + return kindIs( t, "identifier" ) || kindIs( t, "type_identifier" ) || kindIs( t, "simple_identifier" ); +} + +// FE-B (review B4b): the names one type parameter declares, visible over [start, end): the leading identifier(s) after any +// annotation, modifier or variance (Java/C#/Kotlin/Swift/TS `T`, Go `A, B any`), or Python 3.12's `type`-wrapped name. +inline void noteTypeParameter( BindCtx& cx, TSNode param, std::uint32_t start, std::uint32_t end ) +{ + bool named = false; + for( std::uint32_t i = 0, k = ts_node_named_child_count( param ); i < k; ++i ) { - cls = {}; + TSNode c = ts_node_named_child( param, i ); + if( !named && kindIs( ts_node_type( c ), "type" ) && ts_node_named_child_count( c ) == 1 ) + { + c = ts_node_named_child( c, 0 ); // Python `def f[T]( … )` + } + if( identifierKind( ts_node_type( c ) ) ) + { + cx.typeParams.push_back( { nodeTextOf( c, cx.src ), start, end } ); + named = true; + } + else if( named ) + { + return; // the bound / constraint after the names + } } - return cls.empty() ? constructedBy( value, lang, src ) : cls; } -// FE-B (review B3): one declarator's evidence — `name` declared with the class its written `type` names, else the class its -// initializer `value` constructs. A parameter or local whose class is unknown (`var s = make()`) is still a DECLARATION: -// recorded with no class (a tombstone), so it hides a same-named field (receiverevidence.h declaresLocal) and proves nothing. -inline void pushDeclaratorEvidence( BindCtx& cx, LocalBindKind kind, TSNode type, TSNode name, TSNode value, std::uint32_t at ) +// a parameter LIST — the generic declaration that owns it is its parent, and the parameters are visible over all of it +// (the pre-order walk meets the list before the owner's parameters, fields and body) +inline void noteTypeParameters( BindCtx& cx, TSNode list ) { - if( ts_node_is_null( name ) || !( kindIs( ts_node_type( name ), "identifier" ) || kindIs( ts_node_type( name ), "simple_identifier" ) ) ) + const TSNode owner = ts_node_parent( list ); + if( ts_node_is_null( owner ) ) + { + return; + } + const std::uint32_t start = ts_node_start_byte( owner ); + const std::uint32_t end = ts_node_end_byte( owner ); + if( cx.lang == Lang::Python ) + { + noteTypeParameter( cx, list, start, end ); // Python's `type_parameter` IS the list: `[A, B]` → one `type` per name + return; + } + for( std::uint32_t i = 0, k = ts_node_named_child_count( list ); i < k; ++i ) + { + const TSNode p = ts_node_named_child( list, i ); + if( kindIs( ts_node_type( p ), "type_parameter" ) || kindIs( ts_node_type( p ), "type_parameter_declaration" ) ) + { + noteTypeParameter( cx, p, start, end ); + } + } +} + +// Go `func ( b Box[T] ) m()`: the receiver's type arguments are the method's type parameters +inline void noteGoReceiverTypeParameters( BindCtx& cx, TSNode method ) +{ + std::vector pending{ fieldChild( method, NodeField::Receiver ) }; + for( int guard = 0; guard < 64 && !pending.empty(); ++guard ) + { + const TSNode n = pending.back(); + pending.pop_back(); + if( ts_node_is_null( n ) ) + { + continue; + } + if( kindIs( ts_node_type( n ), "type_arguments" ) ) + { + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + TSNode a = ts_node_named_child( n, i ); + if( kindIs( ts_node_type( a ), "type_elem" ) && ts_node_named_child_count( a ) == 1 ) + { + a = ts_node_named_child( a, 0 ); + } + if( identifierKind( ts_node_type( a ) ) ) + { + cx.typeParams.push_back( { nodeTextOf( a, cx.src ), ts_node_start_byte( method ), ts_node_end_byte( method ) } ); + } + } + continue; + } + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + pending.push_back( ts_node_named_child( n, i ) ); + } + } +} + +// is `name`, written at byte `at`, a type parameter of a generic that encloses it +inline bool namesTypeParameter( const BindCtx& cx, std::string_view name, std::uint32_t at ) noexcept +{ + return std::any_of( cx.typeParams.begin(), cx.typeParams.end(), + [ & ]( const TypeParamScope& p ) { return p.name == name && p.start <= at && at < p.end; } ); +} + +// the class a written type names at byte `at` — "" when it names none, or names a type parameter (` … Tank t`) +inline std::string_view writtenClass( const BindCtx& cx, TSNode type, std::uint32_t at ) +{ + const std::string_view cls = annotatedClass( type, cx.src ); + return ( !cls.empty() && namesTypeParameter( cx, cls, at ) ) ? std::string_view{} : cls; +} + +// FE-B: Java's `var` is inference, not a class: the written type, unless it is `var`, else what the initializer constructs. +// A written type that is a type parameter names no class, and the initializer does not override it. +inline std::string_view declaredOrConstructed( const BindCtx& cx, TSNode type, TSNode value, std::uint32_t at ) +{ + const std::string_view written = annotatedClass( type, cx.src ); + if( !written.empty() && !( written == "var" && cx.lang == Lang::Java ) ) + { + return namesTypeParameter( cx, written, at ) ? std::string_view{} : written; + } + return constructedBy( value, cx.lang, cx.src ); +} + +// ── scopes (review B4a) ───────────────────────────────────────────────────────────────────────────────────────────── +// A local's record carries the bytes it is visible in (RawBind spanStart/spanEnd; {0,0} = anywhere in its definition), so +// receiverevidence.h visibleLocal can tell which binding of a name a call site sees. A binding whose class is unknown is +// still recorded (a TOMBSTONE): it hides the field or the outer local of its name over its span and proves nothing. + +// the nearest proper ancestor of `n` whose kind is one of `kinds`; null when none within 64 levels +inline TSNode ancestorOfKind( TSNode n, std::initializer_list kinds ) +{ + TSNode p = ts_node_is_null( n ) ? TSNode{} : ts_node_parent( n ); + for( int guard = 0; guard < 64 && !ts_node_is_null( p ); ++guard ) + { + if( std::find( kinds.begin(), kinds.end(), std::string_view( ts_node_type( p ) ) ) != kinds.end() ) + { + return p; + } + p = ts_node_parent( p ); + } + return TSNode{}; +} + +// a name recorded at `at`, visible over the whole of `scope` ({0,0} when there is none: the whole definition) +inline BindSite siteIn( std::uint32_t at, TSNode scope ) noexcept +{ + return ts_node_is_null( scope ) ? BindSite{ at, 0u, 0u } : BindSite{ at, ts_node_start_byte( scope ), ts_node_end_byte( scope ) }; +} +// a name declared at `at`, visible from there to the end of `scope` (a block's declaration) +inline BindSite siteFrom( std::uint32_t at, TSNode scope ) noexcept +{ + return ts_node_is_null( scope ) ? BindSite{ at, 0u, 0u } : BindSite{ at, at, ts_node_end_byte( scope ) }; +} + +// one local binding of `var`, naming class `cls` ("" = unknown), visible over `site`'s span. `flag`: Go's method receiver. +inline void pushLocalEvidence( BindCtx& cx, std::string_view var, std::string_view cls, BindSite site, bool flag = false ) +{ + if( var.empty() ) { return; } - const std::string_view cls = declaredOrConstructed( type, value, cx.lang, cx.src ); - if( !cls.empty() || kind != LocalBindKind::RecvType ) + RawBind b; + b.fileId = cx.fileId; + b.startByte = site.startByte; + b.lang = cx.lang; + b.kind = LocalBindKind::RecvType; + b.isFromAssignment = flag; + b.spanStart = site.spanStart; + b.spanEnd = site.spanEnd; + b.var.assign( var ); + b.typeName.assign( cls ); + cx.binds->push_back( std::move( b ) ); +} +// the same for a name NODE (an identifier of any grammar's spelling; anything else records nothing) +inline void pushLocalEvidence( BindCtx& cx, TSNode name, std::string_view cls, BindSite site ) +{ + if( !ts_node_is_null( name ) && identifierKind( ts_node_type( name ) ) ) + { + pushLocalEvidence( cx, nodeTextOf( name, cx.src ), cls, site ); + } +} +// every identifier `node` holds at any depth (a destructuring / tuple pattern's names), each as a tombstone over `site` +inline void pushPatternTombstones( BindCtx& cx, TSNode node, BindSite site ) +{ + std::vector pending{ node }; + for( int guard = 0; guard < 256 && !pending.empty(); ++guard ) + { + const TSNode n = pending.back(); + pending.pop_back(); + if( ts_node_is_null( n ) ) + { + continue; + } + const char* t = ts_node_type( n ); + if( identifierKind( t ) || kindIs( t, "shorthand_property_identifier_pattern" ) ) + { + pushLocalEvidence( cx, nodeTextOf( n, cx.src ), {}, site ); + continue; + } + if( kindIs( t, "type_identifier" ) || kindIs( t, "user_type" ) || kindIs( t, "type_annotation" ) || kindIs( t, "predefined_type" ) ) + { + continue; + } + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + pending.push_back( ts_node_named_child( n, i ) ); + } + } +} +// a field whose class is unknown (its written type is a type parameter): a tombstone, so it hides a base class's field +inline void pushMemberTombstone( BindCtx& cx, std::string_view var, std::uint32_t at ) +{ + if( var.empty() ) { - pushEvidenceBind( cx, kind, nodeTextOf( name, cx.src ), cls, {}, at, false ); return; } RawBind b; b.fileId = cx.fileId; b.startByte = at; b.lang = cx.lang; - b.kind = LocalBindKind::RecvType; - b.var.assign( nodeTextOf( name, cx.src ) ); + b.kind = LocalBindKind::MemberType; + b.var.assign( var ); cx.binds->push_back( std::move( b ) ); } +// FE-B (review B3): one declarator's evidence — `name` declared with the class its written `type` names, else the class its +// initializer `value` constructs. A parameter or local whose class is unknown (`var s = make()`) is still a DECLARATION: +// recorded with no class (a tombstone), so it hides a same-named field (receiverevidence.h declaresLocal) and proves nothing. +// A local is visible over `site` (review B4a); a field (MemberType) belongs to its class. +inline void pushDeclaratorEvidence( BindCtx& cx, LocalBindKind kind, TSNode type, TSNode name, TSNode value, BindSite site ) +{ + if( ts_node_is_null( name ) || !( kindIs( ts_node_type( name ), "identifier" ) || kindIs( ts_node_type( name ), "simple_identifier" ) ) ) + { + return; + } + const std::string_view cls = declaredOrConstructed( cx, type, value, site.startByte ); + if( kind == LocalBindKind::RecvType ) + { + pushLocalEvidence( cx, name, cls, site ); + } + else if( !cls.empty() ) + { + pushEvidenceBind( cx, kind, nodeTextOf( name, cx.src ), cls, {}, site.startByte, false ); + } + else + { + pushMemberTombstone( cx, nodeTextOf( name, cx.src ), site.startByte ); + } +} + // the first (or, with `last`, the last) named child of `node` whose kind is one of `kinds`; null when none — Kotlin's grammar // carries no field names and Swift's reuses `name` for the parameter, its label and its type inline TSNode namedChildOfKind( TSNode node, std::initializer_list kinds, bool last = false ) @@ -4268,50 +4486,150 @@ inline TSNode namedChildOfKind( TSNode node, std::initializer_list …` (an identifier only) + return; + } + if( kindIs( t, "inferred_parameters" ) ) + { + pushPatternTombstones( cx, n, siteIn( at, ts_node_parent( n ) ) ); // `( a, b ) -> …` + return; + } + if( kindIs( t, "catch_formal_parameter" ) ) + { + pushLocalEvidence( cx, fieldChild( n, NodeField::Name ), {}, siteIn( at, ts_node_parent( n ) ) ); + return; + } + if( kindIs( t, "resource" ) ) + { + pushDeclaratorEvidence( cx, LocalBindKind::RecvType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), fieldChild( n, NodeField::Value ), + siteIn( at, ancestorOfKind( n, { "try_with_resources_statement" } ) ) ); + return; + } + if( kindIs( t, "instanceof_expression" ) ) + { + pushLocalEvidence( cx, fieldChild( n, NodeField::Name ), {}, siteIn( at, block() ) ); // a record pattern: its components below + return; + } + if( kindIs( t, "type_pattern" ) || kindIs( t, "record_pattern_component" ) ) + { + pushLocalEvidence( cx, namedChildOfKind( n, { "identifier" }, true ), {}, siteIn( at, block() ) ); return; } if( !kindIs( t, "local_variable_declaration" ) && !kindIs( t, "field_declaration" ) ) { return; } - const LocalBindKind kind = kindIs( t, "field_declaration" ) ? LocalBindKind::MemberType : LocalBindKind::RecvType; - const TSNode type = fieldChild( n, NodeField::Type ); + const bool field = kindIs( t, "field_declaration" ); + const LocalBindKind kind = field ? LocalBindKind::MemberType : LocalBindKind::RecvType; + const TSNode type = fieldChild( n, NodeField::Type ); + const BindSite site = field ? BindSite{ at, 0u, 0u } : siteFrom( at, ts_node_parent( n ) ); for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) { const TSNode d = ts_node_named_child( n, i ); if( kindIs( ts_node_type( d ), "variable_declarator" ) ) { - pushDeclaratorEvidence( cx, kind, type, fieldChild( d, NodeField::Name ), fieldChild( d, NodeField::Value ), at ); + pushDeclaratorEvidence( cx, kind, type, fieldChild( d, NodeField::Name ), fieldChild( d, NodeField::Value ), site ); } } } -// FE-B (review B3): C# — a typed parameter, each declarator of a local or field declaration, and a property +// FE-B (review B3, B4): C# — a typed parameter (its method's, local function's or lambda's), each declarator of a local +// (from the declaration to its block's end) or field declaration, a property, and every other binding form: a foreach +// variable, an implicit lambda parameter, a catch variable, a query range variable, a deconstruction, and the variables a +// pattern or an `out var` declares (visible in their enclosing block, a superset, with no class recorded). inline void captureCSharpDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) { + const auto block = [ & ] { return ancestorOfKind( n, { "block", "switch_section", "switch_expression_arm", "lambda_expression", "arrow_expression_clause" } ); }; if( kindIs( t, "parameter" ) ) { - pushDeclaratorEvidence( cx, LocalBindKind::RecvType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), TSNode{}, at ); + const TSNode callable = ancestorOfKind( n, { "method_declaration", "constructor_declaration", "destructor_declaration", "operator_declaration", + "conversion_operator_declaration", "indexer_declaration", "local_function_statement", + "lambda_expression", "anonymous_method_expression", "accessor_declaration" } ); + pushDeclaratorEvidence( cx, LocalBindKind::RecvType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), TSNode{}, siteIn( at, callable ) ); + return; + } + if( kindIs( t, "implicit_parameter" ) ) + { + pushLocalEvidence( cx, nodeTextOf( n, cx.src ), {}, siteIn( at, ts_node_parent( n ) ) ); // `x => …` + return; + } + if( kindIs( t, "foreach_statement" ) ) + { + const TSNode left = fieldChild( n, NodeField::Left ); + if( !ts_node_is_null( left ) && kindIs( ts_node_type( left ), "identifier" ) ) + { + pushLocalEvidence( cx, left, writtenClass( cx, fieldChild( n, NodeField::Type ), at ), siteIn( ts_node_start_byte( left ), n ) ); + } + else + { + pushPatternTombstones( cx, left, siteIn( at, n ) ); // `foreach ( var ( a, b ) in … )` + } + return; + } + if( kindIs( t, "declaration_pattern" ) || kindIs( t, "declaration_expression" ) || kindIs( t, "recursive_pattern" ) || kindIs( t, "var_pattern" ) ) + { + const TSNode name = fieldChild( n, NodeField::Name ); + pushPatternTombstones( cx, ts_node_is_null( name ) && kindIs( t, "var_pattern" ) ? n : name, siteIn( at, block() ) ); + return; + } + if( kindIs( t, "tuple_pattern" ) ) + { + pushPatternTombstones( cx, n, siteIn( at, block() ) ); // `var ( a, b ) = p;` + return; + } + if( kindIs( t, "catch_declaration" ) ) + { + pushLocalEvidence( cx, fieldChild( n, NodeField::Name ), {}, siteIn( at, ts_node_parent( n ) ) ); + return; + } + if( kindIs( t, "from_clause" ) || kindIs( t, "join_clause" ) || kindIs( t, "let_clause" ) || kindIs( t, "query_continuation" ) || kindIs( t, "join_into_clause" ) ) + { + pushLocalEvidence( cx, fieldChild( n, NodeField::Name ), {}, siteIn( at, ancestorOfKind( n, { "query_expression" } ) ) ); return; } if( kindIs( t, "property_declaration" ) ) { - pushDeclaratorEvidence( cx, LocalBindKind::MemberType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), fieldChild( n, NodeField::Value ), at ); + pushDeclaratorEvidence( cx, LocalBindKind::MemberType, fieldChild( n, NodeField::Type ), fieldChild( n, NodeField::Name ), fieldChild( n, NodeField::Value ), + BindSite{ at, 0u, 0u } ); return; } if( !kindIs( t, "variable_declaration" ) ) { return; } - // a field's declaration sits in a field_declaration, a local's in a local_declaration_statement (or a using/for) - const TSNode up = ts_node_parent( n ); - const LocalBindKind kind = ( !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "field_declaration" ) ) ? LocalBindKind::MemberType : LocalBindKind::RecvType; + // a field's declaration sits in a field_declaration, a local's in a local_declaration_statement (or a using/for), whose + // block is where it is visible + TSNode up = ts_node_parent( n ); + const bool field = !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "field_declaration" ); + if( !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "local_declaration_statement" ) ) + { + up = ts_node_parent( up ); + } + const LocalBindKind kind = field ? LocalBindKind::MemberType : LocalBindKind::RecvType; + const BindSite site = field ? BindSite{ at, 0u, 0u } : siteFrom( at, up ); const TSNode type = fieldChild( n, NodeField::Type ); for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) { @@ -4321,57 +4639,171 @@ inline void captureCSharpDeclEvidence( BindCtx& cx, TSNode n, const char* t, std // the initializer is the declarator's last named child, when it has one besides the name (no field names it) const TSNode name = fieldChild( d, NodeField::Name ); const TSNode value = ts_node_named_child_count( d ) > 1 ? ts_node_named_child( d, ts_node_named_child_count( d ) - 1 ) : TSNode{}; - pushDeclaratorEvidence( cx, kind, type, name, ts_node_eq( value, name ) ? TSNode{} : value, at ); + pushDeclaratorEvidence( cx, kind, type, name, ts_node_eq( value, name ) ? TSNode{} : value, site ); } } } -// FE-B (review B3): Kotlin and Swift — a typed parameter (Kotlin `val` constructor parameters are properties), and a local -// or property declaration (a property in a class body, a local elsewhere) -inline void captureKotlinSwiftDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +// FE-B (review B3, B4): Kotlin — a typed parameter (its function's or lambda's; `val` constructor parameters are +// properties), a local or property declaration (a property in a class body, a local elsewhere, visible from the declaration +// to its block's end), and every other variable a declaration binds: a for variable, a lambda parameter, a destructuring +// entry, a `when ( val x = … )` subject, a catch parameter. +inline void captureKotlinDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) { - if( kindIs( t, "parameter" ) || ( cx.lang == Lang::Kotlin && kindIs( t, "class_parameter" ) ) ) + if( kindIs( t, "parameter" ) || kindIs( t, "class_parameter" ) ) { const bool property = kindIs( t, "class_parameter" ); if( property && ts_node_is_null( namedChildOfKind( n, { "binding_pattern_kind" } ) ) ) { return; // a plain constructor parameter, not a property } - // Swift `label p: T`: the LAST identifier is the parameter's own name - pushDeclaratorEvidence( cx, property ? LocalBindKind::MemberType : LocalBindKind::RecvType, - namedChildOfKind( n, { "user_type", "nullable_type", "optional_type" }, true ), - namedChildOfKind( n, { "simple_identifier" }, true ), TSNode{}, at ); + const TSNode callable = ancestorOfKind( n, { "function_declaration", "anonymous_function", "secondary_constructor", "setter", "lambda_literal" } ); + pushDeclaratorEvidence( cx, property ? LocalBindKind::MemberType : LocalBindKind::RecvType, namedChildOfKind( n, { "user_type", "nullable_type" }, true ), + namedChildOfKind( n, { "simple_identifier" }, true ), TSNode{}, property ? BindSite{ at, 0u, 0u } : siteIn( at, callable ) ); + return; + } + if( kindIs( t, "catch_block" ) ) + { + pushLocalEvidence( cx, namedChildOfKind( n, { "simple_identifier" } ), {}, siteIn( at, n ) ); + return; + } + if( kindIs( t, "variable_declaration" ) ) + { + // a declaration's own variable is read with the declaration below; here, every other place a variable is bound + TSNode up = ts_node_parent( n ); + if( !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "multi_variable_declaration" ) ) + { + up = ts_node_parent( up ); + } + if( ts_node_is_null( up ) || kindIs( ts_node_type( up ), "property_declaration" ) ) + { + return; + } + TSNode scope = up; // a for loop + if( kindIs( ts_node_type( up ), "lambda_parameters" ) || kindIs( ts_node_type( up ), "when_subject" ) ) + { + scope = ts_node_parent( up ); // the lambda / the when expression + } + else if( !kindIs( ts_node_type( up ), "for_statement" ) ) + { + scope = ancestorOfKind( n, { "for_statement", "lambda_literal", "when_expression", "function_declaration" } ); + } + pushLocalEvidence( cx, namedChildOfKind( n, { "simple_identifier" } ), writtenClass( cx, namedChildOfKind( n, { "user_type", "nullable_type" } ), at ), + siteIn( at, scope ) ); return; } if( !kindIs( t, "property_declaration" ) ) { return; } - const TSNode up = ts_node_parent( n ); - const LocalBindKind kind = ( !ts_node_is_null( up ) && ( kindIs( ts_node_type( up ), "class_body" ) || kindIs( ts_node_type( up ), "enum_class_body" ) ) ) - ? LocalBindKind::MemberType : LocalBindKind::RecvType; - if( cx.lang == Lang::Kotlin ) + const TSNode up = ts_node_parent( n ); + const bool field = !ts_node_is_null( up ) && ( kindIs( ts_node_type( up ), "class_body" ) || kindIs( ts_node_type( up ), "enum_class_body" ) ); + const BindSite site = field ? BindSite{ at, 0u, 0u } : siteFrom( at, up ); + const TSNode var = namedChildOfKind( n, { "variable_declaration" } ); + if( !ts_node_is_null( var ) ) + { + pushDeclaratorEvidence( cx, field ? LocalBindKind::MemberType : LocalBindKind::RecvType, namedChildOfKind( var, { "user_type", "nullable_type" } ), + namedChildOfKind( var, { "simple_identifier" } ), namedChildOfKind( n, { "call_expression" } ), site ); + return; + } + const TSNode multi = namedChildOfKind( n, { "multi_variable_declaration" } ); // `val ( a, b ) = p` + for( std::uint32_t i = 0, k = ts_node_is_null( multi ) || field ? 0u : ts_node_named_child_count( multi ); i < k; ++i ) + { + const TSNode v = ts_node_named_child( multi, i ); + pushLocalEvidence( cx, namedChildOfKind( v, { "simple_identifier" } ), writtenClass( cx, namedChildOfKind( v, { "user_type", "nullable_type" } ), at ), site ); + } +} + +// Swift: where the names a pattern (or an if/guard/while condition) binds are visible — the statement that binds them, +// the rest of the enclosing block for `guard` and for a declaration +inline BindSite swiftBindingSite( TSNode n, std::uint32_t at ) +{ + const TSNode s = ancestorOfKind( n, { "for_statement", "if_statement", "while_statement", "repeat_while_statement", "switch_entry", "catch_block", + "lambda_literal", "guard_statement", "property_declaration", "function_declaration", "init_declaration" } ); + if( !ts_node_is_null( s ) && ( kindIs( ts_node_type( s ), "guard_statement" ) || kindIs( ts_node_type( s ), "property_declaration" ) ) ) + { + return { at, ts_node_start_byte( s ), ts_node_end_byte( ts_node_parent( s ) ) }; + } + return siteIn( at, s ); +} + +// FE-B (review B3, B4): Swift — a typed parameter (its function's), a local or property declaration (a property in a +// class body, a local elsewhere, visible from the declaration to its block's end), a closure parameter, and every name a +// pattern binds: for-in, if/guard/while `let`, `case let`, catch, a tuple declaration. +inline void captureSwiftDeclEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + if( kindIs( t, "parameter" ) ) + { + // `label p: T`: the LAST identifier is the parameter's own name + const TSNode callable = ancestorOfKind( n, { "function_declaration", "init_declaration", "subscript_declaration", "lambda_literal" } ); + pushDeclaratorEvidence( cx, LocalBindKind::RecvType, namedChildOfKind( n, { "user_type", "optional_type" }, true ), + namedChildOfKind( n, { "simple_identifier" }, true ), TSNode{}, siteIn( at, callable ) ); + return; + } + if( kindIs( t, "lambda_parameter" ) ) + { + pushLocalEvidence( cx, namedChildOfKind( n, { "simple_identifier" } ), writtenClass( cx, namedChildOfKind( n, { "user_type", "optional_type" } ), at ), + siteIn( at, ancestorOfKind( n, { "lambda_literal" } ) ) ); + return; + } + if( kindIs( t, "pattern" ) ) { - const TSNode var = namedChildOfKind( n, { "variable_declaration" } ); // null for `val ( a, b ) = …`: not this rule's question - if( !ts_node_is_null( var ) ) + const TSNode up = ts_node_parent( n ); + if( !ts_node_is_null( up ) && kindIs( ts_node_type( up ), "property_declaration" ) && !ts_node_is_null( namedChildOfKind( n, { "simple_identifier" } ) ) ) + { + return; // a plain declaration: read with its type below + } + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) { - pushDeclaratorEvidence( cx, kind, namedChildOfKind( var, { "user_type", "nullable_type" } ), namedChildOfKind( var, { "simple_identifier" } ), - namedChildOfKind( n, { "call_expression" } ), at ); + const TSNode c = ts_node_named_child( n, i ); + if( kindIs( ts_node_type( c ), "simple_identifier" ) ) + { + pushLocalEvidence( cx, c, {}, swiftBindingSite( n, at ) ); + } } return; } - const TSNode pattern = fieldChild( n, NodeField::Name ); - const TSNode ann = namedChildOfKind( n, { "type_annotation" } ); - pushDeclaratorEvidence( cx, kind, ts_node_is_null( ann ) ? TSNode{} : namedChildOfKind( ann, { "user_type", "optional_type" } ), - ts_node_is_null( pattern ) ? TSNode{} : namedChildOfKind( pattern, { "simple_identifier" } ), fieldChild( n, NodeField::Value ), at ); + if( kindIs( t, "if_statement" ) || kindIs( t, "guard_statement" ) || kindIs( t, "while_statement" ) ) + { + // `if let x = …`: the bound name is the statement's own `bound_identifier` child + const BindSite site = kindIs( t, "guard_statement" ) ? BindSite{ at, at, ts_node_end_byte( ts_node_parent( n ) ) } : siteIn( at, n ); + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); + if( field != nullptr && kindIs( field, "bound_identifier" ) ) + { + pushLocalEvidence( cx, c, {}, site ); + } + return true; + } ); + return; + } + if( !kindIs( t, "property_declaration" ) ) + { + return; + } + const TSNode up = ts_node_parent( n ); + const bool field = !ts_node_is_null( up ) && ( kindIs( ts_node_type( up ), "class_body" ) || kindIs( ts_node_type( up ), "enum_class_body" ) ); + const TSNode pattern = fieldChild( n, NodeField::Name ); + const TSNode ann = namedChildOfKind( n, { "type_annotation" } ); + pushDeclaratorEvidence( cx, field ? LocalBindKind::MemberType : LocalBindKind::RecvType, ts_node_is_null( ann ) ? TSNode{} : namedChildOfKind( ann, { "user_type", "optional_type" } ), + ts_node_is_null( pattern ) ? TSNode{} : namedChildOfKind( pattern, { "simple_identifier" } ), fieldChild( n, NodeField::Value ), + field ? BindSite{ at, 0u, 0u } : siteFrom( at, up ) ); } // FE-B (review B3): a declaration's evidence in Java, C#, Kotlin and Swift — a typed parameter, a typed or constructed -// local (RecvType), a typed or constructed field / property (MemberType). The base resolver bound these receivers by name -// alone; FE-B proves a member call only through evidence, so the declarations are read here. +// local (RecvType), a typed or constructed field / property (MemberType); (review B4) every other binding form, and the +// type parameters a generic declares. The base resolver bound these receivers by name alone; FE-B proves a member call +// only through evidence, so the declarations are read here. inline void captureTypedDeclEvidence( BindCtx& cx, TSNode n, const char* t ) { const std::uint32_t at = ts_node_start_byte( n ); + if( kindIs( t, "type_parameters" ) || kindIs( t, "type_parameter_list" ) ) + { + noteTypeParameters( cx, n ); + return; + } if( cx.lang == Lang::Java ) { captureJavaDeclEvidence( cx, n, t, at ); @@ -4380,173 +4812,200 @@ inline void captureTypedDeclEvidence( BindCtx& cx, TSNode n, const char* t ) { captureCSharpDeclEvidence( cx, n, t, at ); } + else if( cx.lang == Lang::Kotlin ) + { + captureKotlinDeclEvidence( cx, n, t, at ); + } else { - captureKotlinSwiftDeclEvidence( cx, n, t, at ); + captureSwiftDeclEvidence( cx, n, t, at ); } } -// FE-B: one node's receiver evidence: Python, JS/TS and Go, and (review B3) Java, C#, Kotlin and Swift declarations. -inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) +// JS/TS (review B4a): where a declarator's names are visible — a `let`/`const` from the declaration to the end of its block +// (or loop / switch / function), a `var` anywhere in its function ({0,0}) +inline BindSite jsDeclaratorSite( TSNode declarator ) { - const Lang lang = cx.lang; - const std::string_view src = cx.src; - const std::uint32_t at = ts_node_start_byte( n ); - if( lang == Lang::Python ) + const std::uint32_t at = ts_node_start_byte( declarator ); + const TSNode decl = ts_node_parent( declarator ); + if( ts_node_is_null( decl ) || !kindIs( ts_node_type( decl ), "lexical_declaration" ) ) { - if( kindIs( t, "typed_parameter" ) || kindIs( t, "typed_default_parameter" ) ) + return { at, 0u, 0u }; + } + TSNode scope = ts_node_parent( decl ); + for( int guard = 0; guard < 64 && !ts_node_is_null( scope ); ++guard ) + { + const char* st = ts_node_type( scope ); + if( jsFunctionScope( scope ) || kindIs( st, "program" ) || kindIs( st, "statement_block" ) || kindIs( st, "for_statement" ) + || kindIs( st, "for_in_statement" ) || kindIs( st, "switch_body" ) || kindIs( st, "class_static_block" ) ) { - TSNode name = fieldChild( n, NodeField::Name ); - if( ts_node_is_null( name ) && ts_node_named_child_count( n ) > 0 ) - { - name = ts_node_named_child( n, 0 ); // typed_parameter spells its identifier without a field - } - if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) - { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); - } + return siteFrom( at, scope ); } - else if( kindIs( t, "import_from_statement" ) ) - { - ChildCursor cursor( n ); - forEachChild( n, cursor.cur, [ & ]( TSNode c ) - { - if( kindIs( ts_node_type( c ), "aliased_import" ) ) - { - const TSNode name = fieldChild( c, NodeField::Name ); - const TSNode alias = fieldChild( c, NodeField::Alias ); - if( !ts_node_is_null( name ) && !ts_node_is_null( alias ) ) - { - pushEvidenceBind( cx, LocalBindKind::NameAlias, nodeTextOf( alias, src ), finalSegment( nodeTextOf( name, src ) ), {}, ts_node_start_byte( c ), false ); - } - } - return true; - } ); + scope = ts_node_parent( scope ); + } + return { at, 0u, 0u }; +} + +// each name a JS/TS binding pattern binds (never an initializer's or a type's), as a tombstone over `site` +inline void pushJsPatternTombstones( BindCtx& cx, TSNode pattern, BindSite site ) +{ + for( const std::string& name : jsPatternNames( pattern, cx.src ) ) + { + pushLocalEvidence( cx, name, {}, site ); + } +} + +// FE-B (review B4a): Python binds a name for the whole function wherever it assigns it (no block scope), so every +// rebinding form records the name with no span: a `for` target, a `with … as` / `except … as` / `case … as` name, a +// `match` capture, a walrus name, and an assignment the base Type record does not already type (an annotation is read below, a constructor call by +// the base pass). A lambda's parameters and a comprehension's targets are visible inside it only; an untyped parameter +// hides an enclosing function's binding of its name. +inline void capturePythonBindingTombstones( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + std::vector targets; + const auto tombstones = [ & ]( TSNode target, BindSite site ) + { + targets.clear(); + collectPythonNameTargets( target, targets ); + for( const TSNode& id : targets ) + { + pushLocalEvidence( cx, id, {}, site ); } - else if( kindIs( t, "assignment" ) ) + }; + if( kindIs( t, "for_statement" ) ) + { + tombstones( fieldChild( n, NodeField::Left ), BindSite{ at, 0u, 0u } ); + } + else if( kindIs( t, "for_in_clause" ) ) + { + tombstones( fieldChild( n, NodeField::Left ), siteIn( at, ts_node_parent( n ) ) ); + } + else if( kindIs( t, "named_expression" ) ) + { + tombstones( fieldChild( n, NodeField::Name ), BindSite{ at, 0u, 0u } ); + } + else if( kindIs( t, "as_pattern" ) ) + { + // `with X as n:` / `except X as n:` (an `alias` field), `case P() as n:` (the last identifier) + TSNode alias = fieldChild( n, NodeField::Alias ); + if( ts_node_is_null( alias ) ) { - const TSNode lhs = fieldChild( n, NodeField::Left ); - const TSNode rhs = fieldChild( n, NodeField::Right ); - if( ts_node_is_null( lhs ) ) - { - return; - } - const char* lt = ts_node_type( lhs ); - if( kindIs( lt, "identifier" ) && !ts_node_is_null( fieldChild( n, NodeField::Type ) ) ) - { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( lhs, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); - } - else if( kindIs( lt, "identifier" ) && !ts_node_is_null( rhs ) && kindIs( ts_node_type( rhs ), "attribute" ) ) - { - const TSNode obj = fieldChild( rhs, NodeField::Object ); - const TSNode attr = fieldChild( rhs, NodeField::Attribute ); - if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) ) - { - pushEvidenceBind( cx, LocalBindKind::MethodAlias, nodeTextOf( lhs, src ), nodeTextOf( obj, src ), nodeTextOf( attr, src ), at, false ); - } - } - else if( kindIs( lt, "attribute" ) ) - { - const TSNode obj = fieldChild( lhs, NodeField::Object ); - const TSNode attr = fieldChild( lhs, NodeField::Attribute ); - if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) && nodeTextOf( obj, src ) == "self" ) - { - pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( attr, src ), constructedBy( rhs, lang, src ), {}, at, false ); - } - } + alias = namedChildOfKind( n, { "identifier" }, true ); } - return; + tombstones( !ts_node_is_null( alias ) && kindIs( ts_node_type( alias ), "as_pattern_target" ) && ts_node_named_child_count( alias ) > 0 + ? ts_node_named_child( alias, 0 ) : alias, BindSite{ at, 0u, 0u } ); } - if( lang == Lang::JavaScript || lang == Lang::TypeScript ) + else if( kindIs( t, "case_pattern" ) || kindIs( t, "keyword_pattern" ) || kindIs( t, "splat_pattern" ) ) { - if( lang == Lang::JavaScript && kindIs( t, "variable_declarator" ) ) // TS records these as Type already + // a `match` capture: a bare name (`case n:`, `[n, *rest]`, `P(x=n)`); a dotted name is a value pattern, a class + // pattern's class sits under class_pattern + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) { - const TSNode name = fieldChild( n, NodeField::Name ); - if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + const TSNode c = ts_node_named_child( n, i ); + const char* ct = ts_node_type( c ); + if( kindIs( ct, "dotted_name" ) && ts_node_named_child_count( c ) == 1 ) { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), constructedBy( fieldChild( n, NodeField::Value ), lang, src ), {}, at, false ); + pushLocalEvidence( cx, ts_node_named_child( c, 0 ), {}, BindSite{ at, 0u, 0u } ); } - } - else if( lang == Lang::TypeScript && ( kindIs( t, "required_parameter" ) || kindIs( t, "optional_parameter" ) ) ) - { - const TSNode name = fieldChild( n, NodeField::Pattern ); - if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + else if( kindIs( t, "splat_pattern" ) && kindIs( ct, "identifier" ) ) { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), annotatedClass( fieldChild( n, NodeField::Type ), src ), {}, at, false ); + pushLocalEvidence( cx, c, {}, BindSite{ at, 0u, 0u } ); } } - else if( kindIs( t, "method_definition" ) ) + } + else if( kindIs( t, "lambda" ) ) + { + const TSNode params = fieldChild( n, NodeField::Parameters ); + for( std::uint32_t i = 0, k = ts_node_is_null( params ) ? 0u : ts_node_named_child_count( params ); i < k; ++i ) { - // `static m () {}`: the member's side of the lookup, recorded inside its body so it attributes to the member - const TSNode name = fieldChild( n, NodeField::Name ); - const TSNode body = fieldChild( n, NodeField::Body ); - bool isStatic = false; - for( std::uint32_t i = 0, k = ts_node_child_count( n ); i < k && !isStatic; ++i ) - { - isStatic = kindIs( ts_node_type( ts_node_child( n, i ) ), "static" ); - } - if( isStatic && !ts_node_is_null( name ) && !ts_node_is_null( body ) ) - { - pushEvidenceBind( cx, LocalBindKind::StaticMember, nodeTextOf( name, src ), "static", {}, ts_node_start_byte( body ), false ); - } + const TSNode p = ts_node_named_child( params, i ); + pushLocalEvidence( cx, kindIs( ts_node_type( p ), "identifier" ) ? p : fieldChild( p, NodeField::Name ), {}, siteIn( at, n ) ); } - else if( lang == Lang::TypeScript && kindIs( t, "public_field_definition" ) ) + } + else if( kindIs( t, "parameters" ) ) + { + // an untyped parameter (a typed one is read with its annotation): `x`, `x=1`, `*xs`, `**kw` + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) { - const TSNode name = fieldChild( n, NodeField::Name ); - if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "property_identifier" ) ) - { - std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); - if( type.empty() ) - { - type = constructedBy( fieldChild( n, NodeField::Value ), lang, src ); - } - pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( name, src ), type, {}, at, false ); - } + const TSNode p = ts_node_named_child( n, i ); + const char* pt = ts_node_type( p ); + const TSNode id = kindIs( pt, "identifier" ) ? p + : kindIs( pt, "default_parameter" ) ? fieldChild( p, NodeField::Name ) + : ( kindIs( pt, "list_splat_pattern" ) || kindIs( pt, "dictionary_splat_pattern" ) ) && ts_node_named_child_count( p ) > 0 + ? ts_node_named_child( p, 0 ) : TSNode{}; + pushLocalEvidence( cx, id, {}, BindSite{ ts_node_start_byte( p ), 0u, 0u } ); } - else if( kindIs( t, "assignment_expression" ) ) + } + else if( kindIs( t, "assignment" ) && ts_node_is_null( fieldChild( n, NodeField::Type ) ) ) + { + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); + const bool typedByBase = !ts_node_is_null( lhs ) && kindIs( ts_node_type( lhs ), "identifier" ) && !ts_node_is_null( rhs ) + && kindIs( ts_node_type( rhs ), "call" ) && !ts_node_is_null( fieldChild( rhs, NodeField::Function ) ) + && kindIs( ts_node_type( fieldChild( rhs, NodeField::Function ) ), "identifier" ); + if( !typedByBase ) { - const TSNode lhs = fieldChild( n, NodeField::Left ); - if( ts_node_is_null( lhs ) || !kindIs( ts_node_type( lhs ), "member_expression" ) ) - { - return; - } - const TSNode obj = fieldChild( lhs, NodeField::Object ); - const TSNode prop = fieldChild( lhs, NodeField::Property ); - if( !ts_node_is_null( obj ) && !ts_node_is_null( prop ) && kindIs( ts_node_type( obj ), "this" ) && kindIs( ts_node_type( prop ), "property_identifier" ) ) - { - pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( prop, src ), constructedBy( fieldChild( n, NodeField::Right ), lang, src ), {}, at, false ); - } - // `Foo.prototype.m = function …`: the member's class, recorded INSIDE the function so the record attributes to it - const TSNode rhs = fieldChild( n, NodeField::Right ); - if( !ts_node_is_null( obj ) && kindIs( ts_node_type( obj ), "member_expression" ) && !ts_node_is_null( rhs ) - && ( kindIs( ts_node_type( rhs ), "function_expression" ) || kindIs( ts_node_type( rhs ), "function" ) || kindIs( ts_node_type( rhs ), "arrow_function" ) ) - && nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "prototype" ) - { - const TSNode ctor = fieldChild( obj, NodeField::Object ); - if( !ts_node_is_null( ctor ) && kindIs( ts_node_type( ctor ), "identifier" ) ) - { - pushEvidenceBind( cx, LocalBindKind::RecvType, {}, nodeTextOf( ctor, src ), {}, ts_node_start_byte( rhs ), true ); - } - } + tombstones( lhs, BindSite{ at, 0u, 0u } ); } - return; } - if( lang == Lang::Java || lang == Lang::CSharp || lang == Lang::Kotlin || lang == Lang::Swift ) +} + +// FE-B (review B4a): Go — where a name declared inside a function is visible: a short variable declaration in a block from +// there to the block's end; in an if / for / switch header, that whole statement +inline BindSite goShortVarSite( TSNode decl, std::uint32_t at ) +{ + const TSNode up = ts_node_parent( decl ); + if( ts_node_is_null( up ) ) + { + return { at, 0u, 0u }; + } + const char* ut = ts_node_type( up ); + if( kindIs( ut, "block" ) ) { - captureTypedDeclEvidence( cx, n, t ); - return; + return siteFrom( at, up ); } - if( lang != Lang::Go ) + if( kindIs( ut, "for_clause" ) ) { - return; + return siteIn( at, ts_node_parent( up ) ); } - if( kindIs( t, "parameter_declaration" ) ) + if( kindIs( ut, "if_statement" ) || kindIs( ut, "expression_switch_statement" ) || kindIs( ut, "type_switch_statement" ) ) + { + return siteIn( at, up ); + } + return siteFrom( at, ancestorOfKind( decl, { "block" } ) ); +} + +// each identifier an expression_list holds (Go's left-hand sides), recorded naming `cls` over `site` +inline void pushGoNames( BindCtx& cx, TSNode list, std::string_view cls, BindSite site ) +{ + for( std::uint32_t i = 0, k = ts_node_is_null( list ) ? 0u : ts_node_named_child_count( list ); i < k; ++i ) + { + pushLocalEvidence( cx, ts_node_named_child( list, i ), cls, site ); // `_` is an identifier too: harmless, never a receiver + } +} + +// FE-B: Go's receiver evidence — typed parameters (the method receiver names the method's class), typed / constructed +// locals, struct field types; (review B4) every other name a function binds (a range / type-switch / receive variable, an +// untyped short declaration, a reassignment), and the type parameters a generic declares. +inline void captureGoEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + const std::string_view src = cx.src; + if( kindIs( t, "type_parameter_list" ) ) + { + noteTypeParameters( cx, n ); + } + else if( kindIs( t, "method_declaration" ) ) + { + noteGoReceiverTypeParameters( cx, n ); + } + else if( kindIs( t, "parameter_declaration" ) ) { const TSNode list = ts_node_parent( n ); const TSNode method = ts_node_is_null( list ) ? TSNode{} : ts_node_parent( list ); const bool receiver = !ts_node_is_null( method ) && kindIs( ts_node_type( method ), "method_declaration" ) && ts_node_eq( fieldChild( method, NodeField::Receiver ), list ); - const std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + const std::string_view type = writtenClass( cx, fieldChild( n, NodeField::Type ), at ); + const BindSite site = siteIn( at, ancestorOfKind( n, { "function_declaration", "method_declaration", "func_literal" } ) ); bool named = false; ChildCursor cursor( n ); forEachChild( n, cursor.cur, [ & ]( TSNode c ) @@ -4555,7 +5014,7 @@ inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "identifier" ) ) { named = true; - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( c, src ), type, {}, at, receiver ); + pushLocalEvidence( cx, nodeTextOf( c, src ), type, site, receiver ); } return true; } ); @@ -4566,40 +5025,48 @@ inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) } else if( kindIs( t, "var_spec" ) ) { - std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + std::string_view type = writtenClass( cx, fieldChild( n, NodeField::Type ), at ); const TSNode values = fieldChild( n, NodeField::Value ); - if( type.empty() && !ts_node_is_null( values ) && ts_node_named_child_count( values ) == 1 ) + if( type.empty() && ts_node_is_null( fieldChild( n, NodeField::Type ) ) && !ts_node_is_null( values ) && ts_node_named_child_count( values ) == 1 ) { - type = constructedBy( ts_node_named_child( values, 0 ), lang, src ); + type = constructedBy( ts_node_named_child( values, 0 ), cx.lang, src ); } + const BindSite site = siteFrom( at, ancestorOfKind( n, { "block" } ) ); ChildCursor cursor( n ); forEachChild( n, cursor.cur, [ & ]( TSNode c ) { const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "identifier" ) ) { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( c, src ), type, {}, at, false ); + pushLocalEvidence( cx, nodeTextOf( c, src ), type, site ); } return true; } ); } - else if( kindIs( t, "short_var_declaration" ) ) + else if( kindIs( t, "short_var_declaration" ) || kindIs( t, "assignment_statement" ) ) { + // `x := T{}` names T; `a, b := …`, `x := f()` name no class. An ASSIGNMENT rebinds a name declared elsewhere: no span. const TSNode left = fieldChild( n, NodeField::Left ); const TSNode right = fieldChild( n, NodeField::Right ); - if( ts_node_is_null( left ) || ts_node_is_null( right ) || ts_node_named_child_count( left ) != 1 || ts_node_named_child_count( right ) != 1 ) - { - return; // `a, b := …`: which value names which class is not this rule's question - } - const TSNode name = ts_node_named_child( left, 0 ); - if( kindIs( ts_node_type( name ), "identifier" ) ) - { - pushEvidenceBind( cx, LocalBindKind::RecvType, nodeTextOf( name, src ), constructedBy( ts_node_named_child( right, 0 ), lang, src ), {}, at, false ); - } + const bool one = !ts_node_is_null( left ) && !ts_node_is_null( right ) && ts_node_named_child_count( left ) == 1 && ts_node_named_child_count( right ) == 1; + pushGoNames( cx, left, one ? constructedBy( ts_node_named_child( right, 0 ), cx.lang, src ) : std::string_view{}, + kindIs( t, "assignment_statement" ) ? BindSite{ at, 0u, 0u } : goShortVarSite( n, at ) ); + } + else if( kindIs( t, "range_clause" ) || kindIs( t, "receive_statement" ) ) + { + // `for k, v := range xs` (the loop) / `case v := <-ch:` (the case); `=` assigns names declared elsewhere: no span + const BindSite site = jsHasToken( n, ":=" ) ? siteIn( at, ancestorOfKind( n, { "for_statement", "communication_case" } ) ) : BindSite{ at, 0u, 0u }; + pushGoNames( cx, fieldChild( n, NodeField::Left ), {}, site ); + } + else if( kindIs( t, "type_switch_statement" ) ) + { + pushGoNames( cx, fieldChild( n, NodeField::Alias ), {}, siteIn( at, n ) ); // `switch v := x.(type)` } else if( kindIs( t, "field_declaration" ) ) { - const std::string_view type = annotatedClass( fieldChild( n, NodeField::Type ), src ); + const std::string_view written = annotatedClass( fieldChild( n, NodeField::Type ), src ); + const bool param = !written.empty() && namesTypeParameter( cx, written, at ); + const std::string_view type = param ? std::string_view{} : written; bool named = false; ChildCursor cursor( n ); forEachChild( n, cursor.cur, [ & ]( TSNode c ) @@ -4608,17 +5075,267 @@ inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) if( field != nullptr && kindIs( field, "name" ) && kindIs( ts_node_type( c ), "field_identifier" ) ) { named = true; - pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( c, src ), type, {}, at, false ); + if( param ) + { + pushMemberTombstone( cx, nodeTextOf( c, src ), at ); // `item T` in `Box[T any]` + } + else + { + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( c, src ), type, {}, at, false ); + } } return true; } ); - if( !named ) + if( !named && !param ) { pushEvidenceBind( cx, LocalBindKind::MemberType, type, type, {}, at, true ); // an embedded field: its methods are promoted } } } +// FE-B: JS/TS receiver evidence — constructed / typed locals and parameters, `this.x` and TS field types, `static` members, +// prototype methods; (review B4) every other name a function binds (an untyped parameter, a destructuring, a for-in/of +// variable, a catch parameter, a reassignment), each visible where the language scopes it, and TS type parameters. +inline void captureJsEvidence( BindCtx& cx, TSNode n, const char* t, std::uint32_t at ) +{ + const Lang lang = cx.lang; + const std::string_view src = cx.src; + if( kindIs( t, "type_parameters" ) ) + { + noteTypeParameters( cx, n ); + } + else if( kindIs( t, "variable_declarator" ) ) + { + const TSNode name = fieldChild( n, NodeField::Name ); + const BindSite site = jsDeclaratorSite( n ); + if( ts_node_is_null( name ) || !kindIs( ts_node_type( name ), "identifier" ) ) + { + pushJsPatternTombstones( cx, name, site ); // `const { a, b } = o`, `const [ a ] = xs` + return; + } + const TSNode value = fieldChild( n, NodeField::Value ); + if( lang == Lang::JavaScript ) + { + pushLocalEvidence( cx, name, constructedBy( value, lang, src ), site ); + return; + } + // TS records a written or constructed class as a Type record already (the base pass: the annotation's first + // type_identifier, else ctorTypeOf); here, a declaration that names none hides an outer binding of its name, and one + // whose written type is a type parameter ties that Type record (unknown) + const TSNode ann = fieldChild( n, NodeField::Type ); + TSNode written{}; + if( !ts_node_is_null( ann ) ) + { + ChildCursor annCursor( ann ); + forEachChild( ann, annCursor.cur, [ & ]( TSNode c ) + { + if( !kindIs( ts_node_type( c ), "type_identifier" ) ) + { + return true; + } + written = c; + return false; + } ); + } + if( !ts_node_is_null( written ) ) + { + if( namesTypeParameter( cx, nodeTextOf( written, src ), at ) ) + { + pushLocalEvidence( cx, name, {}, BindSite{ at, 0u, 0u } ); + } + } + else if( ctorTypeOf( value, src ).empty() ) + { + pushLocalEvidence( cx, name, {}, site ); + } + } + else if( kindIs( t, "formal_parameters" ) ) + { + const BindSite site = siteIn( at, ts_node_parent( n ) ); // the function, arrow or method the list belongs to + for( std::uint32_t i = 0, k = ts_node_named_child_count( n ); i < k; ++i ) + { + const TSNode p = ts_node_named_child( n, i ); + const TSNode pattern = lang == Lang::TypeScript ? fieldChild( p, NodeField::Pattern ) : TSNode{}; + if( !ts_node_is_null( pattern ) && kindIs( ts_node_type( pattern ), "identifier" ) ) + { + pushLocalEvidence( cx, pattern, writtenClass( cx, fieldChild( p, NodeField::Type ), at ), site ); + } + else + { + pushJsPatternTombstones( cx, p, site ); + } + } + } + else if( kindIs( t, "arrow_function" ) ) + { + pushLocalEvidence( cx, fieldChild( n, NodeField::Parameter ), {}, siteIn( at, n ) ); // `x => …` (an identifier only) + } + else if( kindIs( t, "for_in_statement" ) ) + { + const bool lexical = jsHasToken( n, "let" ) || jsHasToken( n, "const" ); + pushJsPatternTombstones( cx, fieldChild( n, NodeField::Left ), lexical ? siteIn( at, n ) : BindSite{ at, 0u, 0u } ); + } + else if( kindIs( t, "catch_clause" ) ) + { + pushJsPatternTombstones( cx, fieldChild( n, NodeField::Parameter ), siteIn( at, n ) ); + } + else if( kindIs( t, "method_definition" ) ) + { + // `static m () {}`: the member's side of the lookup, recorded inside its body so it attributes to the member + const TSNode name = fieldChild( n, NodeField::Name ); + const TSNode body = fieldChild( n, NodeField::Body ); + bool isStatic = false; + for( std::uint32_t i = 0, k = ts_node_child_count( n ); i < k && !isStatic; ++i ) + { + isStatic = kindIs( ts_node_type( ts_node_child( n, i ) ), "static" ); + } + if( isStatic && !ts_node_is_null( name ) && !ts_node_is_null( body ) ) + { + pushEvidenceBind( cx, LocalBindKind::StaticMember, nodeTextOf( name, src ), "static", {}, ts_node_start_byte( body ), false ); + } + } + else if( lang == Lang::TypeScript && kindIs( t, "public_field_definition" ) ) + { + const TSNode name = fieldChild( n, NodeField::Name ); + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "property_identifier" ) ) + { + const std::string_view written = annotatedClass( fieldChild( n, NodeField::Type ), src ); + if( !written.empty() && namesTypeParameter( cx, written, at ) ) + { + pushMemberTombstone( cx, nodeTextOf( name, src ), at ); // `item: T` in `class Box` + return; + } + const std::string_view type = written.empty() ? constructedBy( fieldChild( n, NodeField::Value ), lang, src ) : written; + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( name, src ), type, {}, at, false ); + } + } + else if( kindIs( t, "assignment_expression" ) ) + { + const TSNode lhs = fieldChild( n, NodeField::Left ); + if( ts_node_is_null( lhs ) ) + { + return; + } + if( kindIs( ts_node_type( lhs ), "identifier" ) ) + { + pushLocalEvidence( cx, lhs, constructedBy( fieldChild( n, NodeField::Right ), lang, src ), BindSite{ at, 0u, 0u } ); // a rebinding: no span + return; + } + if( !kindIs( ts_node_type( lhs ), "member_expression" ) ) + { + return; + } + const TSNode obj = fieldChild( lhs, NodeField::Object ); + const TSNode prop = fieldChild( lhs, NodeField::Property ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( prop ) && kindIs( ts_node_type( obj ), "this" ) && kindIs( ts_node_type( prop ), "property_identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( prop, src ), constructedBy( fieldChild( n, NodeField::Right ), lang, src ), {}, at, false ); + } + // `Foo.prototype.m = function …`: the member's class, recorded INSIDE the function so the record attributes to it + const TSNode rhs = fieldChild( n, NodeField::Right ); + if( !ts_node_is_null( obj ) && kindIs( ts_node_type( obj ), "member_expression" ) && !ts_node_is_null( rhs ) + && ( kindIs( ts_node_type( rhs ), "function_expression" ) || kindIs( ts_node_type( rhs ), "function" ) || kindIs( ts_node_type( rhs ), "arrow_function" ) ) + && nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "prototype" ) + { + const TSNode ctor = fieldChild( obj, NodeField::Object ); + if( !ts_node_is_null( ctor ) && kindIs( ts_node_type( ctor ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::RecvType, {}, nodeTextOf( ctor, src ), {}, ts_node_start_byte( rhs ), true ); + } + } + } +} + +// FE-B: one node's receiver evidence: Python, JS/TS and Go, and (review B3) Java, C#, Kotlin and Swift declarations. +inline void captureReceiverEvidence( BindCtx& cx, TSNode n, const char* t ) +{ + const Lang lang = cx.lang; + const std::string_view src = cx.src; + const std::uint32_t at = ts_node_start_byte( n ); + if( lang == Lang::Python ) + { + if( kindIs( t, "typed_parameter" ) || kindIs( t, "typed_default_parameter" ) ) + { + TSNode name = fieldChild( n, NodeField::Name ); + if( ts_node_is_null( name ) && ts_node_named_child_count( n ) > 0 ) + { + name = ts_node_named_child( n, 0 ); // typed_parameter spells its identifier without a field + } + if( !ts_node_is_null( name ) && kindIs( ts_node_type( name ), "identifier" ) ) + { + pushLocalEvidence( cx, name, writtenClass( cx, fieldChild( n, NodeField::Type ), at ), BindSite{ at, 0u, 0u } ); + } + } + else if( kindIs( t, "type_parameter" ) ) + { + noteTypeParameters( cx, n ); // `def f[T]( … )` / `class C[T]:` + } + else if( kindIs( t, "import_from_statement" ) ) + { + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "aliased_import" ) ) + { + const TSNode name = fieldChild( c, NodeField::Name ); + const TSNode alias = fieldChild( c, NodeField::Alias ); + if( !ts_node_is_null( name ) && !ts_node_is_null( alias ) ) + { + pushEvidenceBind( cx, LocalBindKind::NameAlias, nodeTextOf( alias, src ), finalSegment( nodeTextOf( name, src ) ), {}, ts_node_start_byte( c ), false ); + } + } + return true; + } ); + } + else if( kindIs( t, "assignment" ) ) + { + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); + if( ts_node_is_null( lhs ) ) + { + return; + } + const char* lt = ts_node_type( lhs ); + if( kindIs( lt, "identifier" ) && !ts_node_is_null( fieldChild( n, NodeField::Type ) ) ) + { + pushLocalEvidence( cx, lhs, writtenClass( cx, fieldChild( n, NodeField::Type ), at ), BindSite{ at, 0u, 0u } ); + } + else if( kindIs( lt, "identifier" ) && !ts_node_is_null( rhs ) && kindIs( ts_node_type( rhs ), "attribute" ) ) + { + const TSNode obj = fieldChild( rhs, NodeField::Object ); + const TSNode attr = fieldChild( rhs, NodeField::Attribute ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) ) + { + pushEvidenceBind( cx, LocalBindKind::MethodAlias, nodeTextOf( lhs, src ), nodeTextOf( obj, src ), nodeTextOf( attr, src ), at, false ); + } + } + else if( kindIs( lt, "attribute" ) ) + { + const TSNode obj = fieldChild( lhs, NodeField::Object ); + const TSNode attr = fieldChild( lhs, NodeField::Attribute ); + if( !ts_node_is_null( obj ) && !ts_node_is_null( attr ) && kindIs( ts_node_type( obj ), "identifier" ) && nodeTextOf( obj, src ) == "self" ) + { + pushEvidenceBind( cx, LocalBindKind::MemberType, nodeTextOf( attr, src ), constructedBy( rhs, lang, src ), {}, at, false ); + } + } + } + capturePythonBindingTombstones( cx, n, t, at ); + return; + } + if( lang == Lang::JavaScript || lang == Lang::TypeScript ) + { + captureJsEvidence( cx, n, t, at ); + } + else if( lang == Lang::Java || lang == Lang::CSharp || lang == Lang::Kotlin || lang == Lang::Swift ) + { + captureTypedDeclEvidence( cx, n, t ); + } + else if( lang == Lang::Go ) + { + captureGoEvidence( cx, n, t, at ); + } +} + void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) { // The body below is the pass's own node step, unchanged; these aliases keep it reading against the diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 662b340a9..85fef86ca 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -312,7 +312,11 @@ constexpr std::uint32_t kCacheVersion = 29; // 29: FE-B (test/receiver // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 152; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 154; // bump on any grammar/.scm/extraction change + // 154 = lane FE-B fix round 2 (review B4): every binding form records its name with + // the bytes it is visible in (RecvType spans; an unknown class is a tombstone), and a + // written type that names a type parameter names no class. 154, not 153: 152's rich + // file tag is 153; the train renumbers if it collides. // 152 = lane FE-B fix round 1 (review B3): Java/C#/Kotlin/Swift typed parameters, locals, // fields and constructed declarations record RecvType/MemberType. 152, not 151: 150's // rich file tag is 151; the train renumbers if it collides. diff --git a/src/quality.h b/src/quality.h index ab38597cb..30da0519a 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 29; // MUST equal ingest.cpp's kCacheVersion (gated); 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 152; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 154; // MUST equal ingest.cpp's kParserVer (gated) // 150 = lane FE-B on train 26a (148 = train 26a: PR #373; 146/145 = lane FE-B builds; above 148's rich tag 149) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer // note; kIngestCacheVersionMirror 28 from FE-A, kQSnapCacheScheme 17 from refval-edges) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 9963d0052..156622ec6 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -24,7 +24,9 @@ #include "resolve.h" // includerDir; HashMap / SmallVec through it #include "smallvec.h" +#include #include +#include #include #include #include @@ -155,7 +157,16 @@ struct ReceiverEvidence HashMap memberType; // "Class#field" → field class ("" = tombstone) HashMap> embeds; // "Class" → its other bases: Go embedded classes (promotion) HashMap nameAlias; // "#local" → the class name it imports - HashMap localType; // "#var" → class ("" = tombstone) + // FE-B (review B4): one binding of a local name — the class it names ("" = unknown: it hides an outer binding of the + // name and proves nothing) and the bytes it is visible in. end == 0: a record with no span (a Python name, a reassignment, + // a base pass Type record): somewhere in its definition, extent unknown. + struct LocalFact + { + std::uint32_t start = 0; + std::uint32_t end = 0; + std::string type; + }; + HashMap> localType; // "#var" → its bindings HashMap> methodAlias; // "#var" → ( object var, method ) HashMap moduleAlias; // "#name" → module as written ("" = tombstone) std::vector> fnsByFile; // fileId → functions/methods sorted by start (enclosing walk) @@ -204,6 +215,17 @@ struct ReceiverEvidence } } + void addLocalFact( const std::string& k, const Binding& b ) + { + const bool spanned = b.spanEnd > b.spanStart; + LocalFact f{ spanned ? b.spanStart : 0u, spanned ? b.spanEnd : 0u, b.typeName }; + std::vector& facts = localType[ k ]; + if( std::none_of( facts.begin(), facts.end(), [ & ]( const LocalFact& g ) { return g.start == f.start && g.end == f.end && g.type == f.type; } ) ) + { + facts.push_back( std::move( f ) ); + } + } + void buildOwners() { const std::size_t N = ing.symbols.size(); @@ -346,7 +368,7 @@ struct ReceiverEvidence } else if( typedLocal && !b.var.empty() ) { - tombstoneInsert( localType, keyOf( from, '#', b.var ), b.typeName ); + addLocalFact( keyOf( from, '#', b.var ), b ); } else if( b.kind == LocalBindKind::MemberType && !b.var.empty() ) { @@ -393,19 +415,56 @@ struct ReceiverEvidence } return kNoNode; } - // the class a typed local names in `from` or a function enclosing it; nullptr when untyped (or tombstoned) - const std::string* typedLocal( NodeId from, std::string_view var ) const + // FE-B (review B4): the class local `var` holds at byte `site` of definition `scope`, by lexical scope — the innermost + // spanned binding that holds the site (a later start is more inner; a tie of two classes is unknown), checked against + // every binding with no span (its extent is unknown, so a disagreement is unknown). nullopt: no binding of `var` is + // visible there; "": one is, and it names no single class. + std::optional visibleLocal( NodeId scope, std::string_view var, std::uint32_t site ) const { - const std::string* hit = nullptr; - walkScopes( from, [ & ]( NodeId scope ) + const auto it = localType.find( keyOf( scope, '#', var ) ); + if( it == localType.end() ) { - const auto it = localType.find( keyOf( scope, '#', var ) ); - if( it == localType.end() ) + return std::nullopt; + } + const LocalFact* inner = nullptr; + bool innerSplit = false; + std::optional whole; + for( const LocalFact& f : it->second ) + { + if( f.end == 0 ) { - return false; + whole = ( whole.has_value() && *whole != f.type ) ? std::string_view{} : std::string_view( f.type ); + continue; } - hit = it->second.empty() ? nullptr : &it->second; - return true; + if( site < f.start || site >= f.end ) + { + continue; + } + if( inner == nullptr || f.start > inner->start || ( f.start == inner->start && f.end < inner->end ) ) + { + inner = &f; + innerSplit = false; + } + else if( f.start == inner->start && f.end == inner->end && f.type != inner->type ) + { + innerSplit = true; + } + } + if( inner == nullptr ) + { + return whole; + } + const std::string_view seen = innerSplit ? std::string_view{} : std::string_view( inner->type ); + return ( whole.has_value() && *whole != seen ) ? std::string_view{} : seen; + } + // the local `var` visible at byte `site` of `from` or of a function enclosing it (the innermost that has one) + std::optional localAt( NodeId from, std::string_view var, std::uint32_t site ) const + { + std::optional hit; + walkScopes( from, [ & ]( NodeId scope ) + { + hit = visibleLocal( scope, var, site ); + return hit.has_value(); } ); return hit; } @@ -439,20 +498,19 @@ struct ReceiverEvidence // does `from` or a function enclosing it DECLARE `name` (a parameter or a local, typed or not) — not merely bind it: Java // copies each field name onto its class's methods as a bound-only name (graph.h shadowJavaFieldsOntoMethods), which must // not hide the field itself - bool declaresLocal( NodeId from, std::string_view name ) const + bool declaresLocal( NodeId from, std::string_view name, std::uint32_t site ) const { return walkScopes( from, [ & ]( NodeId s ) { - const std::string& k = keyOf( s, '#', name ); - if( localType.contains( k ) ) + if( visibleLocal( s, name, site ).has_value() ) { - return true; + return true; // a binding of the name is visible at the site (review B4: by its span) } if( localNames == nullptr ) { return false; } - const auto it = localNames->find( k ); + const auto it = localNames->find( keyOf( s, '#', name ) ); return it != localNames->end() && ( it->second & kLocalNameDeclared ) != 0; } ) != kNoNode; } @@ -632,14 +690,14 @@ struct ReceiverEvidence superOnly = true; return callerClass( r.fromSymbol ); } - if( const std::string* t = typedLocal( r.fromSymbol, root ) ) + if( const std::optional t = localAt( r.fromSymbol, root, r.startByte ); t.has_value() && !t->empty() ) { return unalias( r.fileId, *t ); } // Java/C#/Kotlin/Swift (the implicit-receiver languages but C++, whose Rule 2b reads fields, and Ruby, whose fields // are @ivars): a bare `field.m()` inside a method is `this.field.m()` — the field the caller's class (or a base) // declares, when no parameter or local of that name hides it - if( implicitReceiverLang( r.lang ) && r.lang != Lang::Cpp && r.lang != Lang::Ruby && !declaresLocal( r.fromSymbol, root ) ) + if( implicitReceiverLang( r.lang ) && r.lang != Lang::Cpp && r.lang != Lang::Ruby && !declaresLocal( r.fromSymbol, root, r.startByte ) ) { const std::string_view owner = callerClass( r.fromSymbol ); if( const std::string_view ft = owner.empty() ? std::string_view{} : fieldOf( owner, root ); !ft.empty() ) @@ -749,8 +807,8 @@ struct ReceiverEvidence { return methodOf( callerClass( scope ), alias->second, false ); // `write = self.write`: the class's own member } - const std::string* type = typedLocal( scope, alias->first ); - return type != nullptr && methodOf( unalias( r.fileId, *type ), alias->second, false ); + const std::optional type = localAt( scope, alias->first, r.startByte ); + return type.has_value() && !type->empty() && methodOf( unalias( r.fileId, *type ), alias->second, false ); } // does module alias `root` (bound in the caller's file) name the file defining `c` diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 034da2cb4..c623a9531 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -e6abc48760ac5a37459cfb78a07ba46580d1e102d6b16212da342df4c3048773 +34d7ef2e81f633cc75efba1e90d2fe78878377966a091d0037396d06a5bd559d diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 0a34e0308..e6dff8110 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,9 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-08, lane FE-B fix round 2 (review B4): RE-DERIVED with UPDATE_GOLDEN=1 (hash 34d7ef2e81…a5bd559d). kParserVer +# 152 -> 154 (binding spans and tombstones, type parameters; 153 is 152's rich tag); quality.h's mirror with it. +# kCacheVersion and kQSnapCacheScheme unchanged. The train renumbers if needed. # 2026-10-07, lane FE-B fix round 1 (review B3): RE-DERIVED with UPDATE_GOLDEN=1 (hash e6abc48760…048773). kParserVer # 150 -> 152 (Java/C#/Kotlin/Swift declarations record RecvType/MemberType; 151 is 150's rich tag), the quality.h # mirror with it. kCacheVersion and kQSnapCacheScheme unchanged. The train renumbers if needed. diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index e32447f9a..0efe2f4e1 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -91,7 +91,17 @@ # constructed field or property — read bare (implicit this) or through this/self — and a Kotlin `val` # constructor property RESOLVE to the declared class's member (Pump.*: Tank and Barrel both define spill). # Near misses stay hedged: an untyped local (a lower-case factory), a local that hides the field, an -# interface/protocol-typed receiver with two implementors (both still listed). +# interface/protocol-typed receiver with two implementors (both still listed). Every other binding form: (B). +# (B) binding scope (review B4; scope/*: Tank and Barrel both define spill, the class's field / the outer binding is a +# Tank). (a) A binding that HIDES the field or an outer typed local never lets the call prove the field's +# class: Java for/lambda/catch/resource/instanceof/case/record-pattern variables, C# foreach/lambda/is/out +# var/case/deconstruction/catch/LINQ, Kotlin for/destructuring/lambda/let/when/catch, Swift for-in/case let/ +# tuple/closure/if-guard-while let/catch, and the same shapes where FE-B reads TS/JS/Python/Go evidence +# (arrow and catch parameters, for-of, a block const, a destructuring; a Python for/lambda/comprehension/ +# with/except/walrus/match/reassignment; a Go range/if/type-switch variable). Outside its block a local never +# lends its class (a nested block, a lambda's typed parameter), and the field (or outer local) proves again. +# (b) A generic's type parameter spelled like a class (``, `Box`, `[Tank Spiller]`, `def f[Tank]`) +# never proves that class. Kept: the field itself, a typed local, a typed loop / lambda variable's own class. # (I) implicit receiver: Java, Kotlin, C#, C++, Swift, Ruby — a bare call inside a class whose base is OUTSIDE # the tree (or that imports the name from outside) never proves an unrelated class's method. Near # misses: own members (private too), an in-repo superclass's member (the cone, and Java super.m()), @@ -293,7 +303,7 @@ P(){ # answer ROOT VERB SEL [EXTRA…] — write the verb's answer to a per-call file, its exit status beside it (FILE.rc) answer(){ local r="$1" v="$2" s="$3"; shift 3 - local f; f="$TMP/$r.$v.$( printf '%s' "$s$*" | tr '/:.,= ' '______' ).xml" + local f; f="$TMP/$( printf '%s' "$r" | tr '/' '_' ).$v.$( printf '%s' "$s$*" | tr '/:.,= ' '______' ).xml" rw "$r" "--$v=$s" "$@" >"$f"; printf '%s' "$?" >"$f.rc" printf '%s' "$f" } @@ -537,6 +547,56 @@ notproven swift callees Sources/App/Pump.swift:shadow "spill Sources/App/Pump. notproven swift callees Sources/App/Pump.swift:viaIface "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" visible swift callees Sources/App/Pump.swift:viaIface "spill Sources/App/Pump.swift:5" "spill Sources/App/Pump.swift:6" +echo "=== (B) binding scope (review B4): a binding that hides a field or an outer local, and a type parameter ===" +# hides ROOT FILE NAME OUTER BOUND FN… — at each FN's call, a binding of the receiver's name hides the field (or outer local) +# whose class defines NAME at line OUTER: that edge is never PROVEN, and the candidate at line BOUND (the binding's own +# class) stays visible, hedged or proven. Outside a binding's scope the roles swap (OUTER = the binding's class). +hides(){ + local r="$1" f="$2" n="$3" outer="$4" bound="$5" fn; shift 5 + for fn in "$@"; do + notproven "$r" callees "$f:$fn" "$n $f:$outer" + visible "$r" callees "$f:$fn" "$n $f:$bound" + done +} +J=src/app/Scope.java; C=App/Scope.cs; K=src/app/Scope.kt; W=Sources/App/Scope.swift +hides scope/java $J spill 7 8 forEachLoop lambdaParam typedLambda catchUse tryRes patternVar negatedPattern switchPattern recordPattern +hides scope/java $J spill 8 7 nestedBlock lambdaOutside # outside its block: never the binding's Barrel +hides scope/java $J spill 7 7 methodTypeParam classTypeParam typeParamField # : any Spiller, never class Tank proven +proven scope/java callees $J:fieldUse "method spill $J:7" +proven scope/java callees $J:typedLocal "method spill $J:8" +hides scope/cs $C Spill 7 8 ForEachLoop ForEachVar LambdaParam PatternVar OutVar SwitchCase Deconstruct CatchUse Linq +hides scope/cs $C Spill 8 7 NestedBlock LambdaOutside +hides scope/cs $C Spill 7 7 MethodTypeParam ClassTypeParam TypeParamField +proven scope/cs callees $C:FieldUse "method Spill $C:7" +proven scope/cs callees $C:NestedBlock "method Spill $C:7" # the field, past the block that hid it +proven scope/cs callees $C:TypedLocal "method Spill $C:8" +proven scope/cs callees $C:ForEachLoop "method Spill $C:8" # the loop variable's own declared class +hides scope/kt $K spill 5 6 forLoop forDestructure lambdaParam letParam typedLambda destructure whenSubject catchUse +hides scope/kt $K spill 6 5 nestedBlock lambdaOutside +hides scope/kt $K spill 5 5 classTypeParam typeParamField +proven scope/kt callees $K:fieldUse "fn spill $K:5" +proven scope/kt callees $K:typedLocal "fn spill $K:6" +proven scope/kt callees $K:typedLambda "fn spill $K:6" +hides scope/swift $W spill 3 4 forLoop forCaseLet forTuple closureParam typedClosure ifLet guardLet whileLet caseLet ifCaseLet catchLet +hides scope/swift $W spill 4 3 nestedBlock +hides scope/swift $W spill 3 3 classTypeParam typeParamField +proven scope/swift callees $W:fieldUse "fn spill $W:3" +proven scope/swift callees $W:typedLocal "fn spill $W:4" +echo "--- (B) the same shapes where FE-B reads TS / JS / Python / Go evidence (an outer typed or constructed binding hidden)" +hides scope/ts scope.ts spill 4 5 arrowParam forOf blockLocal catchParam destructure +hides scope/ts scope.ts spill 4 4 typeParam classTypeParam typeParamField +proven scope/ts callees scope.ts:typedParam "method spill scope.ts:4" +hides scope/js scope.js spill 2 3 arrowParam forOf catchParam +proven scope/js callees scope.js:blockOutside "method spill scope.js:2" # the outer local, past the block that hid it +proven scope/js callees scope.js:constructed "method spill scope.js:2" +hides scope/py scope.py spill 3 6 for_loop lambda_param comprehension with_as except_as walrus reassign match_capture match_as +hides scope/py scope.py spill 3 3 type_param +proven scope/py callees scope.py:typed_param "fn spill scope.py:3" +hides scope/go scope.go Spill 8 12 RangeLoop ShortIf TypeSwitch +hides scope/go scope.go Spill 8 8 TypeParam TypeParamField +proven scope/go callees scope.go:BlockOutside "method Spill scope.go:8" +proven scope/go callees scope.go:TypedParam "method Spill scope.go:8" + echo "=== (I) implicit receiver: a bare call never proves an unrelated class's method ===" notproven java callees src/main/java/app/Logger.java:line "render src/main/java/app/Exporter.java" notproven java callees src/main/java/app/Logger.java:close "flush src/main/java/app/Buffer.java" "flush src/main/java/app/Exporter.java" diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md index c01ae056c..f66f2a7a2 100644 --- a/test/receiverevidencefix/README.md +++ b/test/receiverevidencefix/README.md @@ -45,3 +45,10 @@ its static (companion) members, a call on an instance the others. A fallback hoo (py/src/tui/ghost.py): `__getattr__` leaves a defined member's call resolved. The names are paraphrases of graded false rows; the code is minimal and is never built. +- `scope/{java,cs,kt,swift,ts,js,py,go}` — review B4: Tank and Barrel both define `spill`, and the class's field (or the + function's outer parameter / local) named `tank` is a Tank. Each method binds `tank` again — a loop, lambda, catch, + resource, pattern, `case`, `out var`, destructuring, `if`/`guard`/`while let`, `with`/`except`/walrus/`match`, a Go + range / if / type-switch variable, a reassignment — and calls `tank.spill()`, which must never prove Tank's spill; + `nestedBlock` / `lambdaOutside` / `blockOutside` call it past the block that hid it, which must never prove Barrel's. A + generic whose type parameter is spelled `Tank` (method and class, a field of that type) must never prove class Tank. + Kept: the field itself, a typed local, a typed loop / lambda variable's own class, a typed parameter. diff --git a/test/receiverevidencefix/scope/cs/App/Scope.cs b/test/receiverevidencefix/scope/cs/App/Scope.cs new file mode 100644 index 000000000..a84ccbbaf --- /dev/null +++ b/test/receiverevidencefix/scope/cs/App/Scope.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Collections.Generic; +namespace App { + // Review B4: Tank and Barrel both define Spill(), and the class's field `tank` is a Tank. + public interface ISpiller { void Spill(); } + public class Tank : ISpiller { public void Spill() {} } + public class Barrel : ISpiller { public void Spill() {} } + + // (a) a binding that hides the field, and a local seen outside its own block; (b) a type parameter named like a class + public class Scope { + private Tank tank = new Tank(); + public void ForEachLoop(List bs) { foreach (Barrel tank in bs) { tank.Spill(); } } + public void ForEachVar(List bs) { foreach (var tank in bs) { tank.Spill(); } } + public void LambdaParam(List bs) { bs.ForEach(tank => tank.Spill()); } + public void PatternVar(object o) { if (o is Barrel tank) { tank.Spill(); } } + public void OutVar(Dictionary d) { if (d.TryGetValue(1, out var tank)) { tank.Spill(); } } + public void SwitchCase(object o) { switch (o) { case Barrel tank: tank.Spill(); break; } } + public void Deconstruct((Barrel, Barrel) p) { var (tank, other) = p; tank.Spill(); } + public void CatchUse() { try { } catch (Exception tank) { tank.Spill(); } } + public void Linq(List bs) { var q = from tank in bs select tank.Spill(); } + public void NestedBlock(bool b) { if (b) { Barrel tank = new Barrel(); } tank.Spill(); } + public void LambdaOutside(List bs) { bs.ForEach((Barrel tank) => { }); tank.Spill(); } + public void FieldUse() { tank.Spill(); } + public void TypedLocal() { Barrel tank = new Barrel(); tank.Spill(); } + public void MethodTypeParam(Tank t) where Tank : ISpiller { t.Spill(); } + } + + public class Box where Tank : ISpiller { + private Tank item; + public void ClassTypeParam(Tank t) { t.Spill(); } + public void TypeParamField() { item.Spill(); } + } +} diff --git a/test/receiverevidencefix/scope/go/go.mod b/test/receiverevidencefix/scope/go/go.mod new file mode 100644 index 000000000..82330bddf --- /dev/null +++ b/test/receiverevidencefix/scope/go/go.mod @@ -0,0 +1,3 @@ +module example.com/scope + +go 1.22 diff --git a/test/receiverevidencefix/scope/go/scope.go b/test/receiverevidencefix/scope/go/scope.go new file mode 100644 index 000000000..af323090e --- /dev/null +++ b/test/receiverevidencefix/scope/go/scope.go @@ -0,0 +1,42 @@ +// Review B4 (language neutrality): a binding that hides a typed parameter of its name, and a type parameter named like +// a type. Tank and Barrel both define Spill. +package scope + +type Spiller interface{ Spill() } +type Tank struct{} + +func (Tank) Spill() {} + +type Barrel struct{} + +func (Barrel) Spill() {} + +func RangeLoop(tank Tank, bs []Barrel) { + for _, tank := range bs { + tank.Spill() + } +} +func ShortIf(tank Tank, b Barrel) { + if tank := b; true { + tank.Spill() + } +} +func TypeSwitch(tank Tank, o any) { + switch tank := o.(type) { + case Barrel: + tank.Spill() + } +} +func BlockOutside(tank Tank, b Barrel) { + { + tank := b + _ = tank + } + tank.Spill() +} +func TypeParam[Tank Spiller](t Tank) { t.Spill() } + +type Box[Tank Spiller] struct{ item Tank } + +func (g Box[Tank]) TypeParamField() { g.item.Spill() } +func TypedParam(t Tank) { t.Spill() } diff --git a/test/receiverevidencefix/scope/java/src/app/Scope.java b/test/receiverevidencefix/scope/java/src/app/Scope.java new file mode 100644 index 000000000..a89ca4e79 --- /dev/null +++ b/test/receiverevidencefix/scope/java/src/app/Scope.java @@ -0,0 +1,35 @@ +package app; + +import java.util.List; + +// Review B4: Tank and Barrel both define spill(), and the class's field `tank` is a Tank. +interface Spiller { void spill(); } +class Tank implements Spiller { public void spill() {} } +class Barrel implements Spiller { public void spill() {} } +record P(Barrel b) {} + +// (a) a binding that hides the field — a loop, lambda, catch or resource variable, a pattern variable — and a local seen +// outside its own block; (b) a generic's type parameter named like a class. Kept: the field, a typed local. +class Scope { + private Tank tank = new Tank(); + void forEachLoop(List bs) { for (Barrel tank : bs) { tank.spill(); } } + void lambdaParam(List bs) { bs.forEach(tank -> tank.spill()); } + void typedLambda(List bs) { bs.forEach((Barrel tank) -> tank.spill()); } + void catchUse() { try { } catch (RuntimeException tank) { tank.spill(); } } + void tryRes() throws Exception { try (Barrel tank = new Barrel()) { tank.spill(); } } + void patternVar(Object o) { if (o instanceof Barrel tank) { tank.spill(); } } + void negatedPattern(Object o) { if (!(o instanceof Barrel tank)) return; tank.spill(); } + void switchPattern(Object o) { switch (o) { case Barrel tank -> tank.spill(); default -> {} } } + void recordPattern(Object o) { if (o instanceof P(Barrel tank)) { tank.spill(); } } + void nestedBlock(boolean b) { if (b) { Barrel tank = new Barrel(); } tank.spill(); } + void lambdaOutside(List bs) { bs.forEach((Barrel tank) -> { }); tank.spill(); } + void fieldUse() { tank.spill(); } + void typedLocal() { Barrel tank = new Barrel(); tank.spill(); } + void methodTypeParam(Tank t) { t.spill(); } +} + +class Box { + Tank item; + void classTypeParam(Tank t) { t.spill(); } + void typeParamField() { item.spill(); } +} diff --git a/test/receiverevidencefix/scope/js/scope.js b/test/receiverevidencefix/scope/js/scope.js new file mode 100644 index 000000000..ee7aadb0e --- /dev/null +++ b/test/receiverevidencefix/scope/js/scope.js @@ -0,0 +1,8 @@ +// Review B4 (language neutrality): a binding that hides an outer constructed local of its name. +export class Tank { spill() {} } +export class Barrel { spill() {} } +export function arrowParam(bs) { const tank = new Tank(); bs.forEach((tank) => tank.spill()); } +export function forOf(bs) { const tank = new Tank(); for (const tank of bs) { tank.spill(); } } +export function catchParam() { const tank = new Tank(); try { } catch (tank) { tank.spill(); } } +export function blockOutside(b) { const tank = new Tank(); { const tank = b; } tank.spill(); } +export function constructed() { const t = new Tank(); t.spill(); } diff --git a/test/receiverevidencefix/scope/kt/src/app/Scope.kt b/test/receiverevidencefix/scope/kt/src/app/Scope.kt new file mode 100644 index 000000000..79c39d70a --- /dev/null +++ b/test/receiverevidencefix/scope/kt/src/app/Scope.kt @@ -0,0 +1,28 @@ +package app + +// Review B4: Tank and Barrel both define spill(), and the class's field `tank` is a Tank. +interface Spiller { fun spill() } +class Tank : Spiller { override fun spill() {} } +class Barrel : Spiller { override fun spill() {} } + +// (a) a binding that hides the field, and a local seen outside its own block; (b) a type parameter named like a class +class Scope { + private val tank: Tank = Tank() + fun forLoop(bs: List) { for (tank in bs) { tank.spill() } } + fun forDestructure(ps: List>) { for ((tank, o) in ps) { tank.spill() } } + fun lambdaParam(bs: List) { bs.forEach { tank -> tank.spill() } } + fun letParam(b: Barrel?) { b?.let { tank -> tank.spill() } } + fun typedLambda(bs: List) { bs.forEach { tank: Barrel -> tank.spill() } } + fun destructure(p: Pair) { val (tank, other) = p; tank.spill() } + fun whenSubject(o: Any) { when (val tank = o) { is Barrel -> tank.spill() } } + fun catchUse() { try { } catch (tank: Exception) { tank.spill() } } + fun nestedBlock(b: Boolean) { if (b) { val tank = Barrel() }; tank.spill() } + fun lambdaOutside(bs: List) { bs.forEach { tank: Barrel -> }; tank.spill() } + fun fieldUse() { tank.spill() } + fun typedLocal() { val tank: Barrel = Barrel(); tank.spill() } +} + +class Box(val item: Tank) { + fun classTypeParam(t: Tank) { t.spill() } + fun typeParamField() { item.spill() } +} diff --git a/test/receiverevidencefix/scope/py/scope.py b/test/receiverevidencefix/scope/py/scope.py new file mode 100644 index 000000000..757e9f502 --- /dev/null +++ b/test/receiverevidencefix/scope/py/scope.py @@ -0,0 +1,50 @@ +# Review B4 (language neutrality): a rebinding of a typed parameter's name, and a type parameter named like a class. +class Tank: + def spill(self): pass + +class Barrel: + def spill(self): pass + +def for_loop(tank: Tank, bs: list): + for tank in bs: + tank.spill() + +def lambda_param(tank: Tank, bs): + return list(map(lambda tank: tank.spill(), bs)) + +def comprehension(tank: Tank, bs): + return [tank.spill() for tank in bs] + +def with_as(tank: Tank, cm): + with cm as tank: + tank.spill() + +def except_as(tank: Tank): + try: + pass + except Exception as tank: + tank.spill() + +def walrus(tank: Tank, f): + if (tank := f()): + tank.spill() + +def reassign(tank: Tank, b): + tank = b + tank.spill() + +def type_param[Tank](t: Tank): + t.spill() + +def typed_param(t: Tank): + t.spill() + +def match_capture(tank: Tank, o): + match o: + case [tank, *rest]: + tank.spill() + +def match_as(tank: Tank, o): + match o: + case Barrel() as tank: + tank.spill() diff --git a/test/receiverevidencefix/scope/swift/Sources/App/Scope.swift b/test/receiverevidencefix/scope/swift/Sources/App/Scope.swift new file mode 100644 index 000000000..7f442aba0 --- /dev/null +++ b/test/receiverevidencefix/scope/swift/Sources/App/Scope.swift @@ -0,0 +1,31 @@ +// Review B4: Tank and Barrel both define spill(), and the class's field `tank` is a Tank. +protocol Spiller { func spill() } +class Tank: Spiller { func spill() {} } +class Barrel: Spiller { func spill() {} } +enum Cask { case b(Barrel) } + +// (a) a binding that hides the field, and a local seen outside its own block; (b) a type parameter named like a class +class Scope { + var tank: Tank = Tank() + func forLoop(_ bs: [Barrel]) { for tank in bs { tank.spill() } } + func forCaseLet(_ bs: [Barrel?]) { for case let tank? in bs { tank.spill() } } + func forTuple(_ ps: [(Barrel, Barrel)]) { for (tank, _) in ps { tank.spill() } } + func closureParam(_ bs: [Barrel]) { bs.forEach { tank in tank.spill() } } + func typedClosure(_ bs: [Barrel]) { bs.forEach { (tank: Barrel) in tank.spill() } } + func ifLet(_ b: Barrel?) { if let tank = b { tank.spill() } } + func guardLet(_ b: Barrel?) { guard let tank = b else { return }; tank.spill() } + func whileLet(_ it: inout IndexingIterator<[Barrel]>) { while let tank = it.next() { tank.spill() } } + func caseLet(_ x: Cask) { switch x { case .b(let tank): tank.spill() } } + func ifCaseLet(_ x: Cask) { if case .b(let tank) = x { tank.spill() } } + func catchLet() { do { } catch let tank { tank.spill() } } + func nestedBlock(_ b: Bool) { if b { let tank = Barrel(); _ = tank }; tank.spill() } + func fieldUse() { tank.spill() } + func typedLocal() { let tank: Barrel = Barrel(); tank.spill() } +} + +class Box { + var item: Tank + init(item: Tank) { self.item = item } + func classTypeParam(_ t: Tank) { t.spill() } + func typeParamField() { item.spill() } +} diff --git a/test/receiverevidencefix/scope/ts/scope.ts b/test/receiverevidencefix/scope/ts/scope.ts new file mode 100644 index 000000000..29ac09d81 --- /dev/null +++ b/test/receiverevidencefix/scope/ts/scope.ts @@ -0,0 +1,18 @@ +// Review B4 (language neutrality): a binding that hides an outer typed binding of its name, and a type parameter named +// like a class. Tank and Barrel both define spill(). +interface Spiller { spill(): void } +export class Tank implements Spiller { spill() {} } +export class Barrel implements Spiller { spill() {} } +export function arrowParam(tank: Tank, bs: Barrel[]) { bs.forEach((tank) => tank.spill()); } +export function forOf(tank: Tank, bs: Barrel[]) { for (const tank of bs) { tank.spill(); } } +export function blockLocal(tank: Tank, b: Barrel) { { const tank = b; tank.spill(); } } +export function catchParam(tank: Tank) { try { } catch (tank) { tank.spill(); } } +export function destructure(tank: Tank, p: [Barrel, Barrel]) { { const [tank] = p; tank.spill(); } } +export function typeParam(t: Tank) { t.spill(); } +export class Box { + item: Tank; + constructor(item: Tank) { this.item = item; } + classTypeParam(t: Tank) { t.spill(); } + typeParamField() { this.item.spill(); } +} +export function typedParam(t: Tank) { t.spill(); } From 72d8cc7c15ec45a7fd7dc837b45e3e55f3bfff1d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:44:57 -0400 Subject: [PATCH 177/229] acks: restore main's A6 api-surface line removed in fix round 1 Co-Authored-By: Claude Opus 5.5 --- .ripwire_quality_acks | 1 + 1 file changed, 1 insertion(+) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 4f249ea22..4c9744ed6 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -96,6 +96,7 @@ ack api-surface 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: ack api-surface 77eb2194156050d0 4 cid=ae974822986c10b4 lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. ack api-surface 78d2e234da12136c 9 cid=f882c119f01e545e F4: --recall's --token-budget ceiling travels to the header through RecallShape, so buildRecall/recallFor each take one more argument (8->9). Both are defaulted; both call sites updated; the alternative was a second estimate of the same number beside the one the root prints. ack api-surface 79b6230346ec0208 5 cid=f936b7f7eca7e9bb P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). +ack api-surface 7b4ba1b688c902ab 7 cid=12665f9a238a04cc by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 7c0e20a9156f7379 10 cid=c070b25dc349431d capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface 7c0e356e60b323ba 3 cid=4451e903bc990da2 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate From c5feeca352dff3a715add5a6e37adce25ca90943 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:51:22 -0400 Subject: [PATCH 178/229] refactor: kindIn names a node-kind set once; B4 fixtures reshaped off a token-shape coincidence Co-Authored-By: Claude Opus 5.5 --- src/ingest_binds.h | 31 +++++++++++++------ .../receiverevidencefix/scope/cs/App/Scope.cs | 2 +- .../scope/java/src/app/Scope.java | 4 +-- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/ingest_binds.h b/src/ingest_binds.h index a69abf79a..90aee6a2d 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -4109,6 +4109,17 @@ struct BindCtx }; // ── FE-B receiver-evidence capture (model.h LocalBindKind::RecvType / MemberType / MethodAlias) ────────────────────────── +// is node kind `t` one of `kinds` +inline bool kindIn( const char* t, std::initializer_list kinds ) noexcept +{ + return std::find( kinds.begin(), kinds.end(), std::string_view( t ) ) != kinds.end(); +} +// an identifier in any grammar's spelling +inline bool identifierKind( const char* t ) noexcept +{ + return kindIn( t, { "identifier", "type_identifier", "simple_identifier" } ); +} + // The class a written TYPE names, final segment — "" for anything that names no single class (an array, a union, a // string annotation, a generic's argument). Unwraps the annotation wrappers each grammar puts around the type. inline std::string_view annotatedClass( TSNode node, std::string_view src ) @@ -4116,7 +4127,7 @@ inline std::string_view annotatedClass( TSNode node, std::string_view src ) for( int guard = 0; guard < 8 && !ts_node_is_null( node ); ++guard ) { const char* t = ts_node_type( node ); - if( kindIs( t, "identifier" ) || kindIs( t, "type_identifier" ) || kindIs( t, "simple_identifier" ) ) + if( identifierKind( t ) ) { return nodeTextOf( node, src ); } @@ -4223,10 +4234,6 @@ inline std::string_view constructedBy( TSNode value, Lang lang, std::string_view return constructedClass( value, lang, src ); } -inline bool identifierKind( const char* t ) noexcept -{ - return kindIs( t, "identifier" ) || kindIs( t, "type_identifier" ) || kindIs( t, "simple_identifier" ); -} // FE-B (review B4b): the names one type parameter declares, visible over [start, end): the leading identifier(s) after any // annotation, modifier or variance (Java/C#/Kotlin/Swift/TS `T`, Go `A, B any`), or Python 3.12's `type`-wrapped name. @@ -4316,8 +4323,14 @@ inline void noteGoReceiverTypeParameters( BindCtx& cx, TSNode method ) // is `name`, written at byte `at`, a type parameter of a generic that encloses it inline bool namesTypeParameter( const BindCtx& cx, std::string_view name, std::uint32_t at ) noexcept { - return std::any_of( cx.typeParams.begin(), cx.typeParams.end(), - [ & ]( const TypeParamScope& p ) { return p.name == name && p.start <= at && at < p.end; } ); + for( const TypeParamScope& p : cx.typeParams ) + { + if( at >= p.start && at < p.end && p.name == name ) + { + return true; + } + } + return false; } // the class a written type names at byte `at` — "" when it names none, or names a type parameter (` … Tank t`) @@ -4350,7 +4363,7 @@ inline TSNode ancestorOfKind( TSNode n, std::initializer_list TSNode p = ts_node_is_null( n ) ? TSNode{} : ts_node_parent( n ); for( int guard = 0; guard < 64 && !ts_node_is_null( p ); ++guard ) { - if( std::find( kinds.begin(), kinds.end(), std::string_view( ts_node_type( p ) ) ) != kinds.end() ) + if( kindIn( ts_node_type( p ), kinds ) ) { return p; } @@ -4474,7 +4487,7 @@ inline TSNode namedChildOfKind( TSNode node, std::initializer_list bs) { foreach (var tank in bs) { tank.Spill(); } } public void LambdaParam(List bs) { bs.ForEach(tank => tank.Spill()); } public void PatternVar(object o) { if (o is Barrel tank) { tank.Spill(); } } - public void OutVar(Dictionary d) { if (d.TryGetValue(1, out var tank)) { tank.Spill(); } } + public void OutVar(Dictionary d) { var found = d.TryGetValue(1, out var tank); if (found) tank.Spill(); } public void SwitchCase(object o) { switch (o) { case Barrel tank: tank.Spill(); break; } } public void Deconstruct((Barrel, Barrel) p) { var (tank, other) = p; tank.Spill(); } public void CatchUse() { try { } catch (Exception tank) { tank.Spill(); } } diff --git a/test/receiverevidencefix/scope/java/src/app/Scope.java b/test/receiverevidencefix/scope/java/src/app/Scope.java index a89ca4e79..33a67463c 100644 --- a/test/receiverevidencefix/scope/java/src/app/Scope.java +++ b/test/receiverevidencefix/scope/java/src/app/Scope.java @@ -5,7 +5,7 @@ // Review B4: Tank and Barrel both define spill(), and the class's field `tank` is a Tank. interface Spiller { void spill(); } class Tank implements Spiller { public void spill() {} } -class Barrel implements Spiller { public void spill() {} } +class Barrel implements Spiller, AutoCloseable { public void spill() {} public void close() {} } record P(Barrel b) {} // (a) a binding that hides the field — a loop, lambda, catch or resource variable, a pattern variable — and a local seen @@ -16,7 +16,7 @@ class Scope { void lambdaParam(List bs) { bs.forEach(tank -> tank.spill()); } void typedLambda(List bs) { bs.forEach((Barrel tank) -> tank.spill()); } void catchUse() { try { } catch (RuntimeException tank) { tank.spill(); } } - void tryRes() throws Exception { try (Barrel tank = new Barrel()) { tank.spill(); } } + void tryRes() throws Exception { try (Barrel tank = new Barrel()) { tank.spill(); tank.close(); } } void patternVar(Object o) { if (o instanceof Barrel tank) { tank.spill(); } } void negatedPattern(Object o) { if (!(o instanceof Barrel tank)) return; tank.spill(); } void switchPattern(Object o) { switch (o) { case Barrel tank -> tank.spill(); default -> {} } } From 3cf2122f816a53e6b2808eeb43bb44aa75ad2fe9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:55:48 -0400 Subject: [PATCH 179/229] refactor: identifier kinds as one table; a type parameter scope answers whether it declares a name Co-Authored-By: Claude Opus 5.5 --- src/ingest_binds.h | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 90aee6a2d..926eda766 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -4088,6 +4088,8 @@ struct TypeParamScope std::string_view name; std::uint32_t start = 0; std::uint32_t end = 0; + + bool declares( std::string_view spelled, std::uint32_t at ) const noexcept { return name == spelled && start <= at && at < end; } }; struct BindCtx @@ -4109,15 +4111,11 @@ struct BindCtx }; // ── FE-B receiver-evidence capture (model.h LocalBindKind::RecvType / MemberType / MethodAlias) ────────────────────────── -// is node kind `t` one of `kinds` -inline bool kindIn( const char* t, std::initializer_list kinds ) noexcept -{ - return std::find( kinds.begin(), kinds.end(), std::string_view( t ) ) != kinds.end(); -} // an identifier in any grammar's spelling +inline constexpr std::array kIdentifierKinds = { "identifier", "type_identifier", "simple_identifier" }; inline bool identifierKind( const char* t ) noexcept { - return kindIn( t, { "identifier", "type_identifier", "simple_identifier" } ); + return std::ranges::find( kIdentifierKinds, std::string_view( t ) ) != kIdentifierKinds.end(); } // The class a written TYPE names, final segment — "" for anything that names no single class (an array, a union, a @@ -4323,14 +4321,7 @@ inline void noteGoReceiverTypeParameters( BindCtx& cx, TSNode method ) // is `name`, written at byte `at`, a type parameter of a generic that encloses it inline bool namesTypeParameter( const BindCtx& cx, std::string_view name, std::uint32_t at ) noexcept { - for( const TypeParamScope& p : cx.typeParams ) - { - if( at >= p.start && at < p.end && p.name == name ) - { - return true; - } - } - return false; + return std::ranges::any_of( cx.typeParams, [ & ]( const TypeParamScope& p ) { return p.declares( name, at ); } ); } // the class a written type names at byte `at` — "" when it names none, or names a type parameter (` … Tank t`) @@ -4363,7 +4354,7 @@ inline TSNode ancestorOfKind( TSNode n, std::initializer_list TSNode p = ts_node_is_null( n ) ? TSNode{} : ts_node_parent( n ); for( int guard = 0; guard < 64 && !ts_node_is_null( p ); ++guard ) { - if( kindIn( ts_node_type( p ), kinds ) ) + if( std::ranges::find( kinds, std::string_view( ts_node_type( p ) ) ) != kinds.end() ) { return p; } @@ -4487,7 +4478,7 @@ inline TSNode namedChildOfKind( TSNode node, std::initializer_list Date: Thu, 8 Oct 2026 02:56:28 -0400 Subject: [PATCH 180/229] skills: the quality-bar trigger text keeps its scope guard ("single-line leaf fix") beside the format-arity exception CI's agentloopcodexcheck asserts the guard phrase in the frontmatter; the description is reworded to carry both within the 5,400-character set ceiling. The defect-shape gate's phrase arm reads whitespace-normalized text, so a sentence that wraps across lines counts the same. Co-Authored-By: Claude Opus 5.5 --- skills/ripwire-quality-bar/SKILL.md | 6 +++--- test/defectshapecheck.sh | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/skills/ripwire-quality-bar/SKILL.md b/skills/ripwire-quality-bar/SKILL.md index 7eb4ed4e1..86aded233 100644 --- a/skills/ripwire-quality-bar/SKILL.md +++ b/skills/ripwire-quality-bar/SKILL.md @@ -2,9 +2,9 @@ name: ripwire-quality-bar description: > Code QUALITY of what YOU just wrote, before commit or 'done', or a cleanup: --quality-delta - lists what got WORSE in 12 kinds, exit 2 if old code got materially worse (new-symbol rows - never gate, except defect-shape format-arity); which fix a measured shape (humps/deep, tangle) - calls for. Merge safety → change-check. Even one-line fixes. + lists what got WORSE in 12 kinds, exit 2 if old code got worse (new-symbol rows never gate, + except defect-shape format-arity); the fix a measured shape (humps/deep, tangle) calls for. + Merge safety → change-check. Even a single-line leaf fix runs it. allowed-tools: Bash, Read --- diff --git a/test/defectshapecheck.sh b/test/defectshapecheck.sh index 4e7e723de..bdf6eae6e 100644 --- a/test/defectshapecheck.sh +++ b/test/defectshapecheck.sh @@ -1256,9 +1256,10 @@ gating_split "$L" 0 L="surfaces" PHRASE='new-symbol rows never gate, except defect-shape format-arity' phrase_ok(){ # LABEL TEXT — the text states the exception, and every "new-symbol rows never gate" in it carries it - local all qual - all="$( printf '%s' "$2" | grep -o 'new-symbol rows never gate' | wc -l | tr -d ' ' )" - qual="$( printf '%s' "$2" | grep -o "$PHRASE" | wc -l | tr -d ' ' )" + local all qual flat # whitespace-normalized first: a sentence may wrap anywhere in a document + flat="$( printf '%s' "$2" | tr '\n' ' ' | tr -s ' ' )" + all="$( printf '%s' "$flat" | grep -o 'new-symbol rows never gate' | wc -l | tr -d ' ' )" + qual="$( printf '%s' "$flat" | grep -o "$PHRASE" | wc -l | tr -d ' ' )" if [ "$qual" -ge 1 ] && [ "$all" = "$qual" ]; then ok "$1: states '$PHRASE' ($qual), no unqualified 'new-symbol rows never gate'" else no "$1: '$PHRASE' x$qual, 'new-symbol rows never gate' x$all — the exception is missing or a sentence still reads unqualified"; fi } From 398899e75d911d30dfb2df774ffa31c1490c5bf3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 02:59:34 -0400 Subject: [PATCH 181/229] docs: regenerate the showcase capture and COMMANDS.md for the definitions-first default The capture is the output of test/showcase_capture.py at c7920353 (clean tree), which now records the default --whereis=rankGraphTeleport as the defs page (60 definitions) with the contrast block --whereis-listing=all; docs/COMMANDS.md is rebuilt from --help and that capture with docs/docs_commands_build.py. The 2026-10-07 capture is superseded and removed. Co-Authored-By: Claude Sonnet 5.5 --- docs/COMMANDS.md | 542 +++--- ...-07.md => COMMANDS_showcase_2026-10-08.md} | 1552 ++++++++--------- 2 files changed, 978 insertions(+), 1116 deletions(-) rename docs/captures/{COMMANDS_showcase_2026-10-07.md => COMMANDS_showcase_2026-10-08.md} (93%) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index f6702ea2c..4b314f5df 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,7 @@ python3 docs/docs_commands_build.py --bin build/ripwire The flag surface below is read from `ripwire --help`, so it cannot disagree with the shipped binary. `test/docscommandscheck.sh` fails if it ever does — in either direction. -Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-07.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-08.md`), trimmed to the first few lines and scrubbed of local paths. It is illustrative, not a golden: run the command yourself for the current shape. @@ -70,7 +70,7 @@ _Same map, capped to the 5 highest-ranked symbols._ $ ./build/ripwire . --top-k=5 - + @@ -106,7 +106,7 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ $ ./build/ripwire . --max-tokens=1500 - + @@ -181,7 +181,7 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - + @@ -218,7 +218,7 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - + @@ -247,7 +247,7 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - + @@ -279,7 +279,7 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + @@ -313,7 +313,7 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - + @@ -346,7 +346,7 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - + @@ -379,7 +379,7 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - @@ -411,11 +411,11 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) @@ -442,7 +442,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -472,7 +472,7 @@ _The repo's best-in-class instance to imitate before writing new code (picked by $ ./build/ripwire . --exemplar="format byte sizes for humans" - + f, A&&... a ) @@ -503,7 +503,7 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ $ ./build/ripwire . --recall="quality delta gating exit codes" ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.969) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 7.970) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -566,20 +566,25 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1399178-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1411912-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` -$ ./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 +$ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 -━━ commands.md (relevance 10.211) ━━ [sections: 1 of 157 selected (187 in doc), section-granular; whole doc 533991 B; lines="3700-3704"; dropped_by_budget=156] [truncated: 1865 of 7739 bytes] -### `--field-affinity[=STRUCT]` +━━ commands.md (relevance 11.013) ━━ [sections: 1 of 159 selected (187 in doc), section-granular; whole doc 546809 B; lines="528-553"; dropped_by_budget=158] [truncated: 1854 of 4293 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base -**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? -Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +... [17 more line(s); run it to see the whole thing] ``` ### `--tree` @@ -629,7 +634,7 @@ A navigation or report verb answers instead of the map, so --html beside one ref _Self-contained HTML force-directed call graph._ ``` -$ ./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html +$ ./build/ripwire . --html=/aux/map2.html (empty) ``` @@ -650,7 +655,7 @@ the flag only sets the initial mode _The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector)._ ``` -$ ./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html --color-by=community +$ ./build/ripwire . --html=/aux/map2.html --color-by=community (empty) ``` @@ -699,7 +704,7 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable $ ./build/ripwire . --no-stable --top-k=3 - + @@ -731,7 +736,7 @@ $ ./build/ripwire . --around=rankGraphTeleport - + @@ -782,7 +787,7 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - + @@ -813,7 +818,7 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + @@ -847,7 +852,7 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + @@ -884,15 +889,15 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - + + - + - + @@ -919,7 +924,7 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - + @@ -941,7 +946,7 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + @@ -1051,7 +1056,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1113,7 +1118,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 75cc95803 +at: c7920353a (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1132,18 +1137,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1192,7 +1197,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - + - + @@ -1494,7 +1499,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1531,7 +1536,7 @@ $ ./build/ripwire . --pack-signatures --top-k=10 - + @@ -1729,7 +1734,7 @@ _Fan-in/out + complexity annotations on the map._ $ ./build/ripwire . --metrics --top-k=10 - + @@ -1765,10 +1770,10 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - + + - + @@ -1802,13 +1807,13 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - + - - + + @@ -1830,7 +1835,7 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + @@ -1861,7 +1866,7 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - + @@ -1894,7 +1899,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1929,7 +1934,7 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - @@ -1960,7 +1965,7 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - @@ -1997,7 +2002,7 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - @@ -2028,7 +2033,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2063,7 +2068,7 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - + @@ -2100,7 +2105,7 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - + @@ -2135,7 +2140,7 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + @@ -2168,16 +2173,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - - + + + - - - + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2200,16 +2205,16 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - - - + - + + - - - + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2226,7 +2231,7 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + @@ -2234,9 +2239,9 @@ $ ./build/ripwire . --cochange --cochange-groups - + - + ... [17 more line(s); run it to see the whole thing] @@ -2255,17 +2260,17 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - + + + + + + - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -2376,7 +2381,7 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - + @@ -2571,15 +2576,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + - + @@ -2602,7 +2607,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2627,11 +2632,11 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - + - + @@ -2664,16 +2669,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2484 files · 23849 symbols · 34232 edges · 2322 modules (10910 call-graph isolated) +2484 files · 23851 symbols · 34236 edges · 2324 modules (10910 call-graph isolated) Root: `.` Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 2322) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1378 symbols -- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1136 symbols -- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 538 symbols +## Modules (call-graph clusters; showing 12 of 2324) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1374 symbols +- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1135 symbols +- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 534 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2690,13 +2695,13 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - + - + - + @@ -2725,7 +2730,7 @@ flowchart LR n102["src/infra
1276"] end subgraph sg1 ["test"] - n103["test
4851"] + n103["test
4853"] n328["test/fixtures/recallpassage
314"] n280["test/expandmodefix
151"] n282["test/expandsibsfix
149"] @@ -2750,7 +2755,7 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - + @@ -2758,7 +2763,7 @@ $ ./build/ripwire . --owners - + @@ -2781,7 +2786,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2846,7 +2851,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2883,7 +2888,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2920,7 +2925,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -2990,7 +2995,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3020,7 +3025,7 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - @@ -3050,7 +3055,7 @@ disclosed as trailing_newline_folded=true; a second one, a deliberate blank line _An unknown TARGET refuses and leaves every file byte-identical._ ``` -$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h +$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h (empty) ``` @@ -3069,17 +3074,17 @@ $ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tm _Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._ ``` -$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h +$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3093,14 +3098,14 @@ Both inserts PAD the block (newlines only, never removed) so it is separated fro _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call._ ``` -$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h +$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3120,7 +3125,7 @@ empty payloads refuse, never imply deletion _An unknown TARGET refuses and leaves every file byte-identical._ ``` -$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h +$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h (empty) ``` @@ -3141,17 +3146,17 @@ RELATIVE (matched against the indexed spelling) or ABSOLUTE (matched against the _Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._ ``` -$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h +$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3173,7 +3178,7 @@ pass this when you are about to edit again immediately). The MCP spelling is pos _The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr._ ``` -$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --no-post-check +$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] ``` @@ -3194,7 +3199,7 @@ op is one of replace_symbol_body, insert_before_symbol, insert_after_symbol each _Neither --dry-run nor --apply: the mode is explicit, so this refuses._ ``` -$ ./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json +$ ./build/ripwire . --edit-plan=/aux/edit_plan.json (empty) ``` @@ -3209,12 +3214,12 @@ exactly one of the two is required the plan's explicit mode: --dry-run preflight _The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure._ ``` -$ ./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] +$ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true}}]} +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3259,7 +3264,7 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - + @@ -3296,7 +3301,7 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - + @@ -3377,7 +3382,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3403,11 +3408,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3457,7 +3462,7 @@ Each line is ranked on its own — no community carve, no bin packing — so the _NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode)._ ``` -$ ./build/ripwire . --plan-lanes --brief=./../../tmp/lean-answers-068/showcase_scratch/aux/lanes_brief.txt +$ ./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt add a --since filter to the doc-drift verb add the CLI parse arm and help text for the new filter write regression tests for the new filter @@ -3482,7 +3487,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3516,11 +3521,12 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - - + + + @@ -3528,7 +3534,6 @@ $ ./build/ripwire . --stray-content=lane/ --plan - ... [17 more line(s); run it to see the whole thing] ``` @@ -3552,8 +3557,8 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - + @@ -3562,7 +3567,7 @@ $ ./build/ripwire . --stray-content=lane/ --abi - + ... [17 more line(s); run it to see the whole thing] @@ -3578,21 +3583,21 @@ $ ./build/ripwire . --stray-content=lane/ --abi **Answers:** find which branch's tree defines or mentions SYM, HEAD first which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch. -Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page is strictly shorter than the --whereis-listing=all page; otherwise every hit is listed. +Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page lists MORE definitions than the --whereis-listing=all page under the row cap, or the same ones in strictly fewer bytes; otherwise every hit is listed. **Try it** -_Which ref's tree defines or mentions SYM — HEAD first, then every local branch._ +_Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes._ ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - + + - - - + + ... [24 more line(s); run it to see the whole thing] ``` @@ -3606,23 +3611,23 @@ $ ./build/ripwire . --whereis=rankGraphTeleport ### `--whereis-listing=WHICH` -**Answers:** with --whereis: which rows to list, defs, refs or all (default: the shorter page). +**Answers:** with --whereis: which rows to list, defs, refs or all (default: more defs, else shorter). -The default lists defs when that page is strictly shorter in bytes than all under the same row cap, else all (a tie lists all). defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. +The default lists defs when that page lists MORE definitions than all under the same row cap (references can fill a capped all page before the branch definitions arrive), whatever its bytes; when both list the same definitions, defs only when strictly shorter in bytes, else all (a tie lists all). defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. **Try it** -_The definitions-only page asked for by name: every kind="def" row (here the first 60, most on branches, each carrying tip= and date=) and the references counted in . The default lists this page only when it is strictly shorter in bytes than the whole list (--whereis-listing=all) under the same row cap; on this symbol it is not, so the default above listed every hit._ +_The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too)._ ``` -$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=defs - - +$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all + + - - - + + + @@ -3763,17 +3769,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3798,17 +3804,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3832,18 +3838,18 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - - - - - - - - + + + + + + + + + + + + ... [18 more line(s); run it to see the whole thing] ``` @@ -3868,7 +3874,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3933,7 +3939,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4033,7 +4039,7 @@ $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" (run= when derivable). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : rank N in this ranking, rows in r= order. : cy … [line truncated: 546 more bytes on this line] - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4066,7 +4072,7 @@ $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --part - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4093,7 +4099,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + @@ -4120,7 +4126,7 @@ feeds a CodeCharta 3D city _Per-file metrics as CodeCharta cc.json._ ``` -$ ./build/ripwire . --export=cc.json:./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json +$ ./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json (empty) ``` @@ -4137,7 +4143,7 @@ caps at 16 (over-cap = capped=1). THE SAME `verb:arg` line is accepted by the MC _One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped ._ ``` -$ ./build/ripwire . --batch=./../../tmp/lean-answers-068/showcase_scratch/aux/batch2.txt +$ ./build/ripwire . --batch=/aux/batch2.txt for:incremental cache invalidation callers:rankGraphTeleport grep:DISCLOSE @@ -4168,11 +4174,11 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - + - - + + - + + @@ -4358,22 +4364,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - - -... [8 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [6 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4435,7 +4441,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4463,7 +4469,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=6c4d27498f3d0809 entries=786 +ripwire legend dictionary ripwire.dict/v1 dictv=c888cb4b5732610c entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -4515,7 +4521,7 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - @@ -4551,7 +4557,7 @@ _Drop matching paths (repeatable) before ranking._ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + @@ -4582,8 +4588,8 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch $ ./build/ripwire . --map-diff --top-k=5 - - + + @@ -4613,10 +4619,10 @@ $ ./build/ripwire . --map-diff --top-k=5 _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` -$ ./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache --top-k=3 +$ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + @@ -4642,7 +4648,7 @@ Writes BOTH families — BASE.lean.ripwirecache (map/ nav/--pr-context) and BASE _CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout._ ``` -$ ./build/ripwire . --index-out=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index +$ ./build/ripwire . --index-out=/aux/ci_index (empty) ``` @@ -4664,7 +4670,7 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co $ ./build/ripwire . --no-cache --top-k=3 - + @@ -4693,7 +4699,7 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses $ ./build/ripwire . --no-ignore --top-k=3 - + @@ -4802,10 +4808,10 @@ Under --scip it also writes the index's covered sites, so a precision join needs _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target._ ``` -$ ./build/ripwire . --pin-census=./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv --top-k=3 +$ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - + @@ -4857,10 +4863,10 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" - + @@ -61,12 +61,10 @@ *Same map, capped to the 5 highest-ranked symbols.* -**wall time: 1.84s** - ````` - + @@ -91,8 +89,6 @@ *NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* -**wall time: 1.59s** - ````` - + @@ -185,10 +179,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *The task lens: ranked signatures + quality metrics framed for the task.* -**wall time: 7.39s** +**wall time: 1.80s** ````` - @@ -225,10 +219,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Name-shaped query: the router picks name-exact BM25 (header says which/why).* -**wall time: 2.71s** - ````` - + @@ -265,10 +257,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* -**wall time: 3.53s** - ````` - + @@ -284,7 +274,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago", bo … [line truncated: 11 more bytes on this line] inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… inline std::string didYouMean( const IngestResult& ing, std::string_view name ) inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = nullptr ) @@ -295,7 +285,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 -### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] ### 2. Gates for A #### The convergence disclosure contract … [102 more display lines; full output is 14999 bytes on 72 raw line(s)] @@ -305,10 +295,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* -**wall time: 2.02s** - ````` - + @@ -326,10 +314,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* -**wall time: 2.02s** - ````` - + @@ -366,10 +352,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* -**wall time: 3.42s** - ````` - @@ -391,7 +375,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) struct Bm25Params inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept -inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] struct CandidateProvenance std::optional<int> runEvalViews( const MainDispatch& d ) @@ -406,10 +390,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Same task with the anchor disabled — the contrast the flag exists for.* -**wall time: 6.01s** - ````` - @@ -431,7 +413,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) struct Bm25Params inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept -inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] struct CandidateProvenance std::optional<int> runEvalViews( const MainDispatch& d ) @@ -449,7 +431,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 ````` - + @@ -462,12 +444,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* -**wall time: 1.42s** - ````` - + f, A&&... a ) @@ -489,8 +469,6 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* -**wall time: 1.03s** - ````` @@ -516,12 +494,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* -**wall time: 1.92s** - ````` ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.969) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 7.970) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -552,19 +528,40 @@ refuses by default. … [159 more lines, 14007 bytes total] ````` -## `./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` +## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` -*The directory-as-knowledge-base pattern: --recall pointed at a 1399178-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* +*The directory-as-knowledge-base pattern: --recall pointed at a 1411912-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* ````` -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 + +━━ commands.md (relevance 11.013) ━━ [sections: 1 of 159 selected (187 in doc), section-granular; whole doc 546809 B; lines="528-553"; dropped_by_budget=158] [truncated: 1854 of 4293 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base + +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? -━━ commands.md (relevance 10.211) ━━ [sections: 1 of 157 selected (187 in doc), section-granular; whole doc 533991 B; lines="3700-3704"; dropped_by_budget=156] [truncated: 1865 of 7739 bytes] -### `--field-affinity[=STRUCT]` +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: -**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. -Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … (capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ````` @@ -579,8 +576,6 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas *File-by-file orientation map (top symbols per file).* -**wall time: 1.99s** - ````` @@ -615,11 +610,11 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas … [366 more display lines; full output is 11120 bytes on 1 raw line(s)] ````` -## `./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html` +## `./build/ripwire . --html=/aux/map2.html` *Self-contained HTML force-directed call graph.* -**wall time: 4.39s** +**wall time: 1.08s** ````` (empty) @@ -628,15 +623,13 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas Artifact written: ````` - 158883 ./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html + 158883 /aux/map2.html ````` ## `./build/ripwire . --order=stable --top-k=5` *Stable (path/id) emit order — provider KV-cache hits across re-runs.* -**wall time: 1.15s** - ````` @@ -658,7 +651,7 @@ Artifact written: - + ````` @@ -670,13 +663,11 @@ Artifact written: *Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* -**wall time: 1.03s** - ````` - + @@ -714,7 +705,7 @@ Artifact written: - + @@ -748,13 +739,11 @@ Artifact written: *The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* -**wall time: 1.74s** - ````` - + @@ -788,11 +777,9 @@ Artifact written: *Who calls SYM (1-hop in-edges).* -**wall time: 2.15s** - ````` - + @@ -807,12 +794,10 @@ Artifact written: *The same rows under --legend=full: the default is the compact dialect (one compact comment plus schema="ripwire.callers/v1" on the root), and --legend=full restores the prose legend — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB more legend. Works on EVERY XML verb.* -**wall time: 2.14s** - ````` - + @@ -827,7 +812,7 @@ Artifact written: *Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* -**exit code: 1** — **wall time: 1.08s** +**exit code: 1** ````` @@ -844,11 +829,9 @@ ripwire: --callers symbol not found: DoesNotExist *What SYM calls (1-hop out-edges).* -**wall time: 1.93s** - ````` - + @@ -864,11 +847,9 @@ ripwire: --callers symbol not found: DoesNotExist *The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* -**wall time: 2.72s** - ````` - + @@ -885,11 +866,9 @@ ripwire: --callers symbol not found: DoesNotExist *Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* -**wall time: 1.85s** - ````` - + @@ -925,19 +904,17 @@ ripwire: --callers symbol not found: DoesNotExist *Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* -**wall time: 2.08s** - ````` - - + + - + - + @@ -950,7 +927,7 @@ ripwire: --callers symbol not found: DoesNotExist - + @@ -965,25 +942,23 @@ ripwire: --callers symbol not found: DoesNotExist *The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* -**wall time: 1.89s** - ````` - - - + + + - + - - + + @@ -1005,11 +980,9 @@ ripwire: --callers symbol not found: DoesNotExist *Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* -**wall time: 2.01s** - ````` - + @@ -1022,11 +995,9 @@ ripwire: --callers symbol not found: DoesNotExist *Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* -**wall time: 1.90s** - ````` - + @@ -1041,8 +1012,6 @@ ripwire: --callers symbol not found: DoesNotExist *Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* -**wall time: 1.90s** - ````` code edges).* -**wall time: 1.86s** - ````` @@ -1100,11 +1067,9 @@ ripwire: --callers symbol not found: DoesNotExist *Test files that transitively reach the changed file.* -**wall time: 2.23s** - ````` - @@ -1128,12 +1093,10 @@ ripwire: --callers symbol not found: DoesNotExist *Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* -**wall time: 2.25s** - ````` ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 75cc95803 +at: c7920353a (0 changed files — working tree is clean, nothing to analyze) ````` @@ -1141,7 +1104,7 @@ at: 75cc95803 *Pre-PR gate on a CLEAN tree: no obligations, exit 0.* -**wall time: 8.55s** +**wall time: 1.87s** ````` @@ -1153,12 +1116,10 @@ at: 75cc95803 *Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* -**wall time: 2.43s** - ````` captured nodes with their enclosing symbol; grammars=/eligible_files= the scope. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). counts_floor=1: every count is a FLOOR, never a total. hits_capped=1: hits= is a floor. root=: p= relative to it. of_files=: indexed files in all (eligible_files= of them are in the query's languages). --> - + - + - + @@ -1403,12 +1352,12 @@ at: 75cc95803 - + - + … [28 more display lines; full output is 3359 bytes on 1 raw line(s)] ````` @@ -1421,10 +1370,8 @@ at: 75cc95803 *Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* -**wall time: 2.59s** - ````` - + @@ -1436,7 +1383,7 @@ at: 75cc95803 The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) struct RankedGraph inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… inline std::uint32_t sliceRdIterationCeiling() noexcept @@ -1461,14 +1408,12 @@ at: 75cc95803 *Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* -**wall time: 1.26s** - ````` - + @@ -1501,8 +1446,6 @@ at: 75cc95803 *Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* -**wall time: 1.47s** - ````` @@ -1532,8 +1475,6 @@ at: 75cc95803 *CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* -**wall time: 1.18s** - ````` @@ -1569,8 +1510,6 @@ ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — *Full body + inline callee signatures.* -**wall time: 1.12s** - ````` @@ -1769,12 +1700,10 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Fan-in/out + complexity annotations on the map.* -**wall time: 1.57s** - ````` - + @@ -1809,14 +1738,12 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *File->file dependency graph (god-files, cycles).* -**wall time: 2.63s** - ````` - - + + - + @@ -1849,18 +1776,18 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Complexity x recent git churn (maintenance pain).* -**wall time: 4.55s** +**wall time: 1.07s** ````` - + - - + + @@ -1868,11 +1795,11 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + - + @@ -1882,18 +1809,18 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan -… [14 more display lines; full output is 5747 bytes on 1 raw line(s)] +… [14 more display lines; full output is 5750 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --clones` *Token-normalized duplicate bodies.* -**wall time: 9.83s** +**wall time: 2.55s** ````` - + @@ -1929,40 +1856,40 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Files that change together in git (hidden coupling).* -**wall time: 5.03s** +**wall time: 1.69s** ````` - - - + + + - - - - + - - + + - + + + + - - + + - - + + ````` @@ -1971,48 +1898,46 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Hotspots scoped to RECENT churn (the regression lens).* -**wall time: 3.09s** +**wall time: 1.29s** ````` - - - - - - + + + + + + - - + + - + - - + + - - + + - - + + -… [13 more display lines; full output is 5103 bytes on 1 raw line(s)] +… [13 more display lines; full output is 5099 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --arch=test/archfix/rules.txt` *Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* -**wall time: 1.94s** - ````` @@ -2051,11 +1976,11 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* -**wall time: 7.75s** +**wall time: 1.69s** ````` - + @@ -2071,7 +1996,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + @@ -2091,8 +2016,6 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *User lint rules (YAML, ast-grep style) from a directory.* -**wall time: 2.69s** - ````` @@ -2118,8 +2041,6 @@ ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x *Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* -**wall time: 1.53s** - Input file: ````` @@ -2173,26 +2094,24 @@ The joined finding — past the display cut above, extracted so the join is visi *Cluster the call graph into cohesive modules.* -**wall time: 2.57s** - ````` - + - + - + @@ -2213,15 +2132,13 @@ The joined finding — past the display cut above, extracted so the join is visi *Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* -**wall time: 2.88s** - ````` - - + - + @@ -2253,14 +2170,12 @@ The joined finding — past the display cut above, extracted so the join is visi *The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* -**wall time: 1.99s** - ````` - - - + + @@ -2268,7 +2183,7 @@ The joined finding — past the display cut above, extracted so the join is visi - + @@ -2278,7 +2193,7 @@ The joined finding — past the display cut above, extracted so the join is visi - + @@ -2286,30 +2201,28 @@ The joined finding — past the display cut above, extracted so the join is visi -… [701 more display lines; full output is 27553 bytes on 1 raw line(s)] +… [699 more display lines; full output is 27481 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --report` *Architecture summary (modules, god-files, cycles) as markdown.* -**wall time: 1.41s** - ````` # ripwire architecture report -2484 files · 23849 symbols · 34232 edges · 2322 modules (10910 call-graph isolated) +2484 files · 23851 symbols · 34236 edges · 2324 modules (10910 call-graph isolated) Root: `.` Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 2322) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1378 symbols -- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1136 symbols -- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 538 symbols +## Modules (call-graph clusters; showing 12 of 2324) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1374 symbols +- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1135 symbols +- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 534 symbols - **src::kindIs@infra/nodekind.h:46:3271 [emit,collect,push]** — 256 symbols - **docs::`--top-k=N`@COMMANDS.md:59:8861 [pack,allow,insert]** — 177 symbols - **src::realpath@infra/os.h:199:13067 [parse,split,read]** — 63 symbols @@ -2321,7 +2234,7 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 - **test::no@decltodefcheck.sh:127:11708 [run]** — 30 symbols ## God files (most depended-on; showing 10 of 445) -- `test/lib/clean-env.sh` — 180 dependents +- `test/lib/clean-env.sh` — 181 dependents - `src/infra/emit.h` — 86 dependents - `src/model.h` — 85 dependents - `src/infra/Diagnostics.h` — 58 dependents @@ -2333,17 +2246,15 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 *Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* -**wall time: 2.60s** - ````` - + - + - + @@ -2366,15 +2277,13 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 -… [102 more display lines; full output is 13421 bytes on 1 raw line(s)] +… [102 more display lines; full output is 13425 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --mermaid` *Module (directory) dependency graph as a Mermaid diagram.* -**wall time: 1.41s** - ````` %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR @@ -2383,7 +2292,7 @@ flowchart LR n102["src/infra
1276"] end subgraph sg1 ["test"] - n103["test
4851"] + n103["test
4853"] n328["test/fixtures/recallpassage
314"] n280["test/expandmodefix
151"] n282["test/expandsibsfix
149"] @@ -2413,11 +2322,11 @@ flowchart LR *Bus-factor: recency-weighted author ownership per file.* -**wall time: 3.54s** +**wall time: 1.56s** ````` - + @@ -2425,7 +2334,7 @@ flowchart LR - + @@ -2446,18 +2355,16 @@ flowchart LR -… [884 more display lines; full output is 97595 bytes on 1 raw line(s)] +… [884 more display lines; full output is 97571 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --dead-code=src` *Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* -**wall time: 2.49s** - ````` - + ````` @@ -2466,8 +2373,6 @@ flowchart LR *Which symbols a TEST FILE exercises — the reverse direction of --affected.* -**wall time: 1.93s** - `````
````` @@ -2492,11 +2395,11 @@ flowchart LR *On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* -**wall time: 55.48s** +**wall time: 15.01s** ````` - @@ -2519,31 +2422,30 @@ flowchart LR - - + + -… [106 more display lines; full output is 13491 bytes on 1 raw line(s)] +… [107 more display lines; full output is 13557 bytes on 1 raw line(s)] ````` stderr: ````` ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) -ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) ````` ## `./build/ripwire . --edit-check=rankGraphTeleport` *Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* -**wall time: 34.55s** +**wall time: 6.84s** ````` - @@ -2558,11 +2460,9 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other *No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* -**wall time: 4.03s** - ````` - ````` @@ -2571,55 +2471,55 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other *The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* -**wall time: 8.23s** +**wall time: 2.11s** ````` - - + - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - -… [283 more display lines; full output is 18391 bytes on 1 raw line(s)] + + + + + + + + +… [96 more display lines; full output is 7654 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` *Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* -**wall time: 33.23s** +**wall time: 10.70s** ````` - - + + - + @@ -2631,11 +2531,11 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other *Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* -**wall time: 9.18s** +**wall time: 3.02s** ````` - + @@ -2664,25 +2564,23 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other -… [304 more display lines; full output is 37803 bytes on 1 raw line(s)] +… [319 more display lines; full output is 39445 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` *A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* -**wall time: 3.75s** - ````` - + - + @@ -2698,15 +2596,16 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other *Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* -**wall time: 111.93s** +**wall time: 59.44s** ````` - + - - + + + @@ -2714,15 +2613,15 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other - - + + + - + - @@ -2730,15 +2629,14 @@ ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other - -… [60909 more display lines; full output is 7506091 bytes on 1 raw line(s)] +… [115194 more display lines; full output is 12883655 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` *A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* -**exit code: 1** — **wall time: 4.56s** +**exit code: 1** ````` (empty) @@ -2755,12 +2653,12 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* -**exit code: 2** — **wall time: 66.00s** +**exit code: 2** — **wall time: 14.23s** ````` - + @@ -2769,7 +2667,7 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - + @@ -2793,20 +2691,60 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer ## `./build/ripwire . --whereis=rankGraphTeleport` -*Which ref's tree defines or mentions SYM — HEAD first, then every local branch.* +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.* + +**wall time: 9.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 18444 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` -**wall time: 44.36s** +*The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).* + +**wall time: 9.48s** ````` - + - + @@ -2831,56 +2769,16 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer … [34 more display lines; full output is 13695 bytes on 1 raw line(s)] ````` -## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=defs` - -*The definitions-only page asked for by name: every kind="def" row (here the first 60, most on branches, each carrying tip= and date=) and the references counted in . The default lists this page only when it is strictly shorter in bytes than the whole list (--whereis-listing=all) under the same row cap; on this symbol it is not, so the default above listed every hit.* - -**wall time: 39.02s** - -````` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -… [35 more display lines; full output is 18430 bytes on 1 raw line(s)] -````` - ## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` *Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* -**wall time: 27.84s** +**wall time: 16.05s** ````` - - - + + + @@ -2908,15 +2806,13 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer -… [36 more display lines; full output is 15174 bytes on 1 raw line(s)] +… [36 more display lines; full output is 15188 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --flags` *The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* -**wall time: 1.21s** - ````` @@ -3005,10 +2901,10 @@ ripwire: run `ripwire . --flags` for the gate table *NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* -**wall time: 2.40s** +**wall time: 1.27s** ````` -{"v":1,"verb":"plan-lanes","at":"75cc95803","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":342 … [line truncated: 339 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"c7920353a","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23851,"edges":342 … [line truncated: 339 more bytes on this line] "symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, @@ -3026,14 +2922,14 @@ ripwire: run `ripwire . --flags` for the gate table {"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":685,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":414,"tested":0}, {"p":"./src/taskroute.h","n":"testCoverageTaskChoice","scope":"rw::taskroute","key":"bf72991aa28355c7","id":"./src/taskroute.h::rw::taskroute::testCoverageTaskChoice","id_addressable":true,"id_collides_with":0,"l":1365,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":34,"tested":0}, {"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5191,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":308,"tested":0}], +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5191,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":309,"tested":0}], "files":[{"p":"./src/darkflags.h","symbols":1,"churn":31,"ccx":8,"hotspot_rank":45}, {"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":31}, {"p":"./src/gitmine.h","symbols":1,"churn":59,"ccx":7,"hotspot_rank":19}, {"p":"./src/main.cpp","symbols":2,"churn":414,"ccx":12,"hotspot_rank":4}, -{"p":"./src/mcpverbs.h","symbols":1,"churn":308,"ccx":2,"hotspot_rank":5}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":309,"ccx":2,"hotspot_rank":5}, {"p":"./src/pageview.h","symbols":1,"churn":18,"ccx":2,"hotspot_rank":134}, -{"p":"./src/taskroute.h","symbols":1,"churn":34,"ccx":2,"hotspot_rank":40}, +{"p":"./src/taskroute.h","symbols":1,"churn":34,"ccx":2,"hotspot_rank":41}, {"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":34}, {"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":8,"hotspot_rank":23}]},"blast_radius":{"reaches":51,"files_total":17,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editplan.h","./src/flipimpact.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./s … [line truncated: 203 more bytes on this line] "tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":51,"module_span":1,"notes":[], @@ -3041,11 +2937,11 @@ ripwire: run `ripwire . --flags` for the gate table … [72 more display lines; full output is 20570 bytes on 1 raw line(s)] ````` -## `./build/ripwire . --plan-lanes --brief=./../../tmp/lean-answers-068/showcase_scratch/aux/lanes_brief.txt` +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` *NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* -**wall time: 1.66s** +**wall time: 1.34s** Input file: @@ -3056,36 +2952,36 @@ write regression tests for the new filter ````` ````` -{"v":1,"verb":"plan-lanes","at":"75cc95803","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":34232,"ambiguous":10278,"unresolved":12860},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"c7920353a","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23851,"edges":34236,"ambiguous":10278,"unresolved":12878},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] "symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] {"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":640,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":308,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":640,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":309,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3204,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, {"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, {"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":12,"cx":14,"ccx":15,"churn":36,"tested":0}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":12,"cx":26,"ccx":38,"churn":36,"tested":0}, {"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, {"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1642,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":70,"tested":0}, {"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, -{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":67,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":67,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":41,"tested":0}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], -"files":[{"p":"./src/cli.h","symbols":1,"churn":435,"ccx":0,"hotspot_rank":6}, +"files":[{"p":"./src/cli.h","symbols":1,"churn":436,"ccx":0,"hotspot_rank":6}, {"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":31}, {"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":87}, -{"p":"./src/mcprefusal.h","symbols":1,"churn":40,"ccx":0,"hotspot_rank":55}, -{"p":"./src/mcpverbs.h","symbols":1,"churn":308,"ccx":0,"hotspot_rank":5}, +{"p":"./src/mcprefusal.h","symbols":1,"churn":41,"ccx":0,"hotspot_rank":54}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":309,"ccx":0,"hotspot_rank":5}, {"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":52}, {"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":4,"hotspot_rank":23}], -"symbols_total":3146,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"symbols_total":3147,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], "tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":12,"module_span":6,"notes":[], "execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":7,"module_span":6,"max_ccx":38,"sum_ccx":62,"ambiguous_calls":26,"blast_reaches":12,"contract_touches":1,"conflicts":3,"untested":12,"t … [line truncated: 139 more bytes on this line] {"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":107,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] -{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2825,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, -{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2904,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2828,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3204,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2907,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, {"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":107,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, {"p":"./src/slice.h","n":"sliceRdIterationCeiling","scope":"slicev","key":"449132b302bb4324","id":"./src/slice.h::slicev::sliceRdIterationCeiling","id_addressable":true,"id_collides_with":0,"l":1372,"ord":0,"overloads":1,"amb":0,"cx":9,"ccx":12,"churn":51,"tested":0}, -{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":41,"tested":0}, … [63 more display lines; full output is 19235 bytes on 1 raw line(s)] ````` @@ -3167,20 +3063,20 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped ````` - - -
- - - - - - - - - - - + + + + + + + + + + + + + + @@ -3196,31 +3092,29 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped -… [243 more display lines; full output is 25197 bytes on 1 raw line(s)] +… [243 more display lines; full output is 25199 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --doc-drift --gateability` *The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* -**wall time: 1.02s** - ````` - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -3236,7 +3130,7 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped -… [269 more display lines; full output is 26396 bytes on 1 raw line(s)] +… [269 more display lines; full output is 26398 bytes on 1 raw line(s)] ````` Tail of the same output — the `` section: @@ -3273,25 +3167,23 @@ Tail of the same output — the `` section: *Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* -**wall time: 1.20s** - ````` - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -3306,7 +3198,7 @@ Tail of the same output — the `` section: -… [221 more display lines; full output is 25111 bytes on 1 raw line(s)] +… [221 more display lines; full output is 25113 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --from-trace=-` @@ -3399,7 +3291,7 @@ AddressSanitizer:DEADLYSIGNAL (run= when derivable). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : rank N in this ranking, rows in r= order. : cy … [line truncated: 546 more bytes on this line] - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -3415,7 +3307,7 @@ AddressSanitizer:DEADLYSIGNAL - + @@ -3426,8 +3318,6 @@ AddressSanitizer:DEADLYSIGNAL *Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* -**wall time: 1.27s** - ````` wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> @@ -3435,7 +3325,7 @@ AddressSanitizer:DEADLYSIGNAL - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -3445,10 +3335,10 @@ AddressSanitizer:DEADLYSIGNAL mcp-tools pass-through. Written into the server command only where its argument shape is known t…inline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept - + -inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept @@ -3469,7 +3359,7 @@ AddressSanitizer:DEADLYSIGNAL **wall time: 1.06s** ````` - + @@ -3481,7 +3371,7 @@ AddressSanitizer:DEADLYSIGNAL The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) struct RankedGraph inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… inline std::uint32_t sliceRdIterationCeiling() noexcept @@ -3502,11 +3392,11 @@ AddressSanitizer:DEADLYSIGNAL … [60 more display lines; full output is 9337 bytes on 10 raw line(s)] ````` -## `./build/ripwire . --export=cc.json:./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json` +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` *Per-file metrics as CodeCharta cc.json.* -**wall time: 1.05s** +**wall time: 1.02s** ````` (empty) @@ -3515,15 +3405,15 @@ AddressSanitizer:DEADLYSIGNAL Artifact written: ````` - 374998 ./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json + 374998 /aux/ripwire2.cc.json {"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri ````` -## `./build/ripwire . --batch=./../../tmp/lean-answers-068/showcase_scratch/aux/batch2.txt` +## `./build/ripwire . --batch=/aux/batch2.txt` *One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* -**wall time: 2.11s** +**wall time: 1.37s** Input file: @@ -3538,7 +3428,7 @@ lego:Vehicle - + @@ -3577,15 +3467,15 @@ lego:Vehicle *Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* -**exit code: 1** — **wall time: 4.30s** +**exit code: 1** — **wall time: 2.61s** ````` - - + - - + + - + + @@ -3666,34 +3556,30 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s ````` -## `./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` *Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* -**wall time: 1.22s** - ````` - - - - - - - - - - - - + + + + + + + + + + - - - - + + + + - + ````` @@ -3717,11 +3603,9 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf *Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* -**wall time: 1.29s** - ````` - + 0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp 0,1,1,2,3,3,3 @@ -3737,24 +3621,22 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf *Flat top-K export for an external reranker.* -**wall time: 1.55s** - ````` - - + + inline std::string spanTierMemoPath( const std::string& diskPath ) - + inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) - + ### See the map — not just the numbers - + inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) - + const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) @@ -3781,7 +3663,7 @@ ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidate *Machine-parseable JSON, same content, keys mirror the XML attrs.* ````` -{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, {"t":"fn","n":"rankGraph","p":"src/graph.h:5606"}, {"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:6252"}, {"t":"fn","n":"runEval","p":"src/eval.h:171"}, @@ -3810,15 +3692,15 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Pagination: 3 items, skipping the first 3 (deterministic seams).* -**wall time: 2.47s** +**wall time: 1.06s** ````` - + - - + + ````` @@ -3829,7 +3711,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m ````` - + @@ -3854,12 +3736,10 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Drop matching paths (repeatable) before ranking.* -**wall time: 1.13s** - ````` - + @@ -3884,13 +3764,11 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* -**wall time: 1.59s** - ````` - - + + @@ -3914,12 +3792,12 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* -**wall time: 4.20s** +**wall time: 1.72s** ````` - + @@ -3932,16 +3810,16 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m ````` -## `./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache --top-k=3` +## `./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3` *Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* -**wall time: 4.93s** +**wall time: 1.80s** ````` - + @@ -3957,7 +3835,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m Artifact written: ````` - 18643301 ./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache + 18649971 /aux/warm2.ripwirecache ````` ## `./build/ripwire . --max-file-size=8K --top-k=3` @@ -4004,12 +3882,12 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Multi-root workspace: ONE merged graph over two roots, paths labeled /.* -**wall time: 4.10s** +**wall time: 1.59s** ````` - + @@ -4036,18 +3914,18 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Self-eval: co-change recall vs BM25.* -**wall time: 30.20s** +**wall time: 16.02s** ````` ripwire --eval (co-change recovery, averaged over 80 historical commits) ranker recall@5 recall@10 recall@20 - ripwire 4.1% 8.5% 12.3% - BM25 12.8% 15.0% 19.5% - BM25sub 17.3% 23.1% 30.6% - BM25body 13.7% 26.2% 44.2% - fused 14.2% 17.6% 29.4% - anchored 13.7% 26.2% 44.2% - same-dir 0.0% 8.8% 13.1% + ripwire 4.1% 8.3% 15.1% + BM25 13.0% 15.1% 20.2% + BM25sub 17.3% 23.3% 31.6% + BM25body 13.7% 25.7% 44.5% + fused 14.2% 18.3% 29.6% + anchored 13.7% 25.7% 44.5% + same-dir 0.0% 8.8% 13.8% random 0.2% 0.4% 0.8% <- floor (random ranking over F=2484 files) note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 / @@ -4064,7 +3942,7 @@ ripwire --eval (co-change recovery, averaged over 80 historical commits) *Known-item retrieval eval: MRR + recall@k per ranker per query mode.* -**wall time: 151.77s** +**wall time: 134.50s** ````` ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) @@ -4073,11 +3951,11 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) ranker query-mode MRR recall@1 recall@5 recall@10 - subtoken name 0.693 56.1% 85.7% 90.8% + subtoken name 0.693 56.1% 85.8% 90.8% subtoken doc-phrase 0.920 90.0% 94.0% 94.9% name-exact name 0.905 83.5% 96.1% 97.4% name-exact doc-phrase 0.013 0.5% 1.8% 2.6% - anchored name 0.699 57.6% 85.2% 89.2% + anchored name 0.698 57.6% 85.2% 89.2% anchored doc-phrase 0.917 89.5% 94.0% 94.5% routed name 0.908 83.7% 96.3% 97.7% routed doc-phrase 0.919 90.0% 93.8% 94.7% @@ -4089,11 +3967,11 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co (routed==name-exact on name, ~=subtoken+body on doc-phrase). ````` -## `./build/ripwire . --eval-stray=./../../tmp/lean-answers-068/showcase_scratch/aux/stray_labels2.tsv` +## `./build/ripwire . --eval-stray=/aux/stray_labels2.tsv` *Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* -**exit code: 3** — **wall time: 9.64s** +**exit code: 3** — **wall time: 7.70s** Input file: @@ -4113,7 +3991,7 @@ arm/orient-map-067-narrow merged ````` -## `./build/ripwire skills --eval-skills=./../../tmp/lean-answers-068/showcase_scratch/aux/skills_labels2.tsv` +## `./build/ripwire skills --eval-skills=/aux/skills_labels2.tsv` *Labelled skill-ROUTING eval over the repo's own skills/ directory (4 hand-labelled prompts).* @@ -4127,7 +4005,7 @@ what is the weather in Paris none neg ````` ````` -ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus './../../tmp/lean-answers-068/showcase_scratch/aux/skills_labels2.tsv'; split test=4 dev=0) +ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus '/aux/skills_labels2.tsv'; split test=4 dev=0) arm hit@1 hit@2 mrr sep-auc fire/abstain@ORACLE-th (upper bound) overlap 66.7% 66.7% 0.690 0.000 50.0% (th=-1.000) name 33.3% 66.7% 0.537 0.667 50.0% (th=0.000) @@ -4157,7 +4035,7 @@ ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router ripwire-write-tests 0 0 0 0 0 misses (overlap): line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" -… [19 more lines, 3699 bytes total] +… [19 more lines, 3665 bytes total] ````` ## `./build/ripwire wrap claude` @@ -4203,7 +4081,7 @@ Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after th *Version + short build info.* ````` -ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=75cc95803) +ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c7920353a) ````` @@ -4228,7 +4106,7 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=75cc ````` - + @@ -4261,7 +4139,7 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - + @@ -4273,7 +4151,7 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - + @@ -4316,9 +4194,11 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* +**wall time: 1.02s** + ````` - + @@ -4342,7 +4222,7 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - + @@ -4362,9 +4242,11 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* +**wall time: 1.04s** + ````` - + @@ -4395,7 +4277,7 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - + @@ -4468,11 +4350,9 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* -**wall time: 1.03s** - ````` - + @@ -4492,7 +4372,7 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* -**wall time: 1.81s** +**wall time: 1.82s** ````` @@ -4505,7 +4385,7 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* -**wall time: 2.01s** +**wall time: 1.67s** ````` @@ -4520,20 +4400,20 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < DISCLOSE( ex, Exclusions::DisclosureWhy::InnerCommaSaturated, "atoms: the inner-comma exclusion stream spent its whole b DISCLOSE( st, Type3Stats::DisclosureWhy::PairCapHit, "clones: Type-3 pair cap hit — first N compared (both-gate-surviv DISCLOSE( scan, CommentCoherenceScan::DisclosureWhy::UnreadableFile, "comment-coherence: an indexed file could not be re -DISCLOSE( st, StreamBlobStats::DisclosureWhy::ListUnwritable, "crossref: cannot write the blob-batch list — cross-bran -DISCLOSE( st, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: git cat-file --batch failed to start — cross -DISCLOSE( st, StreamBlobStats::DisclosureWhy::StreamEndedMidBlob, "crossref: git cat-file stream e -DISCLOSE( dropSink, RefEnumeration::DisclosureWhy::TipNotObjectName, "crossref: for-each-ref yielded a ref whose tip is -DISCLOSE( "crossref: refusing a diff whose revision arguments are not resolved object names" ) -DISCLOSE( sweep, ParallelSweep::DisclosureWhy::WorkerThrew, "crossref: a git worker threw — this shard of the sweep is -DISCLOSE( plumb, RefPlumbing::DisclosureWhy::RevisionNotObjectName, "crossref: ref tip or HEAD is not a resolved object -DISCLOSE( plumb, RefPlumbing::DisclosureWhy::MergeBaseNotObjectName, "crossref: merge-base returned something that is no -DISCLOSE( plumb, RefPlumbing::DisclosureWhy::NoMergeBase, "crossref: no merge-base for ref (shallow clone or unrelated h -DISCLOSE( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ) -DISCLOSE( "whereis: the index's def sites match no HEAD row (working tree drifted from HEAD?) — keeping the lexical la -DISCLOSE( scan, WorktreeScan::DisclosureWhy::ListingFailed, "whereis: git could not list the working tree's changes — -DISCLOSE( scan, WorktreeScan::DisclosureWhy::OverPathCap, "whereis: more changed paths than the overlay reads — the re -DISCLOSE( scan, WorktreeScan::DisclosureWhy::CopyUnread, "whereis: a changed path could not be read from the working tre +DISCLOSE( st, StreamBlobStats::DisclosureWhy::ListUnwritable, "crossref: cannot write the blob-batch list — cross-bran +DISCLOSE( st, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: git cat-file --batch failed to start — cross +DISCLOSE( st, StreamBlobStats::DisclosureWhy::StreamEndedMidBlob, "crossref: git cat-file stream e +DISCLOSE( dropSink, RefEnumeration::DisclosureWhy::TipNotObjectName, "crossref: for-each-ref yielded a ref whose tip is +DISCLOSE( "crossref: refusing a diff whose revision arguments are not resolved object names" ) +DISCLOSE( sweep, ParallelSweep::DisclosureWhy::WorkerThrew, "crossref: a git worker threw — this shard of the sweep is +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::RevisionNotObjectName, "crossref: ref tip or HEAD is not a resolved object +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::MergeBaseNotObjectName, "crossref: merge-base returned something that is no +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::NoMergeBase, "crossref: no merge-base for ref (shallow clone or unrelated h +DISCLOSE( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ) +DISCLOSE( "whereis: the index's def sites match no HEAD row (working tree drifted from HEAD?) — keeping the lexical la +DISCLOSE( scan, WorktreeScan::DisclosureWhy::ListingFailed, "whereis: git could not list the working tree's changes — +DISCLOSE( scan, WorktreeScan::DisclosureWhy::OverPathCap, "whereis: more changed paths than the overlay reads — the re +DISCLOSE( scan, WorktreeScan::DisclosureWhy::CopyUnread, "whereis: a changed path could not be read from the working tre DISCLOSE( out, CMakeScan::DisclosureWhy::SymlinkEscapesRoot, "flags: a CMake file's symlink target leaves the root — f DISCLOSE( out, CMakeScan::DisclosureWhy::RootWalkFailed, "flags: cannot walk root for CMake files — cmake gates omitte DISCLOSE( r, Result::DisclosureWhy::BaseTreeUnavailable, "dmm: the base commit's tree could not be materialized or parse @@ -4564,7 +4444,7 @@ ripwire: --pattern: this pattern collapses to a single bare token, which is a TE ````` @@ -4845,10 +4725,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* -**wall time: 1.04s** - ````` - + @@ -4885,10 +4763,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* -**wall time: 1.07s** - ````` - + @@ -4925,14 +4801,12 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* -**wall time: 1.39s** - ````` - + -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) @@ -4942,13 +4816,13 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? inline const char* kAcksFile = ".ripwire_quality_acks" DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome +struct QualityDeltaOutcome inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -inline std::string qualityAcksPath( const std::string& root ) +inline std::string qualityAcksPath( const std::string& root ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at inline std::string gitHeadSha( const std::string& root ) inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) @@ -4957,7 +4831,7 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] -inline std::string qualityBaselinePath( const std::string& root ) +inline std::string qualityBaselinePath( const std::string& root ) … [52 more display lines; full output is 9064 bytes on 1 raw line(s)] ````` @@ -4965,14 +4839,12 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* -**wall time: 1.24s** - ````` - + -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) @@ -4982,13 +4854,13 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? inline const char* kAcksFile = ".ripwire_quality_acks" DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome +struct QualityDeltaOutcome inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -inline std::string qualityAcksPath( const std::string& root ) +inline std::string qualityAcksPath( const std::string& root ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at inline std::string gitHeadSha( const std::string& root ) inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) @@ -4997,7 +4869,7 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] -inline std::string qualityBaselinePath( const std::string& root ) +inline std::string qualityBaselinePath( const std::string& root ) … [53 more display lines; full output is 9230 bytes on 1 raw line(s)] ````` @@ -5038,22 +4910,22 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* -**wall time: 1.64s** +**wall time: 1.35s** ````` - + - + - - - + + + + ````` @@ -5062,19 +4934,19 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* -**wall time: 2.11s** +**wall time: 1.32s** ````` - + - + + ````` @@ -5124,7 +4996,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist ````` - + @@ -5144,7 +5016,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist ````` - + @@ -5163,17 +5035,17 @@ stderr: ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable ordering the server turns on) — it changed nothing here; the CLI map orders important-first unless you pass --order=stable ````` -## `./build/ripwire . --run-trace="cat ./../../tmp/lean-answers-068/showcase_scratch/aux/asan_trace_now.txt; exit 1"` +## `./build/ripwire . --run-trace="cat /aux/asan_trace_now.txt; exit 1"` *EXEC-MODE --from-trace: run a command, and on a non-zero exit map its captured output onto indexed symbols in the same call — the whole fix-loop entry. ripwire exits 4 here because the wrapped command failed, which is the signal, not an incident.* **exit code: 4** ````` - + - + pw = biasPrior( g, p ); @@ -5219,7 +5091,7 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o ````` - + ````` @@ -5227,12 +5099,12 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o *A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* -**exit code: 4** — **wall time: 2.55s** +**exit code: 4** — **wall time: 2.43s** ````` - + ````` @@ -5267,11 +5139,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* -**wall time: 5.06s** +**wall time: 3.98s** ````` - @@ -5309,7 +5181,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - + @@ -5327,7 +5199,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - + @@ -5345,7 +5217,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - @@ -5373,7 +5245,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + … [21 more display lines; full output is 11209 bytes on 1 raw line(s)] ````` @@ -5381,11 +5253,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* -**wall time: 2.72s** +**wall time: 3.31s** ````` - @@ -5421,11 +5293,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* -**wall time: 2.64s** +**wall time: 2.62s** ````` - @@ -5441,19 +5313,19 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + - + - + - + - + … [35 more display lines; full output is 7334 bytes on 1 raw line(s)] ````` @@ -5461,11 +5333,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* -**wall time: 1.63s** +**wall time: 1.78s** ````` - @@ -5501,11 +5373,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* -**wall time: 1.58s** +**wall time: 1.41s** ````` - @@ -5539,7 +5411,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - + @@ -5555,7 +5427,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + @@ -5575,11 +5447,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* -**wall time: 9.75s** +**wall time: 12.45s** ````` - + @@ -5612,11 +5484,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* -**wall time: 3.05s** +**wall time: 3.89s** ````` - + @@ -5632,7 +5504,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + @@ -5691,7 +5563,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=6c4d27498f3d0809 entries=786 +ripwire legend dictionary ripwire.dict/v1 dictv=c888cb4b5732610c entries=786 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5721,14 +5593,14 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [757 more display lines; full output is 78294 bytes on 787 raw line(s)] +… [757 more display lines; full output is 78308 bytes on 787 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` *Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* -**wall time: 2.14s** +**wall time: 3.27s** ````` @@ -5768,7 +5640,7 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= *DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* -**wall time: 2.31s** +**wall time: 2.65s** ````` @@ -5808,7 +5680,7 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= *An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* -**exit code: 1** — **wall time: 2.09s** +**exit code: 1** — **wall time: 3.00s** ````` (empty) @@ -5824,7 +5696,7 @@ ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-' *A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* -**exit code: 1** — **wall time: 2.04s** +**exit code: 1** — **wall time: 2.92s** ````` (empty) @@ -5840,7 +5712,7 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- *The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* -**wall time: 2.05s** +**wall time: 2.82s** ````` {"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] @@ -5873,13 +5745,13 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- {"id":"atom-nested-ternary","shortDescription":{"text":"atom-nested-ternary"},"properties":{"builtin":true,"capped":false,"applicable":true}}, {"id":"atom-implicit-predicate","shortDescription":{"text":"atom-implicit-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, {"id":"atom-octal-literal","shortDescription":{"text":"atom-octal-literal"},"properties":{"builtin":true,"capped":false,"applicable":true}}, -… [10017 more display lines; full output is 1403377 bytes on 1 raw line(s)] +… [10019 more display lines; full output is 1403667 bytes on 1 raw line(s)] ````` Parsed summary of the same SARIF (past the display cut): ````` -sarif 2.1.0 rules= 39 results= 5004 +sarif 2.1.0 rules= 39 results= 5005 ````` ## `./build/ripwire . --lint --sarif --limit=5` @@ -5902,11 +5774,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* -**wall time: 4.61s** +**wall time: 4.45s** ````` -

@@ -5917,14 +5789,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* -**wall time: 8.57s** +**wall time: 10.09s** ````` - - -

-

+ + +

+

````` @@ -5932,14 +5804,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The range form: tree HEAD vs tree HEAD~3.* -**wall time: 8.87s** +**wall time: 9.73s** ````` - -

-

+ +

+

````` @@ -5947,11 +5819,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* -**wall time: 1.93s** +**wall time: 1.79s** ````` - + @@ -5959,9 +5831,9 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M - + - + @@ -5987,49 +5859,49 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect.* -**wall time: 1.19s** +**wall time: 1.44s** ````` - - - + - + + + + + + - - - - + - - + + + - - + ````` -## `./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html --color-by=community` +## `./build/ripwire . --html=/aux/map2.html --color-by=community` *The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* -**wall time: 1.43s** +**wall time: 1.62s** ````` (empty) @@ -6038,14 +5910,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M Artifact written: ````` - 158888 ./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html + 158888 /aux/map2.html ````` -## `./build/ripwire . --index-out=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index` +## `./build/ripwire . --index-out=/aux/ci_index` *CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* -**wall time: 4.52s** +**wall time: 5.03s** ````` (empty) @@ -6054,26 +5926,26 @@ Artifact written: stderr: ````` -ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache (18643301 bytes, lean family) -ripwire: --index-out wrote ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache (39963389 bytes, rich family) +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (18649971 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (39975434 bytes, rich family) ````` Artifact written: ````` - 18643301 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache - 39963389 ./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.rich.ripwirecache - 58606690 total + 18649971 /aux/ci_index.lean.ripwirecache + 39975434 /aux/ci_index.rich.ripwirecache + 58625405 total ````` -## `./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index.lean.ripwirecache --top-k=3` +## `./build/ripwire . --cache=/aux/ci_index.lean.ripwirecache --top-k=3` *Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* ````` - + @@ -6086,14 +5958,14 @@ Artifact written: ````` -## `./build/ripwire . --pin-census=./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv --top-k=3` +## `./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3` *Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* ````` - + @@ -6117,7 +5989,7 @@ Artifact written: # whose template-argument list spans lines keeps its line break as \n instead of splitting the row. # line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): # the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. - 114912 ./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv + 114971 /aux/pin_census.tsv ````` ## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` @@ -6128,7 +6000,7 @@ Artifact written: ````` - + @@ -6150,7 +6022,7 @@ Artifact written: ````` - + @@ -6162,15 +6034,15 @@ Artifact written: *--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* -**exit code: 1** — **wall time: 5.63s** +**exit code: 1** — **wall time: 2.78s** ````` - - + - - + + + untested blast radius ; exit 4 while either exists. window: total= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). tests_capped=/untested_capped=: 1 = cut. counts_floor=1: every count is a FLOOR, never a total. graph_ambiguous=/graph_unresolved=: resolver gauge. graph_unindexed=N: N files no grammar could read (the map header's unindexed=); their calls raise neither gauge. declined_calls=K: K call sites left unbound (no evidence chose one def), in no count or row. at=: commit+dirty+shallow. root=: p= relative to it. next=: the one pasteable follow-up. impacted=N: symbols that transitively call the change (changed symbols excluded). shown_tests=/shown_untested=: t rows and u rows printed, two independent counts. script_gates_unmodelled=N: test/*.sh runners in the corpus, a path count; not call-graph modelled. run_first=N: the first N t rows have the most direct evidence (changed/partner/hops=1, else nearest hops=); not a skip list. script_gates_registered=N: shell gates test/regression.sh registers as suite members. script_gates_mapped=N: registered gates with exact dependency evidence (literal paths or RIPWIRE_TEST_DEPS). script_gates_unresolved_dynamic=N: registered gates with no mappable deps; they may cover the change unlisted. ccx_bar=N: the cognitive-complexity bar a u row's ccx= is read against. untested_modscope=N: owners excluded from untested= (#324, uncallable); still in impacted=. tests=/untested=: tests to run (t total) / impacted symbols no test reaches (u total). --> @@ -6283,11 +6155,11 @@ at: 75cc95803+dirty *Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* -**exit code: 2** — **wall time: 22.76s** +**exit code: 2** — **wall time: 22.67s** ````` - @@ -6316,7 +6188,7 @@ at: 75cc95803+dirty -… [115 more display lines; full output is 15390 bytes on 1 raw line(s)] +… [116 more display lines; full output is 15456 bytes on 1 raw line(s)] ````` stderr: @@ -6330,11 +6202,11 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend.* -**exit code: 2** — **wall time: 10.77s** +**exit code: 2** — **wall time: 8.55s** ````` - @@ -6363,7 +6235,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap -… [115 more display lines; full output is 20204 bytes on 1 raw line(s)] +… [116 more display lines; full output is 20270 bytes on 1 raw line(s)] ````` stderr: @@ -6377,10 +6249,10 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* -**exit code: 2** — **wall time: 15.12s** +**exit code: 2** — **wall time: 8.82s** ````` -{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":133,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"75cc95803+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":134,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"c7920353a+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] {"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, @@ -6410,7 +6282,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap {"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, {"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, {"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, -… [112 more display lines; full output is 13998 bytes on 1 raw line(s)] +… [113 more display lines; full output is 14067 bytes on 1 raw line(s)] ````` stderr: @@ -6424,7 +6296,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* -**exit code: 1** — **wall time: 6.86s** +**exit code: 1** — **wall time: 5.63s** ````` (empty) @@ -6442,7 +6314,7 @@ ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written *NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* -**wall time: 7.83s** +**wall time: 5.59s** ````` (empty) @@ -6460,11 +6332,11 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already *Re-run after the partial ack: acked=3, the rest still gate (exit 2).* -**exit code: 2** — **wall time: 10.20s** +**exit code: 2** — **wall time: 11.33s** ````` - @@ -6493,7 +6365,7 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already -… [114 more display lines; full output is 15093 bytes on 1 raw line(s)] +… [115 more display lines; full output is 15159 bytes on 1 raw line(s)] ````` stderr: @@ -6523,11 +6395,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6539,11 +6411,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire row identical.* -**wall time: 1.46s** +**wall time: 1.33s** ````` - @@ -6555,11 +6427,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6595,13 +6467,13 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). changed=K: K indexed git-changed files seed the PageRank teleport (0: uniform, incl. no git). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - - + + @@ -6635,11 +6507,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire of ; dup_loc=/dup_pct=. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). root=: p= relative to it. groups=/type3=: Type-2 and Type-3 group totals over all groups; total= is their sum. exempt_groups=N: groups whose members all sit on fixture/shell-runner paths quality-delta duplication ignores. idiom_groups=/demoted_groups=: groups of one recognized idiom / those quality-delta demotes to minor; floors. clone_groups=N: clusters after merging pairs (rows sharing gid=); a floor under type3_capped=1. total_loc=N: lines of every function body the detector considered; dup_pct= is dup_loc= over it. group exempt=fixture|shell-runner: every member is on such a path; quality-delta duplication ignores it. group idiom=: the recognized shape every member spells: threshold-ladder, switch-name-table, builder-chain. --> - + @@ -6675,12 +6547,12 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire ; v=unknown = no merge base. at=: commit+dirty+shallow. head=: the HEAD commit, bare 9-hex sha (at= adds +dirty). head_ref=: HEAD's branch (HEAD when detached); that branch itself is not scanned. refs=N: local branches scanned (refs/heads only); unmerged + superseded + merged + unknown = refs. blobs=N: distinct git blobs read for the sweep. unmerged=N: refs whose authored work the live line genuinely lacks. superseded=N: refs whose work the live line re-implemented (removed the same base code). merged=N: refs whose work HEAD already has; omitted from the rows. name=: the local branch. tip=: the branch tip commit (9 hex). date=: the tip's committer date, YYYY-MM-DD. files=N: files with stray lines; rows capped at 12, a more element counts the rest (detail=1 lists all). ref superseded=N>: of this ref's stray= lines, those in files the live line re-implemented. unknown=N: refs that could not be analysed (v=unknown, e.g. no merge base); never counted merged. --> - - + + ````` @@ -6688,18 +6560,18 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire row rather than silently dropping them.* -**wall time: 3.86s** +**wall time: 2.57s** ````` - + ````` @@ -6707,18 +6579,18 @@ stderr: stderr: ````` -[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1171, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1173, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) ````` ## `./build/ripwire . --dmm` *The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* -**wall time: 4.08s** +**wall time: 3.73s** ````` -

@@ -6729,11 +6601,11 @@ stderr: *OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* -**wall time: 10.72s** +**wall time: 9.69s** ````` - @@ -6762,7 +6634,7 @@ stderr: -… [117 more display lines; full output is 15360 bytes on 1 raw line(s)] +… [118 more display lines; full output is 15426 bytes on 1 raw line(s)] ````` stderr: @@ -6775,7 +6647,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* -**exit code: 1** — **wall time: 8.51s** +**exit code: 1** — **wall time: 5.72s** ````` (empty) @@ -6795,11 +6667,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* -**wall time: 4.95s** +**wall time: 2.56s** ````` - + @@ -6824,11 +6696,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr - - - - - + + + + + ````` @@ -6837,10 +6709,8 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* -**wall time: 1.57s** - ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 keep this branch-free — it sits inside the PageRank sort comparator 75cc95803667a285f5899c2a6218343bea65f8b4 lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 keep this branch-free — it sits inside the PageRank sort comparator c7920353a6d41f95f6ad61193358a10a3b255acf lane/lean-answers-068 ````` stderr: @@ -6865,7 +6735,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort - + @@ -6882,7 +6752,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort *Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry.* -**exit code: 1** — **wall time: 1.04s** +**exit code: 1** ````` (empty) @@ -6917,7 +6787,7 @@ ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByS *The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 75cc95803667a285f5899c2a6218343bea65f8b4 lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort c7920353a6d41f95f6ad61193358a10a3b255acf lane/lean-answers-068 ````` ## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` @@ -6961,15 +6831,15 @@ src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-07 chose the flat The element on the same output — past the 30-line display cut above: ````` - + - + ````` -## `./build/ripwire . --replace-symbol-body=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/empty_payload.h` +## `./build/ripwire . --replace-symbol-body=lessByScoreDescId --edit-payload=/aux/empty_payload.h` *An EMPTY payload refuses — it never implies deletion.* @@ -6985,11 +6855,11 @@ stderr: ripwire: --edit-payload is empty; empty never means delete ````` -## `./build/ripwire . --replace-symbol-body=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_lessByScoreDescId.h` +## `./build/ripwire . --replace-symbol-body=lessByScoreDescId --edit-payload=/aux/payload_lessByScoreDescId.h` *Whole-symbol replace without a whole-file read: the payload is the ORIGINAL flat body, so this edit undoes the sandbox's deep-nesting regression. The receipt's span is the POST-edit byte range; replaced_bytes counts the old bytes overwritten.* -**wall time: 6.68s** +**wall time: 4.87s** Input file: @@ -7006,12 +6876,12 @@ inline bool lessByScoreDescId( const std::vector& scores, std::uint32_t a ````` {"applied":"replace_symbol_body","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2162,"end":2375},"lines":{"start":49,"end":56},"replaced_bytes":1718,"old_file_bytes":12329,"new_file_bytes":10824,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separa … [line truncated: 726 more bytes on this line] -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ````` stderr: @@ -7024,15 +6894,15 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *The closed loop: contract unchanged (same params, same publicness) after the replace — nothing provably incompatible.* -**wall time: 1.97s** +**wall time: 1.42s** ````` - + - + @@ -7042,20 +6912,20 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, ````` -## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h` +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h` *Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* -**wall time: 5.27s** +**wall time: 3.09s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ````` stderr: @@ -7064,11 +6934,11 @@ stderr: ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: bash test/greptiercheck.sh ````` -## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --no-post-check` +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check` *The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* -**wall time: 2.08s** +**wall time: 1.47s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] @@ -7080,23 +6950,23 @@ stderr: ripwire edit: applied atomically; receipt carries region, blob_sha (post-check skipped); next: --edit-check=src/infra/sortutil.h:lessByScoreDescId ````` -## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` +## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` *Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* -**wall time: 5.53s** +**wall time: 3.37s** ````` {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ````` stderr: @@ -7105,11 +6975,11 @@ stderr: ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: --uses=src/infra/sortutil.h:nonNegativeFloatDescKey ````` -## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h` +## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h` *An unknown TARGET refuses and leaves every file byte-identical.* -**exit code: 1** — **wall time: 2.95s** +**exit code: 1** — **wall time: 1.49s** ````` (empty) @@ -7121,11 +6991,11 @@ stderr: ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found ````` -## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --dry-run` +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --dry-run` *A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* -**wall time: 1.84s** +**wall time: 1.47s** Input file: @@ -7143,24 +7013,24 @@ Input file: ````` ````` -{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 354 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 354 more bytes on this line] ````` -## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --apply` +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` *The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* -**wall time: 4.35s** +**wall time: 3.02s** ````` -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true}}]} +"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true}}]} ````` -## `./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json` +## `./build/ripwire . --edit-plan=/aux/edit_plan.json` *Neither --dry-run nor --apply: the mode is explicit, so this refuses.* @@ -7180,11 +7050,11 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply *After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* -**exit code: 2** — **wall time: 11.55s** +**exit code: 2** — **wall time: 9.05s** ````` - @@ -7212,8 +7082,8 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply - -… [111 more display lines; full output is 14457 bytes on 1 raw line(s)] + +… [112 more display lines; full output is 14523 bytes on 1 raw line(s)] ````` stderr: @@ -7227,7 +7097,7 @@ ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: du *REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* -**exit code: 1** — **wall time: 7.54s** +**exit code: 1** — **wall time: 7.01s** ````` (empty) @@ -7246,7 +7116,7 @@ ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against *The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* -**wall time: 11.71s** +**wall time: 10.01s** ````` (empty) @@ -7256,16 +7126,16 @@ stderr: ````` ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") -ripwire: wrote ./.ripwire_quality_baseline (snapshot of 23136 per-symbol rows, 23851 indexed symbols total) +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 23138 per-symbol rows, 23853 indexed symbols total) ````` Artifact written: ````` - 3875998 .ripwire_quality_baseline + 3876272 .ripwire_quality_baseline # ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit -producer 384869b1e64dca97276ab209b6d3fb50c9f58098e7eadb0088f175cda691d61e -head 75cc95803667a285f5899c2a6218343bea65f8b4 +producer 075ad01d2576bcb1b0bdac7efeb7b3b61d37df7c7fb9adbf3794b90643e96ba0 +head c7920353a6d41f95f6ad61193358a10a3b255acf dirty 1 absorbed 2 ccx d19d7c13cbc7 2 @@ -7278,11 +7148,11 @@ ccx 7d8862be4e *Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* -**wall time: 10.92s** +**wall time: 11.55s** ````` - @@ -7305,13 +7175,13 @@ ccx 7d8862be4e - - + + -… [106 more display lines; full output is 13322 bytes on 1 raw line(s)] +… [107 more display lines; full output is 13388 bytes on 1 raw line(s)] ````` @@ -7352,7 +7222,7 @@ ccx 7d8862be4e {"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 1933 more bytes on this line] {"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] {"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] -{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1364 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1387 more bytes on this line] {"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] {"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] {"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] @@ -7363,14 +7233,14 @@ ccx 7d8862be4e The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: ````` -tools= 33 manifest_bytes= 46880 (~tokens at 4 bytes/token: 11720 ) +tools= 33 manifest_bytes= 46903 (~tokens at 4 bytes/token: 11725 ) batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] explore desc_bytes= 1322 schema_bytes= 774 required=['task'] edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] flags desc_bytes= 881 schema_bytes= 720 required=[] +whereis desc_bytes= 701 schema_bytes= 851 required=['symbol'] exemplar desc_bytes= 954 schema_bytes= 591 required=[] -whereis desc_bytes= 701 schema_bytes= 828 required=['symbol'] replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] @@ -7433,87 +7303,77 @@ quality_delta desc_bytes= 772 schema_bytes= 193 required=[] *MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* -**wall time: 2.19s** +**wall time: 1.25s** ````` {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= total= capped= has_more= next_offset= offset= limit= (ca … [line truncated: 14122 more bytes on this line] -"_index":"[index: files=2484 symbols=23849 hash=780d3b8f]","_fresh":"ok"}} -{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"", i ); i = len( t ) if j < 0 else j + 3 + return i +def est( t ): + m = re.search( r']*\sest_tokens="([0-9]+)"', t ); return int( m.group( 1 ) ) if m else -1 +d, f = doc( sys.argv[ 1 ] ), doc( sys.argv[ 2 ] ) +over = 1 if re.match( r'\s*]*\sover_ceiling="1"', d ) else 0 +nxt = 1 if re.search( r']*\snext="', d ) else 0 +print( header( d ), header( f ), est( d ), est( f ), over, nxt ) +PY +# p1HeaderLarger DEFAULT_HDR FULL_HDR — the exemption's header comparison, ONE spelling (arm (P1-B) pins it, and a mutant of it). +p1HeaderLarger(){ [ "$1" -gt "$2" ]; } +# p1Exempt DEF FULL BUDGET — 0 when a lost row is the ruled exemption; leaves the facts it read in $p1facts either way. Every +# field is checked present and numeric before any comparison (CHECKLIST 14): an unreadable answer is never exempt. +p1Exempt(){ + set -- $( python3 "$TMP/p1d.py" "$1" "$2" ) "$3" + if [ $# -ne 7 ] || ! printf '%s' "$1$2$3$4$5$6$7" | grep -qE '^[0-9]+$'; then + p1facts="unreadable answer ($*)"; return 1 + fi + p1facts="header $1 B vs full's $2 B, est_tokens $3 (full $4) of $7, over_ceiling=$5, sigs next=$6" + p1HeaderLarger "$1" "$2" && [ "$3" -le "$7" ] && [ "$5" -eq 0 ] && [ "$6" -eq 1 ] +} +p1n=0; p1bad=0; p1more=0; p1exempt=0 for task in "rank symbols by pagerank" "token budget ceiling ladder" "parse command line flags" "legend posture compact" "escapeXml"; do for tb in 700 1500 3000 6000; do "$BIN" "$ROOT/src" --for="$task" --token-budget=$tb >"$TMP/p1.def" 2>/dev/null @@ -772,13 +815,18 @@ for task in "rank symbols by pagerank" "token budget ceiling ladder" "parse comm set -- $( python3 "$TMP/rows.py" "$TMP/p1.def" "$TMP/p1.full" | tr -d "[](),'" ) p1n=$(( p1n + 1 )) if [ "${3:-1}" -ne 0 ]; then - p1bad=$(( p1bad + 1 )); no "(P1) --for='$task' --token-budget=$tb: the default LOST $3 answer row(s) that --legend=full carries (default $1, full $2 symbol rows): ${4:-} ${5:-} ${6:-}" + p1lost="the default LOST $3 answer row(s) that --legend=full carries (default $1, full $2 symbol rows): ${4:-} ${5:-} ${6:-}" + if p1Exempt "$TMP/p1.def" "$TMP/p1.full" "$tb"; then + p1exempt=$(( p1exempt + 1 )); printf ' .. (P1 exempt, ruling B) --for=%s --token-budget=%s: %s; %s\n' "'$task'" "$tb" "$p1lost" "$p1facts" + else + p1bad=$(( p1bad + 1 )); no "(P1) --for='$task' --token-budget=$tb: $p1lost — not the ruled exemption ($p1facts)" + fi fi [ "${1:-0}" -gt "${2:-0}" ] && p1more=$(( p1more + 1 )) grep -q ']*capped="1"' "$TMP/p1.full" && head -c 300 "$TMP/p1.def" | grep -qF 'schema="ripwire.for/v1"' && p1bind=$(( ${p1bind:-0} + 1 )) done done -[ "$p1bad" -eq 0 ] && [ "$p1n" -eq 20 ] && ok "(P1) rows(default) ⊇ rows(--legend=full) on all $p1n budgeted --for runs (5 tasks x 4 budgets)" +[ "$p1bad" -eq 0 ] && [ "$p1n" -eq 20 ] && ok "(P1) rows(default) ⊇ rows(--legend=full) on all $p1n budgeted --for runs (5 tasks x 4 budgets) but the $p1exempt the ruled exemption names (header larger than full's, fits its budget, the row paid for )" # THE BINDING GUARD (L1 fix round): it asked for a run where the default carries MORE rows. Since the fix round the compact # header defines task=/next=/pure=/ and its sig charge is capped at the full dialect's (verbs_for.h runForLens), so # the default buys no row it cannot pay for and "more" is no longer the claim. What the superset arm needs is that the @@ -786,6 +834,83 @@ done [ "${p1bind:-0}" -gt 0 ] && ok "(P1) the arm binds: on ${p1bind} of $p1n runs the budget cut the full answer's and the default is the compact posture (default carries more rows on $p1more)" \ || no "(P1) on none of $p1n runs did the budget cut the full answer's under a compact default — the superset arm proved nothing" +# (P1-B) THE EXEMPTION, PINNED (knob-honesty-068 round 3, ruling B). Three parts: +# 1. the header comparison on synthetic sizes — larger is exempt, a tie and a smaller header are not (a mutant of +# p1HeaderLarger goes red here even where no live argv lands on the boundary); +# 2. FIXED ARMS on a SHORT RELATIVE root (`src`, run from $ROOT): the sweep above runs on "$ROOT/src", and that path rides the +# header the rows are budgeted against, so a checkout path longer than ~52 chars hid these two (green locally, red on CI's +# shorter path at 4acbcd22: default 7 vs full 8, 9 vs 10). Each must keep every row full keeps OR be the ruled exemption, +# and P4 holds on both whatever the branch: inside the budget, or no further over it than full; +# 3. the TWIN on the same root: at --token-budget=6000 the compact header is the SMALLER one, so the exemption cannot apply +# and P1 holds strictly. +# …and a sentinel: MCP `for` has ONE dialect (it declares no `legend` field, src/mcprefusal.h; the compact layer never runs on +# it), so no compact header exists there to exceed full's; it goes red the day MCP `for` gains a compact dialect. +p1bbad=0 +if p1HeaderLarger 1201 1000 && ! p1HeaderLarger 1000 1000 && ! p1HeaderLarger 900 1000; then + ok "(P1-B) the exemption's header comparison: a larger default header is exempt, a tie and a smaller one are not" +else + p1bbad=$(( p1bbad + 1 )); no "(P1-B) the exemption's header comparison is not 'strictly larger' (1201>1000 exempt, 1000=1000 and 900<1000 not)" +fi +# …and the other two conditions, on synthetic answers (each a near miss the predicate could wave through): the positive is a +# larger header that fits and paid for next=; over the budget, labelled over_ceiling="1", or with no is not exempt. +p1syn(){ printf '' "$1" "$2" "$3" "$4" >"$TMP/p1s.def"; } +printf '' >"$TMP/p1s.full" +p1synbad="" +p1syn 1450 "" "a much longer legend comment" ' next="x"'; p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" 1500 || p1synbad="$p1synbad positive($p1facts)" +p1syn 1550 "" "a much longer legend comment" ' next="x"'; p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" 1500 && p1synbad="$p1synbad over-budget" +p1syn 1450 ' over_ceiling="1"' "a much longer legend comment" ' next="x"'; p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" 1500 && p1synbad="$p1synbad over_ceiling" +p1syn 1450 "" "a much longer legend comment" ""; p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" 1500 && p1synbad="$p1synbad no-next" +p1syn 1450 "" "tiny" ' next="x"'; p1Exempt "$TMP/p1s.def" "$TMP/p1s.full" 1500 && p1synbad="$p1synbad smaller-header" +if [ -z "$p1synbad" ]; then + ok "(P1-B) the exemption on synthetic answers: exempt only when the header is larger AND it fits AND rides (4 near misses refused)" +else + p1bbad=$(( p1bbad + 1 )); no "(P1-B) the exemption on synthetic answers misjudged:$p1synbad" +fi +p1b_arm(){ # p1b_arm TASK BUDGET MODE(either|strict) + ( cd "$ROOT" && "$BIN" src --for="$1" --token-budget="$2" ) >"$TMP/p1b.def" 2>/dev/null + ( cd "$ROOT" && "$BIN" src --for="$1" --token-budget="$2" --legend=full ) >"$TMP/p1b.full" 2>/dev/null + local task="$1" tb="$2" mode="$3" + set -- $( python3 "$TMP/rows.py" "$TMP/p1b.def" "$TMP/p1b.full" | tr -d "[](),'" ) + local drows="${1:-}" frows="${2:-}" lost="${3:-}" + set -- $( python3 "$TMP/p1d.py" "$TMP/p1b.def" "$TMP/p1b.full" ) + if [ $# -ne 6 ] || ! printf '%s' "$1$2$3$4$drows$frows$lost" | grep -qE '^[0-9]+$'; then + p1bbad=$(( p1bbad + 1 )); no "(P1-B) --for='$task' --token-budget=$tb on root src: could not read both answers (a root, est_tokens=, rows) — got '$*' rows='$drows/$frows/$lost'"; return + fi + local dh="$1" fh="$2" de="$3" fe="$4" why="" + [ "$de" -gt "$tb" ] && [ "$de" -gt "$fe" ] && why="$why P4: est_tokens $de over the budget AND over full's $fe;" + if [ "$mode" = strict ]; then + [ "$lost" -ne 0 ] && why="$why P1: lost $lost row(s) full carries (default $drows, full $frows);" + p1HeaderLarger "$dh" "$fh" && why="$why twin premise: the compact header ($dh B) is no longer the smaller one (full's $fh B);" + branch="P1 strict, header $dh B < full's $fh B" + elif [ "$lost" -ne 0 ]; then + if p1Exempt "$TMP/p1b.def" "$TMP/p1b.full" "$tb"; then branch="exempt ($p1facts)" + else why="$why P1: lost $lost row(s) full carries (default $drows, full $frows) and is not the ruled exemption ($p1facts);"; fi + else + branch="P1 holds" + fi + if [ -n "$why" ]; then + p1bbad=$(( p1bbad + 1 )); no "(P1-B) --for='$task' --token-budget=$tb on root src:$why" + else + ok "(P1-B) --for='$task' --token-budget=$tb on root src: rows $drows (full $frows), est $de (full $fe): $branch" + fi +} +p1b_arm "rank symbols by pagerank" 1500 either +p1b_arm "parse command line flags" 1500 either +p1b_arm "rank symbols by pagerank" 6000 strict +printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":"src","task":"parse command line flags","budget_tokens":1500}}}' \ + | ( cd "$ROOT" && "$BIN" --mcp 2>/dev/null ) | tail -1 >"$TMP/p1b.mcp" +if grep -q ' answer — the sentinel proves nothing" +fi +[ "$p1bbad" -eq 0 ] && ok "(P1-B) the ruled exemption is pinned: its comparison, the short-root fixed arms, the scope twin and the MCP sentinel" + # (P2) the default map's --token-budget gate decides on the price the default PRINTS. It used to decide on the full # dialect's price before the compact layer ran, so a map that fits once compacted was withheld (exit 3). Budget = the # compact price itself: the default must answer (exit 0), --legend=full must still withhold (exit 3), and the full price diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 68dc86ac7..2695f15bb 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -38,5 +38,5 @@ safe_delete 0 8b99d83566b89332ac4b2967abbb39284db775bbed601628b97b8c7909f04ac2 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 e40ee1ddba0fbf548c58c98b91fe052af8c34f8913edb3edd85245b12a6967b9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 4b9537e1185b3bffe91a73c4cd5513dc9942b84a98f31b2a42b47a7c1f70480d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 b0dd219c7cf9ff8bddcb612680dddcf5f5335236d8460f16744d6f20b7dde4a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 938ff4f8802a8f5d9cb10f9a1f3804ac755be44c8ab00e24e45bfe8255224a21 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From e0b22cabaa33a411db4b0a1b92841d7f45e8ebf3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 05:50:44 -0400 Subject: [PATCH 188/229] =?UTF-8?q?test:=20countfloorcheck=20(GO)=20?= =?UTF-8?q?=E2=80=94=20Go=20t=3D"type"=20and=20t=3D"functype"=20floor=20li?= =?UTF-8?q?ke=20a=20Go=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three kinds honesty-small-068 appended read UseForm::NotCalls in kUseFormOfKind (merged with count-floor-068): a conversion is one call, a type mention is never counted, so grep carries callers_floor="1" and floor_next= beside the count, as the Go struct beside them does. Co-Authored-By: Claude Opus 5.5 --- test/countfloorcheck.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/countfloorcheck.sh b/test/countfloorcheck.sh index b1a1085e0..caaff53ca 100644 --- a/test/countfloorcheck.sh +++ b/test/countfloorcheck.sh @@ -43,6 +43,9 @@ # their derived type); (near miss) a Rust impl for a type the tree never defines still floors. # M MCP twins: grep's `enclosing` JSON carries callers_floor/floor_next exactly where the CLI does; the MCP # `for` lego block equals the CLI's. +# GO grep on the Go kinds honesty-small-068 added (t="type" `type N string`, t="functype"): read as a struct +# is (a conversion is one call, a type mention is never counted) => callers_floor="1" + floor_next=, like the +# Go struct beside them (the kinds' rows in countfloor.h kUseFormOfKind, train 26b). # N legend: every new attribute is defined in the answer that carries it (compact and full dialects). # O determinism (x2) + xmllint. # @@ -376,6 +379,24 @@ MCP_F="$( mcp_call for "{\"path\":\"$W/tsl\",\"task\":\"which router classes imp CLI_F="$( run tsl '--for=which router classes implement the Router interface' --sections=lego | grep -oE '.*' | head -1 )" if [ -n "$CLI_F" ] && [ "$MCP_F" = "$CLI_F" ]; then ok "(M) MCP for == CLI --for (implementors_floor included)"; else no "(M) lego twin differs: cli='$CLI_F' mcp='$MCP_F'"; fi +# ── GO: the Go named-type kinds floor as a struct does ────────────────────────────────────────────────────── +echo "=== GO: grep on Go t=\"type\" / t=\"functype\" floors like the Go struct ===" +mkdir -p "$W/go" +printf 'package m\n\ntype TestName string\n\ntype Pair struct{ a int }\n\ntype Handler func(int) int\n\nfunc Use(s string) int {\n\tn := TestName(s)\n\tvar p Pair\n\t_ = p\n\treturn len(n)\n}\n\nfunc Wrap(h Handler) int { return h(1) }\n' > "$W/go/m.go" +MAP_GO="$( run go )" +for kn in "type:TestName" "struct:Pair" "functype:Handler"; do + printf '%s' "$MAP_GO" | grep -q " row"; continue; } + if [ "$( attr "$E_GO" callers_floor )" = 1 ] && [ "$( attr "$E_GO" floor_next )" = "--grep=$n" ]; then + ok "(GO) callers=\"$( attr "$E_GO" callers )\" carries callers_floor=\"1\" floor_next=\"--grep=$n\"" + else + no "(GO) printed its count as a total: $E_GO" + fi +done + # ── N: legend ──────────────────────────────────────────────────────────────────────────────────────────── echo "=== N: every new attribute is defined where it rides ===" for dialect in compact full; do From 550feb6c0db4c300a7fa560b00276d4d226532a9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 05:53:17 -0400 Subject: [PATCH 189/229] docs: name the two honesty-small floors; kParserVer 156 in the versions note - A Go 1.24 generic alias `type A[T any] = B` reads t="type", not t="alias": the grammar gives it no alias node (CHANGELOG Go kinds entry and the model.h NamedType note). - A --path gap row's next= names the row by file and name, so two same-named definitions in one file both serve. - The versions paragraph states kParserVer 124 -> 156: the Go kinds and the train 25 re-review extraction fixes each took 145 on their branches and land at 156, above every branch build in flight (up to 154, rich tag 155). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +++++++++---- src/model.h | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b734bcdab..a91c74331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -767,7 +767,8 @@ says the search was incomplete: `searched=` (the symbols it reached), `gaps="dec `gap_syms=` with up to three `` rows (nearest `from=` first, `gap_syms_capped="1"` past that) and a `next=` that expands their bodies. A `` row is where the search could not see, never a hop. Ambiguous calls are not counted (every candidate has an edge the search follows), nor are calls to names defined nowhere in the tree. When the -search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. Gate: +search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. A floor: +the `next=` names a gap row by file and name, so where one file holds two definitions of that name it serves both. Gate: `test/pathgapcheck.sh`. ### Fixed — a Go named type's `t=` says what it is: `type`, `functype` or `alias`, not `struct` @@ -779,7 +780,9 @@ Every Go `type_spec` was labelled `t="struct"`, so `type TestName string` and `t `type ( … )` blocks and generic types follow the same rule. The three new kinds behave as `struct` did everywhere else (a conversion `TestName( s )` keeps its caller edge), `--graph-query`'s `kind()` accepts them, and the map's `--legend=full` gains a clause for them only on a tree that has one. Only Go is split: other languages' typedefs, aliases and enums still -read `t="struct"`. `kParserVer` 143 → 145, so every ingest cache is re-indexed once. Gate: `test/gokindcheck.sh`. +read `t="struct"`. A floor: a Go 1.24 generic alias `type A[T any] = B` reads `t="type"`, not `t="alias"`, because +the grammar gives it no alias node. `kParserVer` moves (see the versions note), so every ingest cache is re-indexed once. +Gate: `test/gokindcheck.sh`. ### Fixed — a count the index cannot vouch for is marked a floor beside the number; safe-delete rows carry the call line @@ -1306,7 +1309,7 @@ per page instead. Gate: `test/impactdepthcheck.sh`. ### Changed — the versions this release moves, stated once -`kParserVer` 124 → 148 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier +`kParserVer` 124 → 156 (the function-literal fix takes 128; #338 and #325 take 129; the body-less C/C++ type-specifier span fix and the TypeScript `await f(x)` / `!f(x)` calls each took a number of their own on their branches, as did the false-edge resolution (134, 135), the value-reference rows (140) and the Ruby method-lookup changes (#373: one per step from 130 to 137 — bare-word calls, mixins, typed receivers, RSpec targets, Rails declared calls, Rake and Jbuilder @@ -1314,7 +1317,9 @@ files, RSpec matcher chains, class objects — then 142 and 145 for its rebase a change); 141 sat above every number a branch build of unreleased work had used, and the review fixes to the global-object shadow and the value-reference slot text take 143, above 141's full-use file tag 142. The Ruby method lookup lands at 148, renumbered from its branch's 145: other branch builds in flight have used up to 147, and 148's full-use file tag is -149, so no cache such a build wrote is read as this release's), `kCacheVersion` 25 → 28 +149. The Go named-type kinds and the train 25 re-review's extraction fixes each took 145 on their branches and land +together at 156: branch builds in flight have used up to 154, whose full-use file tag is 155, so no cache such a build +wrote is read as this release's), `kCacheVersion` 25 → 28 (the function-literal fix's record changes, then the false-edge fix's member-call fields; the Ruby branch's appended binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 17 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions diff --git a/src/model.h b/src/model.h index c0848a5d7..ca15dd299 100644 --- a/src/model.h +++ b/src/model.h @@ -68,7 +68,8 @@ inline constexpr NodeId kNoNode = 0xFFFFFFFFu; // NamedType / Alias / FuncType (honesty-small-068) are APPENDED after ModuleScope so no existing kind renumbers. They // split the old `@definition.type` → Struct bucket where a grammar's syntax PROVES the type's form, which today is Go // alone: `type N string` / `type L[T any] []T` (a defined type over a non-struct, non-interface, non-func form, or over -// another named type) is t="type", `type F func(...)` is t="functype", `type A = B` is t="alias". A Go struct or +// another named type) is t="type", `type F func(...)` is t="functype", `type A = B` is t="alias" (a floor: a Go 1.24 +// generic alias `type A[T any] = B` reads t="type", because the grammar gives it no type_alias node). A Go struct or // interface keeps t="struct"/t="iface". Every other language's @definition.type capture still maps to Struct (C/C++ // typedef and enum, TS/Swift/Dart aliases, Java/C#/PHP/GDScript enums, Elixir @type) — a disclosed floor, not a claim // that those are structs. The three kinds BEHAVE as Struct did everywhere a predicate reads the kind From 27feb1248b87173ae8a0a3b57a99c45be17dadde Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 06:25:12 -0400 Subject: [PATCH 190/229] fix(hooks): the prompt hooks' lead strip is the six ASCII bytes, in every locale Both prompt-router hooks stripped a prompt's leading whitespace with bash's [:space:], which in a UTF-8 locale (macOS bash 3.2) also matches U+00A0, U+2003, U+3000 and other Unicode spaces. src/taskroute.h's classifier strips only space, \t, \n, \v, \f and \r, so a prompt led by U+00A0 and a --- CHANGELOG.md | 4 +++- hooks/ripwire-claude-route.sh | 6 ++++-- hooks/ripwire-codex-route.sh | 4 +++- src/taskroute.h | 3 ++- test/codexpromptroutecheck.sh | 11 +++++++++++ test/routehookcheck.sh | 22 +++++++++++++++++++++- 6 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a52f7ce..115c424a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -851,7 +851,9 @@ and the `--help-task` classifier now treat a prompt that starts with ``: no classification, a `skip-system` log row. ``, `` and a prompt that merely mentions a wrapper mid-sentence still route; a prompt that itself starts with a literal `` or `` fragment (a pasted RSS fragment without its prolog, say) is treated as a harness event and is not routed. The -classifier's leading-whitespace strip now also covers `\v` and `\f`, as the hooks' strip always did. `meter.conf` +classifier's leading-whitespace strip now also covers `\v` and `\f`, as the hooks' strip always did, and the hooks' +strip is now those same six ASCII bytes in every locale: it used bash's `[:space:]`, which in a UTF-8 locale also +stripped a leading U+00A0 (and other Unicode spaces), so a hook skipped a prompt the classifier routes. `meter.conf` parsing, in `resolve_arm` of both router hooks and in the meter, no longer loses a final line that has no newline and no longer reads a CRLF value (`arm=control\r`) as `treatment`. `bench/routing_ab_report.py` prints per-arm counts, says `NO CONTROL ARM` when an arm has no prompts, and marks the 2026-09-02 registration window void (it had no control arm); the readout clock restarts at this release, and the report prints diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index a7acd8240..dc48ecbaf 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -394,7 +394,7 @@ case "$promptBytes" in ''|*[!0-9]*) exit 0;; esac # newline in `rest` and missed the case match below — the guard then fell through to actually invoke the # classifier (a real subprocess), log status="abstain" instead of status="skip-system", and delete an # existing session's routing-pending file, none of which a harness/system event should ever cause. This -# bash parameter-expansion form strips the FULL leading run of [:space:] bytes (space/tab/newline/CR) from +# bash parameter-expansion form strips the FULL leading run of whitespace bytes (space/tab/newline/VT/FF/CR) from # the whole string in one pass, not line by line. # ---- BEGIN MIRRORED BLOCK rw_is_harness_event (issue #381) ------------------------------------------------ # KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh and hooks/ripwire-codex-route.sh; test/routehookcheck.sh @@ -415,7 +415,9 @@ rw_is_harness_event() return 1 } # ---- END MIRRORED BLOCK rw_is_harness_event --------------------------------------------------------------- -lead="${prompt%%[![:space:]]*}" +# The lead strip is the six ASCII bytes src/taskroute.h's looksLikeSystemEvent strips (space \t \n \v \f \r), written +# out: bash's [:space:] also matches U+00A0 and other Unicode spaces in a UTF-8 locale, which the classifier keeps. +lead="${prompt%%[!$' \t\n\v\f\r']*}" rest="${prompt#"$lead"}" if rw_is_harness_event "$rest" then diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index bad6adb7f..99a3e04e2 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -316,7 +316,9 @@ rw_is_harness_event() return 1 } # ---- END MIRRORED BLOCK rw_is_harness_event --------------------------------------------------------------- -lead="${prompt%%[![:space:]]*}" +# The lead strip is the six ASCII bytes src/taskroute.h's looksLikeSystemEvent strips (space \t \n \v \f \r), written +# out: bash's [:space:] also matches U+00A0 and other Unicode spaces in a UTF-8 locale, which the classifier keeps. +lead="${prompt%%[!$' \t\n\v\f\r']*}" rest="${prompt#"$lead"}" if rw_is_harness_event "$rest" then diff --git a/src/taskroute.h b/src/taskroute.h index be356c2f8..85f6435db 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -180,7 +180,8 @@ inline constexpr std::array kHarnessEventTags{ { inline bool looksLikeSystemEvent( std::string_view task ) noexcept { std::size_t i = 0; - // The six bytes the hooks' bash `[:space:]` strip removes: space and \t \n \v \f \r (kept equal, issue #381). + // The six bytes the prompt hooks' lead strip removes, `[!$' \t\n\v\f\r']`: space and \t \n \v \f \r. ASCII only, in + // every locale on both sides (issue #381; routehookcheck N10d/N10e, codexpromptroutecheck U+00A0 arms). while( i < task.size() && ( task[i] == ' ' || ( task[i] >= '\t' && task[i] <= '\r' ) ) ) { ++i; diff --git a/test/codexpromptroutecheck.sh b/test/codexpromptroutecheck.sh index 9e6f03cda..849a78e3d 100755 --- a/test/codexpromptroutecheck.sh +++ b/test/codexpromptroutecheck.sh @@ -167,6 +167,17 @@ chan_case "near-miss " cx7 " $CHT" route chan_case "near-miss " cx8 " $CHT" route chan_case "mid-sentence mention" cx9 "what does mean? $CHT" route chan_case "mid-sentence mention" cx10 "what does mean? $CHT" route +# A Unicode space before the wrapper is not stripped (D-N1): the hook's strip is the six ASCII bytes, as src/taskroute.h's, +# never bash's locale-dependent [:space:] (which matches U+00A0 in a UTF-8 locale on macOS bash 3.2). Run under a UTF-8 +# locale; without one installed the arm SKIPs by name. +UTF8LOC="" +for _l in en_US.UTF-8 C.UTF-8; do locale -a 2>/dev/null | grep -qix "$_l" && { UTF8LOC="$_l"; break; }; done +if [ -z "$UTF8LOC" ]; then + echo " SKIP U+00A0 arms: no UTF-8 locale installed (locale -a lists neither en_US.UTF-8 nor C.UTF-8)" +else + LC_ALL="$UTF8LOC" LANG="$UTF8LOC" chan_case "U+00A0 before under $UTF8LOC routes" cx14 "$( printf '\302\240%s' "$CHT" )" route + LC_ALL="$UTF8LOC" LANG="$UTF8LOC" chan_case "U+00A0 before under $UTF8LOC routes" cx15 "$( printf '\302\240 %s' "$CHT" )" route +fi OFF="$( printf '%s\n' "{\"prompt\":\"$PROMPT\",\"cwd\":\"$TMP/repo\",\"session_id\":\"route-off\"}" | \ PATH="$TMP/bin:$PATH" RIPWIRE_HOME="$TMP/off" RIPWIRE_ROUTE_METER=0 "$HOOK" )" diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 8b36f00a5..36e808887 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -710,11 +710,31 @@ chan_route "N9c channel mention: mid-sentence still routes" chn3 " chan_route "N9d agent-message near-miss: is not the wrapper" chn4 " $CHROUT" chan_route "N9e agent-message mention: mid-sentence still routes" chn5 "what does mean here? $CHROUT" chan_route "N9f channel near-miss: a bare word 'channel' in prose routes" chn6 "channel the energy: $CHROUT" -# Leading VERTICAL TAB / FORM FEED: bash's [:space:] strip removes them, and src/taskroute.h's strip must remove +# Leading VERTICAL TAB / FORM FEED: the hook's lead strip removes them, and src/taskroute.h's strip must remove # the same bytes (it used to stop at space/tab/LF/CR, so `--help-task` routed a prompt the hook skipped). chan_skip "N10 channel: a leading \\v before skips" chn7 "$( printf '\v%s' "$CHROUT" )" chan_skip "N10b channel: a leading \\f before skips" chn8 "$( printf '\f %s' "$CHROUT" )" chan_route "N10c channel: \\v then a near-miss still routes" chn9 "$( printf '\v %s' "$CHROUT" )" +# A Unicode space before the wrapper is NOT stripped (D-N1): the strip is the six ASCII bytes above, written out as +# `[!$' \t\n\v\f\r']`, because bash's `[:space:]` also matches U+00A0, U+2003, U+3000 … in a UTF-8 locale (macOS bash +# 3.2), and src/taskroute.h's strip stops at ASCII. So the hook skipped, under a UTF-8 locale only, a prompt --help-task +# routes. The arm runs the hook under a UTF-8 locale; without one installed it SKIPs by name. +UTF8LOC="" +for _l in en_US.UTF-8 C.UTF-8; do locale -a 2>/dev/null | grep -qix "$_l" && { UTF8LOC="$_l"; break; }; done +NBSP="$( printf '\302\240' )" +if [ -z "$UTF8LOC" ]; then + echo " SKIP N10d/N10e: no UTF-8 locale installed (locale -a lists neither en_US.UTF-8 nor C.UTF-8)" +else + for _case in "N10d:nbsp1:${NBSP}$CHROUT" "N10e:nbsp2:${NBSP} $CHROUT"; do + _lab="${_case%%:*}"; _r="${_case#*:}"; _ses="${_r%%:*}"; _pr="${_r#*:}" + _hd="$TMP/hc_$_ses"; mkdir -p "$_hd" + _co="$( route_run "$_hd" "$WITH_RIPWIRE" "$( promptjson "$_ses" "$NREPO" "$_pr" )" RIPWIRE_METER_ARM=treatment LC_ALL="$UTF8LOC" LANG="$UTF8LOC" )" + _ht="$( "$BIN" "$NREPO" --help-task="$_pr" 2>/dev/null | grep -c 'status="recommend"' )" + printf '%s' "$_co" | grep -Fq -- '--expand' && [ "$( rowget "$_hd/routing.jsonl" 1 status )" = "recommend" ] && [ "$_ht" -gt 0 ] \ + && ok "$_lab: a U+00A0 before the wrapper is not stripped under LC_ALL=$UTF8LOC: the hook routes, as --help-task does" \ + || no "$_lab: under LC_ALL=$UTF8LOC the hook and --help-task disagree: out=[$_co] status=[$( rowget "$_hd/routing.jsonl" 1 status )] help-task-recommend=[$_ht]" + done +fi # ═══════════════════════════════════════════════════════════════════════════════════════════════════ # O9 — rw_is_ripwire_call: ONE block, three files, and the shapes an agent actually types (PR #215 item 6) From ab4b4c7df69d1dabf23239311b032a3a803e649e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 06:32:44 -0400 Subject: [PATCH 191/229] defect-shape: the macro-name lookup orders string_view with rw::sortutil::svLess (portablebuildcheck #6c, CI 37757960003) The fix-round-1 lower_bound comparator compared two string_views with operator<, the shape that aborts the Linux G1 leg; every red job of the dispatched run was this one arm. Co-Authored-By: Claude Opus 5.5 --- src/defectshape.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/defectshape.h b/src/defectshape.h index 1391a9b5d..fe0aee54f 100644 --- a/src/defectshape.h +++ b/src/defectshape.h @@ -43,6 +43,7 @@ #include "infra/Diagnostics.h" #include "infra/namesplit.h" // the ONE ASCII identifier-character pair and the quote-pair strip +#include "infra/sortutil.h" // svLess: the string_view order a comparator lambda may use (portablebuildcheck #6c) namespace rw::defectshape { @@ -845,7 +846,7 @@ inline bool inSortedNames( const std::vector* names, std::string_vi { return false; } - const auto it = std::lower_bound( names->begin(), names->end(), w, []( const std::string& a, std::string_view b ) { return std::string_view( a ) < b; } ); + const auto it = std::lower_bound( names->begin(), names->end(), w, []( const std::string& a, std::string_view b ) { return rw::sortutil::svLess( a, b ); } ); return it != names->end() && *it == w; } From 118251e84b88b053deb1c29067f729cf70124c87 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 07:03:56 -0400 Subject: [PATCH 192/229] test: re-pin fixedbufsweep and selfcheckcheck for two merged lanes Both gates read source text, and both were already red on a signed lane head (each lane's targeted gates did not include them): - fixedbufsweep (S2/S6): count-floor-068 replaced the two implementors= header writes in src/serialize.h with string appends, so the 'hdr' row is 13 sites, not 15, and the pinned population is 242 calls / 352 mentions / 242 sites (for-spine-span-068's +3 included). - selfcheckcheck (C): honesty-small-068's pathSearchGaps postcondition calls PathSearchGaps::any(), a read-only member predicate (calls > 0); it is now on ALLOW with that reason. Co-Authored-By: Claude Opus 5.5 --- test/fixedbufsweep.sh | 4 ++-- test/selfcheckcheck.sh | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index 69cab4b74..fea74dd92 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -190,7 +190,7 @@ TABLE = { ( "src/serialize.h", "fitAttr" ): ( 1, "safe", "fitAttr[96]: two %zu plus the literal ' over_ceiling=1'." ), ( "src/serialize.h", "attr" ): ( 2, "safe", "attr[352] x2: the per-symbol metric attrs. Widest 26 lit + 4x10 digits + 11 role + qbuf(<=95) + ambs(<=35: amb= + lpin=) + kbuf(<=23) = 230 B." ), ( "src/serialize.h", "tail" ): ( 2, "safe", "tail[192] x2: the row tail. Widest 34 lit+digits + inAttr(<=23) + lens(qbuf, <=79) + pure(9) = 145 B." ), - ( "src/serialize.h", "hdr" ): ( 15, "safe", "hdr[64] x2 (packBodies/packOutline): ' implementors= header writes, two hdr sites, with string appends): ' plus pageDisclosure's own capped buffer, sized against that cap by construction." ), ( "src/serialize.h", "hb" ): ( 3, "latent", "hb[176]: ; shape= is a fixed vocabulary but acd/nccd are %.1f/%.2f on DOUBLES, formally unbounded. Realistic worst case 66 lit + 60 digits + 24 float + 10 shape = 160 B, ~16 B of margin. Truncation drops the '/>' and orphans the element." ), ( "src/serialize.h", "fit" ): ( 1, "safe", "fit[160]: the JSON max_tokens/fit_bytes twin; the %s is the literal ',\"over_ceiling\":true'." ), @@ -600,7 +600,7 @@ if not bad: # 238 -> 239 calls/sites, 104 -> 105 rows). noteCacheReject's new `char detail[192]` names both version # numbers on a refused cache blob; one formatTo of a literal and two std::uint32_t, rowed 'not-markup' # (a stderr notice). Re-derived on the train 20 merged tree; no other train lane moves the population. -EXPECTED = { "mentions": 354, "calls": 244, "sites": 244, "rows": 107, "widthforms": 0 } +EXPECTED = { "mentions": 352, "calls": 242, "sites": 242, "rows": 107, "widthforms": 0 } # train 26b: -2, count-floor-068 (serialize.h hdr row) # 2026-09-30 (#325 ruby_bases_unscoped=): +2 calls/+2 mentions/+2 sites, rows unchanged — graphGaugeAttrXml/Json (graphlegend.h) each format the absent-at-zero Ruby gauge into the SAME local buf[160]: one size_t, no string argument (21 + 20 digits worst case). # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the diff --git a/test/selfcheckcheck.sh b/test/selfcheckcheck.sh index d60e4feac..4167ee89e 100755 --- a/test/selfcheckcheck.sh +++ b/test/selfcheckcheck.sh @@ -151,6 +151,7 @@ ALLOW = { "std::all_of": "read-only walk of the given range (#150 keepStdQualifiedCandidates postcondition)", "isDefinitionNotDeclaration": "read-only span comparison (model.h); no state — the predicate std::all_of walks above", "isFunctionLikeKind": "read-only enum comparison (graph.h, a local lambda); no state — the F2 body-test scope guard", + "any": "read-only member predicate (pathgaps.h PathSearchGaps::any: calls > 0); no state — the gap-cone postcondition", } # (T) ASSUMED-THEN-TESTED allowlist: "site -> reason", one line each. A site lands here only when the equality # really is a true invariant (the re-test below is dead defensive code that should eventually be deleted, not From 99fb97839a7ee26e3baf3bb9c0cf62381b765a36 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 07:04:27 -0400 Subject: [PATCH 193/229] docs: name every reader the value-ref depth disclosure does not reach yet The Depth bullet (CHANGELOG), docs/ARCHITECTURE.md and two src/ingest_valuerefs.h comments said every answer that reads value references discloses a depth cut, with --quality-delta's dead kind as the one exception. Two more readers serve role=value sites without it: --verify='uses(SYM)' / 'unused(SYM)' (verdict confirmed/refuted with count=1 and no depth attribute on a tree with a use below the cap, so count= is a floor there; the verdict itself stands on its witness) and the LSP references/hover handlers, which have no attribute channel. They are now named beside the first exception. Comment and docs only. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +++++--- docs/ARCHITECTURE.md | 5 +++-- src/ingest_valuerefs.h | 9 +++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615463011..41200cb0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -988,9 +988,11 @@ call-shaped references, so on such a function `--callers`, `--callees`, `--impac cut is disclosed, never silent: every answer that reads value references for the cut file's language — the callers/callees roots, `--impact`, `--safe-delete`, `--dead-code`, `--uses`, `--path` (beside `to_value_refs=`), `--json` and the MCP twins — carries `value_refs_depth_capped=N` (files cut) and `value_refs_depth_at=FILE:LINE` - (the first cut), so a missing row, a `count=` or a listed dead function there reads as a floor. The one exception, - named: `--quality-delta`'s dead-code kind and its `value-ref-excluded=` count (CLI and MCP) read the rows and do not - yet carry it. An answer over a tree with no cut is byte-identical. + (the first cut), so a missing row, a `count=` or a listed dead function there reads as a floor. The named + exceptions read the rows and do not yet carry it: `--quality-delta`'s dead-code kind and its `value-ref-excluded=` + count (CLI and MCP), `--verify='uses(SYM)'` / `'unused(SYM)'` (their `count=` is a floor there; the verdict is not: + a witness confirms, an absence is `not-established`), and the LSP references and hover handlers, which have no + attribute to carry it. An answer over a tree with no cut is byte-identical. - **Second review.** A parameter of a prototype, a function-pointer typedef or parameter, a C++ member declaration, a Go func type or interface method, or a TS function type, `declare function` or interface signature no longer hides a same-named function in the enclosing scope (`signal( SIGINT, handler )` beside `typedef void (*fn_t)( int handler )` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b1d164513..3516a028e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -382,8 +382,9 @@ a reason a function is not dead (one made by another function or at file scope: stores itself stays dead, as a recursive self-call is no caller). The capture stops at 512 levels of nesting; a file it cut carries one depth-cut record, and every answer that reads value references for that file's language says so (`value_refs_depth_capped=`, `value_refs_depth_at=`): the callers/callees roots, -`--impact`, `--safe-delete`, `--dead-code`, `--uses`, `--path`, the JSON form and the MCP twins. The one exception is -`--quality-delta`'s dead-code kind and its `value-ref-excluded=` count (CLI and MCP), which do not yet say so. +`--impact`, `--safe-delete`, `--dead-code`, `--uses`, `--path`, the JSON form and the MCP twins. The exceptions, which +do not yet say so: `--quality-delta`'s dead-code kind and its `value-ref-excluded=` count (CLI and MCP), +`--verify='uses(SYM)'` / `'unused(SYM)'` (their `count=` is then a floor), and the LSP references and hover handlers. ### graph — the CSR diff --git a/src/ingest_valuerefs.h b/src/ingest_valuerefs.h index d30e8b982..8670585a1 100644 --- a/src/ingest_valuerefs.h +++ b/src/ingest_valuerefs.h @@ -38,7 +38,8 @@ // (model.h kValueRefDepthCutScope); argCount=the subtrees kVrMaxDepth stopped the walk above (saturating); // startByte/line=the first one. Not a row: valuerefindex.h counts it, and every answer that reads value // references discloses it (value_refs_depth_capped= / value_refs_depth_at=), except --quality-delta's dead kind -// and its value-ref-excluded= count (a named deferral). +// and its value-ref-excluded= count, --verify uses()/unused() (count= is a floor there) and the LSP +// references/hover handlers (named deferrals). // A local/file Through survives only when the same scope fed that container a function value (the filter at // scope exit / file end), so an ordinary `x.m()` adds nothing; a parameter Through always survives — its // values arrive from callers elsewhere. @@ -826,9 +827,9 @@ class ValueRefWalk } } - // The file's ONE depth-cut record (RECORD SHAPE in the header comment): the cut is disclosed through it on every - // answer that reads value references (valuerefs.h value_refs_depth_capped=, --dead-code), and it is cached with the - // file's other references, so a warm run discloses it too. + // The file's ONE depth-cut record (RECORD SHAPE in the header comment): the cut is disclosed through it on the + // answers that read value references (valuerefs.h value_refs_depth_capped=, --dead-code; the named exceptions are in + // the header comment), and it is cached with the file's other references, so a warm run discloses it too. void emitDepthCut() { EXPECTS( m_depthCut.subtrees > 0, "a depth-cut record is written only when the cap stopped the walk" ); From ef25ea0669b6e7b91d4feae53f338fc733a86d56 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 07:04:41 -0400 Subject: [PATCH 194/229] =?UTF-8?q?docs(CONTRIBUTING):=20six=20more=20pre-?= =?UTF-8?q?review=20checks=20=E2=80=94=20entity=20keys,=20capability=20ski?= =?UTF-8?q?ps,=20merge=20format=20args,=20commit-before-mutate,=20UTF-8=20?= =?UTF-8?q?caps,=20derived=20caches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0038007fd..dccddb03b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -845,6 +845,24 @@ round. Each takes minutes to check; each miss costs a review round. claim; a dedup or merge of findings keeps the most severe row, not the first; a substring match on a name is whole-word or bounded (probe `catalog` and `dialog` for `log`); a per-tag or per-file cap cannot make the result depend on file order. +14. **A match key names the real entity.** A filter, dedup or match identifies owner + name in its binding + scope, never file + name; probe two owners of one name in one file, and a bare name beside a same-named + class member. +15. **A gate detects what it needs and skips by name.** A gate that needs a sanitizer, a tool or a locale + checks for it itself and SKIPs naming it when absent; a guard on machine state exits rather than printing; + a new row or attribute name does not match the name patterns existing gates grep for. +16. **After any merge, count format placeholders against arguments** at every format call either side + touched: two sides adding the same `{}` merge cleanly and silently drop an argument. Never edit a tree + while its suite runs. +17. **Commit before you mutate.** Commit the fix before a mutation test, and undo a mutation by restoring the + committed file, never by checking out over uncommitted work; a build's `built_from` matches the commit it + claims. +18. **A text cap cuts on a UTF-8 boundary** and adds the ellipsis only when bytes were dropped; probe a + multibyte character straddling the cap, a text of exactly the cap, and the same slot in every language + that writes it. +19. **A derived cache dies with its parent.** A cached object that points into a rebuilt structure is dropped + wherever that structure is reassigned, never kept on a content stamp; probe a rebuild that keeps the + stamp (a chmod, a new non-source file) under the sanitizer build. **The gate count is a build product.** It is stated in `README.md`, `docs/EVALS.md` and `present/deck5_ripwire_build.js` — eight sites — and every one of them is written by From 15f64e015c7aae99331fd570a4b315dd7148211d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 09:50:58 -0400 Subject: [PATCH 195/229] fix(for): a hop the hop-slot rule drops no longer makes the header define via="name" With receiver evidence and the hop-slot rule both in, --for's header asked whether any compact hop candidate had a name-only out-edge. A candidate whose EVERY callee edge is name-only takes no slot, so its via="name" rows never print, yet the header still carried the via clause (seen on test/docdemotefix: noedge=6, no via row, clause present). One predicate, hopSlotHasProvenEdge (serialize.h), now decides both packHops' slot test and which hop ids the header's via question reads. Arm: test/docdemotecheck.sh (f) present-only via clause (red on the merge commit, green here). Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 30 ++++++++++++++++++++++++------ src/verbs_for.h | 3 +++ test/docdemotecheck.sh | 13 +++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 448f3609f..caf3d436e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2711,6 +2711,27 @@ inline bool namesOnlyOutAny( const std::vector& outOff, const std return false; } +// THE HOP-SLOT RULE as one predicate (gate test/forsigspancheck.sh (H)): a candidate earns a packHops slot only with +// at least one callee edge that is NOT name-only (empty bits: a graph with no name-only edge, so every edge is proven). +// packHops' slot test and --for's header question "may a hop row carry via=name?" (verbs_for.h) both read it, so a hop +// the rule drops can no longer make the header define an attribute no row carries (test/docdemotecheck.sh (f)). +inline bool hopSlotHasProvenEdge( const std::vector& outOff, const std::vector& outNameOnly, + NodeId id ) noexcept +{ + const std::uint32_t outDeg = ( std::size_t( id ) + 1 < outOff.size() ) ? outOff[ id + 1 ] - outOff[ id ] : 0u; + if( outDeg == 0 ) + { + return false; + } + if( outNameOnly.empty() ) + { + return true; + } + ASSUME( outOff[ id + 1 ] <= outNameOnly.size(), "the hedge bits are one per flattened edge (graph.h buildGraph)" ); + return !std::all_of( outNameOnly.begin() + outOff[ id ], outNameOnly.begin() + outOff[ id + 1 ], + []( std::uint8_t bit ) { return bit != 0; } ); +} + inline void serialize( std::FILE* out, const IngestResult& ing, const std::vector& rank, const std::vector& outOff, const std::vector& outTargets, int topK, bool mostImportantLast = false, @@ -7110,15 +7131,12 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // stays, and that is the HONEST trade rather than the cheap one: `capped="1"` alone cannot say // WHY a candidate has no row, and folding "has no edges" into "the budget stopped" would make // a fact about the graph look like a fact about the budget. - const std::uint32_t outDeg = ( id + 1 < outOff.size() ) ? outOff[ id + 1 ] - outOff[ id ] : 0u; // THE HOP-SLOT RULE (gate test/forsigspancheck.sh (H)): a slot needs at least one PROVEN callee edge. A candidate // whose every out-edge was bound by name alone (an off-topic getter whose one callee row was `bag.lookup()` on an // untyped local, measured on a graded answer) spent the slot on a hedged edge; it now counts with noedge=, whose - // reading is "no RESOLVED callee found" — a name-only binding is a hedge, not a resolution. - const bool noProvenEdge = outDeg > 0 && !outNameOnly.empty() - && std::all_of( outNameOnly.begin() + outOff[ id ], outNameOnly.begin() + outOff[ id + 1 ], - []( std::uint8_t bit ) { return bit != 0; } ); - if( outDeg == 0 || noProvenEdge ) + // reading is "no RESOLVED callee found" — a name-only binding is a hedge, not a resolution. No out-edge at all + // counts the same way (hopSlotHasProvenEdge). + if( !hopSlotHasProvenEdge( outOff, outNameOnly, id ) ) { ++noEdgeCount; continue; diff --git a/src/verbs_for.h b/src/verbs_for.h index b0adbeeae..2d319e950 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2707,6 +2707,9 @@ std::optional runForLens( const MainDispatch& d ) if( autoBundleMode && plan.compact ) { viaIds = forCompactHopIds( lensSurfaceIds, lensRank ); + // only the hops packHops gives a slot can carry a row (the hop-slot rule drops a hop whose every callee + // edge is name-only, so its via="name" rows never print) + std::erase_if( viaIds, [ & ]( rw::NodeId id ) { return !rw::hopSlotHasProvenEdge( g.outOff, g.outNameOnly, id ); } ); } else if( autoBundleMode && plan.autoBodies ) { diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index e938a89ef..e3660c9e3 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -248,6 +248,19 @@ grep -q 'doc_tier="' "$TMP/conceptc.xml" \ diff -q "$TMP/concept.xml" "$ROOT/test/docdemotegolden_for.xml" >/dev/null \ && ok "(f) conceptual --for byte-identical to the pre-change golden" \ || no "(f) conceptual --for drifted from test/docdemotegolden_for.xml" +# (f) present-only via clause: the header's via="name" reading rides only when a row carries via="name". On this +# fixture the one hop whose only callee is name-only (request_page -> evict_one) takes no slot (the hop-slot rule, +# test/forsigspancheck.sh (H)), so no row is left to carry the attribute and the clause must not ride either. +if [ -s "$TMP/concept.xml" ] && grep -q ' row`) + if grep -q 'matched by name alone' "$TMP/concept.xml" && ! grep -Eq '<[a-z]+ [^<>]*via="name"' "$TMP/concept.xml"; then + no "(f) the via=\"name\" legend clause rides with no via=\"name\" row in the answer (a hop dropped by the hop-slot rule still counted)" + else + ok "(f) the via=\"name\" legend clause rides only beside a via=\"name\" row" + fi +else + no "(f) conceptual --for produced no root, so the present-only via arm proves nothing" +fi # ── (g) --recall untouched ────────────────────────────────────────────────────────────────────────────── diff -q "$TMP/recall.xml" "$ROOT/test/docdemotegolden_recall.golden" >/dev/null \ From a2b0caf85fb91c8256ce378d6ef1032300f2e767 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 09:54:18 -0400 Subject: [PATCH 196/229] =?UTF-8?q?train=2026b:=20receiver-evidence=20merg?= =?UTF-8?q?e=20conditions=20=E2=80=94=20CHANGELOG=20floor=20clause,=20vers?= =?UTF-8?q?ions,=20docdemote=20re-pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG receiver-evidence entry: the floor list names a Python conditional or later rebinding (an older assignment-type rule types the variable for the whole function); its versions line points at the release's kParserVer 156. The versions paragraph adds the receiver-evidence records (145-154 on their branch, 156 here) and kCacheVersion 25 -> 29. - CHANGELOG e= entry: a compact --for hop row needs a resolved callee (the hop-slot rule, now live). - test/docdemotegolden_for.xml re-pinned: request_page's only callee is name-only, so its hop row goes (noedge 5 -> 6); the note in docdemotecheck.sh says what moved. - test/qschemetripcheck.sh: PENDING note — the pin is re-derived once after the last lane merges. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 21 ++++++++++++++------- test/docdemotecheck.sh | 5 +++++ test/docdemotegolden_for.xml | 2 +- test/qschemetripcheck.sh | 4 ++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4fb5b2d..c01fcae29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,10 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. even when a class of that name exists. Wider than the language, and so name-only rather than resolved: a pattern variable hides its field in its whole enclosing block (an `else` branch too), a Java local hides a field anywhere in its method, and a Python rebinding anywhere in its function. Not read, the floor: a nested function or class - declaration that reuses a typed binding's name (JS/TS/Python), and Kotlin's implicit `it` beside a field named `it`. A JS/TS call on the class object reaches its `static` members only, + declaration that reuses a typed binding's name (JS/TS/Python), Kotlin's implicit `it` beside a field named `it`, and a + Python conditional or later rebinding, which still types the variable for the whole function: `tank = Barrel()` in a + branch proves `Barrel.spill` for a `tank.spill()` beside it, even one before the assignment (the older assignment-type + rule, which this change does not read). A JS/TS call on the class object reaches its `static` members only, and a call on an instance never one. A Ruby call's candidates that Ruby's own method lookup proves (the entries below) are its answer, never `via="name"`. - **A call nothing proves is NAME-ONLY.** Its candidates the language's own lookup proves are its answer (an @@ -66,7 +69,7 @@ in a Java class with an outside base drawn to an unrelated class's `flush`. change); the `--max-tokens=3000` maps of the eight repositories keep 337 rows (331 before the change). The gate is `test/receiverevidencecheck.sh` (every surface, CLI/MCP parity, census conservation, and near misses for every evidence rule). Ingest records the receiver chain and the Java/C#/Kotlin/Swift declarations - (`kParserVer` 154, `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. + (`kParserVer` 156 in this release, see the versions note; `kCacheVersion` 29), so a cache written by an earlier build is re-parsed. ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` @@ -956,6 +959,10 @@ after the fix. `docs_after_code`: it is the answer it was before the reorder, so its est_tokens promise stays what it was (the reading would be uncharged bytes). These are the same three ceilings that drop `e=`, decided by one predicate. `RIPWIRE_NO_DOCS_AFTER_CODE`, set to any value (even empty or `0`), turns the reorder off everywhere (CLI and MCP `for`). +- **A hop row needs a resolved callee.** In a compact `--for` answer's ``, a candidate whose every callee edge + was bound by name alone (the `via="name"` rows of the receiver-evidence entry above) gets no `` row and is counted + in `noedge=`, whose reading is "no RESOLVED callee found", never "none exists". The header then defines `via=` only + when a hop that keeps its row has such a callee. Gates: `test/forsigspancheck.sh` (H), `test/docdemotecheck.sh` (f). Gate: `forsigspancheck` (fixture `test/forcompletefix`, C/JS/Python/TS). @@ -1482,11 +1489,11 @@ files, RSpec matcher chains, class objects — then 142 and 145 for its rebase a change); 141 sat above every number a branch build of unreleased work had used, and the review fixes to the global-object shadow and the value-reference slot text take 143, above 141's full-use file tag 142. The Ruby method lookup lands at 148, renumbered from its branch's 145: other branch builds in flight have used up to 147, and 148's full-use file tag is -149. The Go named-type kinds and the train 25 re-review's extraction fixes each took 145 on their branches and land -together at 156: branch builds in flight have used up to 154, whose full-use file tag is 155, so no cache such a build -wrote is read as this release's), `kCacheVersion` 25 → 28 -(the function-literal fix's record changes, then the false-edge fix's member-call fields; the Ruby branch's appended -binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 18 +149. The Go named-type kinds and the train 25 re-review's extraction fixes each took 145 on their branches, and the +receiver-evidence records took 145 to 154 on theirs; all three land together at 156: branch builds have used up to 154, +whose full-use file tag is 155, so no cache such a build wrote is read as this release's), `kCacheVersion` 25 → 29 +(the function-literal fix's record changes, then the false-edge fix's member-call fields, then the receiver chain +`memberPath`/`memberCtor`; the Ruby branch's appended binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 18 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions held as values, then on a function that only stores itself). Every ingest cache written by an earlier build is refused and re-indexed once, and every cached quality snapshot is recomputed. The session legend dictionary is `dictv=cbadf4aa6acda0c2 entries=787`. diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index e3660c9e3..321bc2b1e 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,11 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-08 (train 26b: FE-B merged beside for-spine-span's hop-slot rule): docdemotegolden_for.xml 6268 -> 6167 B +# (est_tokens "2507" -> "2467"). FE-B binds request_page's only callee `evict_one( self )` by name alone, so the hop-slot +# rule gives request_page no slot: with its one row becomes . That and est_tokens= are the whole diff against the train-side golden (checked mechanically); the header +# carries no via clause, since no row carries via="name" (arm (f) present-only via clause). The noroute golden is unchanged. # RE-PIN 2026-10-04 (lane for-spine-span-068, by design): docdemotegolden_for.xml 5821 -> 6268 B and # docdemotegolden_noroute.xml 9423 -> 9695 B. Every row with a known extent gains e="N" right after l= (5 and 6 # rows), the full legend gains the e= clause, est_tokens moves with the bytes. The routed golden also shows the diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index b6b8d4b80..790a51d8e 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 2a70e34d0..4d473ecff 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# PENDING (train 26b, not yet re-derived): the pin below is FE-B's lane pin (34d7ef2e81…), and it is stale on the train, +# whose watched text is kParserVer 156 (above FE-B's 154 and its rich tag 155; honesty-small and train25-cr2-followup +# folded in too), kCacheVersion 29 (FE-B) and kQSnapCacheScheme 18 (train25-cr2-followup), with quality.h's mirrors. +# Re-derive ONCE with UPDATE_GOLDEN=1 after the train's last lane merges and replace this line with that entry. # 2026-10-08, lane FE-B fix round 2 (review B4): RE-DERIVED with UPDATE_GOLDEN=1 (hash 34d7ef2e81…a5bd559d). kParserVer # 152 -> 154 (binding spans and tombstones, type parameters; 153 is 152's rich tag); quality.h's mirror with it. # kCacheVersion and kQSnapCacheScheme unchanged. The train renumbers if needed. From bbe15dac05892bee54bce086c7558aca59f71bdd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 10:24:10 -0400 Subject: [PATCH 197/229] test(receiverevidence): read a hop the hop-slot rule drops through --callees, premises checked With the hop-slot rule live, the eight --for hop arms' hops (runGo, reply, stamped, gather, encode, respond, pipePayload, back) have only name-only callees, so they take no slot and the arms failed on "the arm needs that hop". The assertion still holds and is now read where the fact lives: the hop's row has r= within , noedge= >= 1, and --callees=FILE:HOP lists the name and proves no row; any other absence FAILs. A shown hop is checked exactly as before (the lane binary still takes that branch: ALL PASS). Mutation: inverting the proven-row test turns all eight arms red. Co-Authored-By: Claude Opus 5.5 --- test/receiverevidencecheck.sh | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index 559cbce51..f38b0b62a 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -382,14 +382,40 @@ marked(){ else no "($r) --$v=$s dropped the name-only true edge [$spec]: $( show )"; fi done } -# hopnotproven ROOT QUERY HOP NAME LINE — the --for answer shows the hop block (else FAIL: the arm needs -# it), and that hop's row NAME at l=LINE is absent or via="name" +# hopnotproven ROOT QUERY HOP NAME LINE — the --for answer shows the hop block , and that hop's row +# NAME at l=LINE is absent or via="name". A hop with no block is read through the HOP-SLOT RULE (test/forsigspancheck.sh +# (H)): a hop candidate whose every callee edge is name-only gets no row and counts in noedge=. Then the arm reads +# the same fact off --callees instead, and every premise is checked, so an absent hop is never an empty pass: HOP's +# row is in the answer with r= within (it was a hop candidate), noedge= >= 1, and --callees=FILE:HOP lists +# NAME and proves no row at all (else the hop was dropped for another reason: FAIL). hopnotproven(){ local r="$1" q="$2" h="$3" n="$4" l="$5" f f="$( answer "$r" for "$q" )" ran_ok "($r) --for=\"$q\"" "$f" || return P hcalls "$f" "$h" || return - if [ "$PARSED" = "NOROOT" ]; then no "($r) --for=\"$q\" shows no hop with (the arm needs that hop)"; return; fi + if [ "$PARSED" = "NOROOT" ]; then + local drow hp hr htot hne + drow="$( grep -o "]*n=\"$h\"[^>]*>" "$f" | head -1 )" + hp="$( printf '%s' "$drow" | sed -n 's/.* p="\([^"]*\)".*/\1/p' )" + hr="$( printf '%s' "$drow" | sed -n 's/.* r="\([0-9][0-9]*\)".*/\1/p' )" + htot="$( grep -o ']*>' "$f" | head -1 | sed -n 's/.* total="\([0-9][0-9]*\)".*/\1/p' )" + hne="$( grep -o ']*>' "$f" | head -1 | sed -n 's/.* noedge="\([0-9][0-9]*\)".*/\1/p' )" + case "$hp|$hr|$htot|$hne" in + *'||'*|'|'*|*'|') no "($r) --for=\"$q\" shows no hop , and the hop-slot premises are unreadable (d p=/r=: '$hp'/'$hr', hops total=/noedge=: '$htot'/'$hne')"; return ;; + esac + if [ "$hr" -gt "$htot" ] || [ "$hne" -lt 1 ]; then + no "($r) --for=\"$q\" shows no hop , and it was no dropped hop candidate (r=$hr, hops total=$htot, noedge=$hne)"; return + fi + getrows "$r" callees "$hp:$h" || return + if [ -z "$( printf '%s\n' "$GOT" | awk -v n="$n" 'NF >= 5 && $2 == n' )" ]; then + no "($r) --for=\"$q\" dropped hop $h, and --callees=$hp:$h does not list $n either: $( show )" + elif [ -n "$( printf '%s\n' "$GOT" | awk 'NF >= 5 && $5 != "name"' )" ]; then + no "($r) --for=\"$q\" dropped hop $h, yet --callees=$hp:$h proves a row (the hop-slot rule does not explain the drop): $( show )" + else + ok "($r) --for=\"$q\": hop $h takes no slot (every callee name-only: --callees=$hp:$h, noedge=$hne), so it never proves " + fi + return + fi if [ -n "$( printf '%s\n' "$PARSED" | awk -v n="$n" -v l="$l" '$1 == n && $2 == l && $3 != "name"' )" ]; then no "($r) --for=\"$q\": hop $h proves (no via=\"name\"): $( printf '%s' "$PARSED" | tr '\n' ';' )" else ok "($r) --for=\"$q\": hop $h never proves (absent or hedged)"; fi From e9d568e1931497390e7b35780943b89e102481d5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 10:54:51 -0400 Subject: [PATCH 198/229] test: re-pin the four --for goldens and the compact-legend for pin on the merged tree for-spine-span's e= and lean-answers' zero elision compose on test/anchorfix, test/routefix, test/docdemotefix (routed and --no-route) and the compactlegend pre_l1_full for.xml pin. Each new output, with ONLY the zero elision undone (zeros back, its reading removed, est_tokens=/at= masked), equals the train-side golden exactly; the notes in each gate say what moved. Co-Authored-By: Claude Opus 5.5 --- test/anchorcheck.sh | 4 ++++ test/anchorfix/golden_for.xml | 2 +- test/compactlegendcheck.sh | 4 ++++ test/compactlegendfix/pre_l1_full/for.xml | 2 +- test/docdemotecheck.sh | 3 +++ test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- test/routecheck.sh | 3 +++ test/routefix/golden_for.xml | 2 +- 9 files changed, 19 insertions(+), 5 deletions(-) diff --git a/test/anchorcheck.sh b/test/anchorcheck.sh index b7210ba91..834f5ca0d 100755 --- a/test/anchorcheck.sh +++ b/test/anchorcheck.sh @@ -108,6 +108,10 @@ QUERY="frobnicate widget cache" # ladder trimmed used to appear in neither section), and the clause defining the tail says so. Verified before # re-pinning: with every comment and est_tokens= normalized out, old and new documents are byte-identical — # this fixture's head covers every file, so its tail is unchanged and every ranking byte is unmoved. +# RE-PIN 2026-10-08 (train 26b: lean-answers merged onto for-spine-span): 3807 -> 3783 B, est_tokens="1449" -> "1439". +# The two lanes' changes compose: e= rows and clause (for-spine-span) plus the zero elision and its clause (lean). Verified +# mechanically: undoing ONLY the zero elision (zeros back after e=/p=, clause removed, est_tokens=/at= masked) gives the +# train-side golden exactly. # RE-PIN 2026-10-07 (lane for-spine-span-068, by design): 3557 -> 3807 B (+250 B), est_tokens="1349" -> "1449". CAUSE: --for # rows with a known extent carry e="N" (the definition's last line) right after l= (3 rows here), and the full # legend gains the clause defining e=. Verified before re-pinning (tmp re-pin script, base binary 0c115310 == old golden): diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index 12c347c71..09364161d 100644 --- a/test/anchorfix/golden_for.xml +++ b/test/anchorfix/golden_for.xml @@ -1,4 +1,4 @@ -frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit() 901 (header and root). Checked: the new output differs from the pin by exactly # those two clauses and that figure; the six other pins are unchanged. +# RE-ANCHORED 2026-10-08 (train 26b: lean-answers merged onto for-spine-span), for.xml only: 4201 -> 4125 B, est_tokens= +# 1683 -> 1650. The two lanes' changes compose (e= rows and clause, then the zero elision and its clause); undoing ONLY +# the zero elision (est_tokens=/at= masked) gives the previous pin exactly. at= is stored masked, as before. The six other +# pins are unchanged. # RE-ANCHORED BY HAND 2026-10-04 (lane for-spine-span-068, by design), for.xml only: the four rows gain e="N" (the # definition's last line) right after l=, the full legend gains the present-only e= clause ("; e= on a d row: the 1-based # line where that definition ends, …", 221 B), and est_tokens= moves 1580 -> 1683 with the bytes (3937 -> 4194 B). diff --git a/test/compactlegendfix/pre_l1_full/for.xml b/test/compactlegendfix/pre_l1_full/for.xml index 4ae99ace8..ba0c0ada4 100644 --- a/test/compactlegendfix/pre_l1_full/for.xml +++ b/test/compactlegendfix/pre_l1_full/for.xml @@ -1 +1 @@ -double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files +double distance( Point a, Point b )double distance( Point a, Point b )double diagonal( Point a, Point b )double perimeter( const Point* pts, int n )# Geometry Fixture## Symbols# Geometry Fixture A tiny polyglot corpus for ripwire regression tests — kept small and stable so the golden snapshot only moves when ripwire's output genuinely changes. ## Symbols `distance` and `perimeter` live in the C++ files \ No newline at end of file diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 04f24183e..68c36ede5 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,6 +99,9 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-10-08 (train 26b: lean-answers merged): docdemotegolden_for.xml 6167 -> 5902 B (est_tokens "2467" -> "2361") +# and docdemotegolden_noroute.xml 9695 -> 9364 B ("3441" -> "3309"). Only the zero elision and its legend reading moved: +# undoing it (zeros back, reading removed, est_tokens=/at= masked) gives the train-side goldens exactly, on both fixtures. # RE-PIN 2026-10-08 (train 26b: FE-B merged beside for-spine-span's hop-slot rule): docdemotegolden_for.xml 6268 -> 6167 B # (est_tokens "2507" -> "2467"). FE-B binds request_page's only callee `evict_one( self )` by name alone, so the hop-slot # rule gives request_page no slot: with its one row becomes def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/docdemotegolden_noroute.xml b/test/docdemotegolden_noroute.xml index ac7fd28a7..e2216ec48 100644 --- a/test/docdemotegolden_noroute.xml +++ b/test/docdemotegolden_noroute.xml @@ -1,4 +1,4 @@ -# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ): 4002 B, est_tokens="1533" -> "1524". +# The two lanes' changes compose: e= rows and clause plus the zero elision and its clause. Verified mechanically: +# undoing ONLY the zero elision (est_tokens=/at= masked) gives the train-side golden exactly. # RE-PIN 2026-10-07 (lane for-spine-span-068, by design): 3776 -> 4026 B (+250 B), est_tokens="1433" -> "1533". CAUSE: --for # rows with a known extent carry e="N" (the definition's last line) right after l= (3 rows here), and the full # legend gains the clause defining e=. Verified before re-pinning (tmp re-pin script, base binary 0c115310 == old golden): diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index 63a0539e8..ae45429ee 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Thu, 8 Oct 2026 10:55:13 -0400 Subject: [PATCH 199/229] =?UTF-8?q?train=2026b:=20lean-answers=20review=20?= =?UTF-8?q?nits=20=E2=80=94=20cli.h=20field=20comment,=20MCP=20skill=20spe?= =?UTF-8?q?lling,=20L16-MCP=20named=20SKIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/cli.h: the --whereis-listing field comment states the served rule (more definitions, else strictly shorter), not the old 'shorter of defs and all'. - skills/ripwire-mcp/SKILL.md: the root attribute is listing="defs" (listing:"…" is the argument). - test/crossrefcheck.sh: L16-MCP SKIPs by name without python3, as L14 and L15 do. - test/compactlegendcheck.sh: the re-anchor note states the stored pin's size (4117 B, at= masked). Co-Authored-By: Claude Opus 5.5 --- skills/ripwire-mcp/SKILL.md | 2 +- src/cli.h | 2 +- test/compactlegendcheck.sh | 2 +- test/crossrefcheck.sh | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index 949c48c02..3e251e505 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -32,7 +32,7 @@ pair `whereis` / `stray_content`, which answer "where does this content live?" a question `git cherry` cannot, since it compares commit ancestry and every other verb indexes one worktree. `stray_content` marks a branch `superseded` when the live line re-implemented its work, which is exactly the case `git cherry` calls unmerged forever; `whereis` lists the definitions and counts the references -(`listing:"defs"` on the root) when that page lists MORE definitions than the whole list would under the same row +(`listing="defs"` on the root) when that page lists MORE definitions than the whole list would under the same row cap, or the same ones in strictly fewer bytes — `listing:"all"` asks for every row) + the 3 span-addressed edit verbs (`replace_symbol_body`, `insert_before_symbol`, `insert_after_symbol`). Only `find_symbol` and `find_referencing_symbols` attach a stable `handle=` instead of the body (fetch it only when you need it, diff --git a/src/cli.h b/src/cli.h index d25523eeb..ab80fe8f3 100644 --- a/src/cli.h +++ b/src/cli.h @@ -385,7 +385,7 @@ struct Config std::string_view laneBrief; // --brief=FILE: one non-blank line per lane, each ranked on its own bool whereisFlag = false; // --whereis was given at all (a bare/empty value still routes to the // handler and refuses loudly rather than falling through to the map) - std::string_view whereisListing; // --whereis-listing=defs|refs|all (default: the shorter of defs and all; crossref.h WhereisListing) + std::string_view whereisListing; // --whereis-listing=defs|refs|all (default: defs when it lists more definitions than all, else the strictly shorter; crossref.h whereisServedListing) std::string_view whereis; // --whereis=SYM: every ref whose TREE contains SYM, // HEAD first, with on-head= saying whether the live line has it at all. // Scans each ref's FULL tree; each distinct blob is read once (content- diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index d2dff94ca..4da6fe945 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -288,7 +288,7 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # outside-import/use,super-past-the-tree;...)" (was "(builtin/stdlib-name-...,external-import,...)"), +106 B — and the # est_tokens= that prices them, 858 -> 901 (header and root). Checked: the new output differs from the pin by exactly # those two clauses and that figure; the six other pins are unchanged. -# RE-ANCHORED 2026-10-08 (train 26b: lean-answers merged onto for-spine-span), for.xml only: 4201 -> 4125 B, est_tokens= +# RE-ANCHORED 2026-10-08 (train 26b: lean-answers merged onto for-spine-span), for.xml only: 4201 -> 4117 B, est_tokens= # 1683 -> 1650. The two lanes' changes compose (e= rows and clause, then the zero elision and its clause); undoing ONLY # the zero elision (est_tokens=/at= masked) gives the previous pin exactly. at= is stored masked, as before. The six other # pins are unchanged. diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index 3610da443..466d1e31b 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -713,6 +713,8 @@ if command -v python3 >/dev/null 2>&1; then [ "$( mcpq "$D16" zqMany '' )" = "$M16_A" ] \ && ok "LEAN (L16-MCP): MCP whereis serves the same all page as the CLI when both pages show the same definitions" \ || no "LEAN (L16-MCP): the MCP default differs from the CLI's on the capped, same-definitions answer" +else + printf ' SKIP LEAN (L16-MCP) MCP twin (no python3)\n' fi if command -v xmllint >/dev/null 2>&1; then From f6a50aee1306619202c9bc484fc6697ac0896f67 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 10:55:31 -0400 Subject: [PATCH 200/229] =?UTF-8?q?docs(CONTRIBUTING):=20two=20more=20pre-?= =?UTF-8?q?review=20checks=20=E2=80=94=20gates=20fail=20closed,=20claims?= =?UTF-8?q?=20match=20every=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the gate-robustness and claims-and-surfaces checks as items 14 and 15 of 'Before you ask for review'; the six checks after them renumber to 16-21, so the list keeps one number per check. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dccddb03b..f0471c3f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -845,22 +845,34 @@ round. Each takes minutes to check; each miss costs a review round. claim; a dedup or merge of findings keeps the most severe row, not the first; a substring match on a name is whole-word or bounded (probe `catalog` and `dialog` for `log`); a per-tag or per-file cap cannot make the result depend on file order. -14. **A match key names the real entity.** A filter, dedup or match identifies owner + name in its binding +14. **A gate fails closed.** A shell gate checks that a value is present and numeric before it compares it, so an + erroring comparison never falls through to PASS; it SKIPs a case only for a named, expected missing premise and + FAILs on any other; an absence ("no row X", "nothing skipped") is read only off a command that succeeded and + produced its root or rows; when one harness call is added to sibling gates, diff the call sites so every arm + runs. A wrapper records the runner's own exit status right after it, and a reported `rc=0` is checked against + the log's failure count. +15. **Claims match every surface.** A comment, legend or doc that says a case is handled names the arm that proves + it, or the claim goes. After a behaviour change, search every surface that describes it (CLI help, MCP tool + descriptions, legends, docs) for sentences the change made false, and re-check size ceilings such as the MCP + tool list. A count over a set (overloads, roots, shards) counts distinct entities; probe two members that meet + the same one. An external read on an answer path tells "could not read" from "read, empty" and discloses the + failure, never "none". +16. **A match key names the real entity.** A filter, dedup or match identifies owner + name in its binding scope, never file + name; probe two owners of one name in one file, and a bare name beside a same-named class member. -15. **A gate detects what it needs and skips by name.** A gate that needs a sanitizer, a tool or a locale +17. **A gate detects what it needs and skips by name.** A gate that needs a sanitizer, a tool or a locale checks for it itself and SKIPs naming it when absent; a guard on machine state exits rather than printing; a new row or attribute name does not match the name patterns existing gates grep for. -16. **After any merge, count format placeholders against arguments** at every format call either side +18. **After any merge, count format placeholders against arguments** at every format call either side touched: two sides adding the same `{}` merge cleanly and silently drop an argument. Never edit a tree while its suite runs. -17. **Commit before you mutate.** Commit the fix before a mutation test, and undo a mutation by restoring the +19. **Commit before you mutate.** Commit the fix before a mutation test, and undo a mutation by restoring the committed file, never by checking out over uncommitted work; a build's `built_from` matches the commit it claims. -18. **A text cap cuts on a UTF-8 boundary** and adds the ellipsis only when bytes were dropped; probe a +20. **A text cap cuts on a UTF-8 boundary** and adds the ellipsis only when bytes were dropped; probe a multibyte character straddling the cap, a text of exactly the cap, and the same slot in every language that writes it. -19. **A derived cache dies with its parent.** A cached object that points into a rebuilt structure is dropped +21. **A derived cache dies with its parent.** A cached object that points into a rebuilt structure is dropped wherever that structure is reassigned, never kept on a content stamp; probe a rebuild that keeps the stamp (a chmod, a new non-source file) under the sanitizer build. From 69ab8b5dd6eff0b8c01beac0c30d279b1691e1fe Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:09:59 -0400 Subject: [PATCH 201/229] fix(for): a row the code-above-docs reorder moves keeps the lens row spelling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With lean-answers and for-spine-span both in, reorderDocsAfterCode re-headed every moved row with a SigRowFacts that left out the zero elision, so a moved row spelled cx="0" ccx="0" in="0" again while its neighbours did not (and the legend says an absent one is 0). The re-head now passes the call's SigRowSpelling. Arm: test/docdemotecheck.sh (f) one row spelling (red at the merge, 3 rows). mcpincrementalcheck's default-posture twin read rows off a bare ; with the reorder the tag can carry docs_after_code=, so the twin found no rows and said DIFF. It now matches . Co-Authored-By: Claude Opus 5.5 --- src/serialize.h | 4 +++- test/docdemotecheck.sh | 10 ++++++++++ test/mcpincrementalcheck.sh | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index faf2949ef..5a5f8227c 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -5469,7 +5469,9 @@ inline void packSignatures( std::FILE* out, const IngestResult& ing, const std:: cut.docsAfterCode = reorderDocsAfterCode( entries, [ & ]( const SigEntry& e ) { return ing.symbols[ e.id ].lang == Lang::Markdown; }, [ & ]( SigEntry& e, std::uint32_t newRank ) - { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext }, esc, rootArg ); } ); + // the moved row keeps the row spelling it was charged and shown with (lean-answers' zero elision included) + { e.head = sigRowHead( ing, e.id, SigRowFacts{ metrics, fanIn, e.lensRun.c_str(), e.pureSig ? " pure=\"1\"" : "", newRank, topRowNext, + spelling.elideZeroMetrics }, esc, rootArg ); } ); } if( cutOut ) { diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index 68c36ede5..d03f2aa70 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -262,6 +262,16 @@ grep -q 'doc_tier="' "$TMP/conceptc.xml" \ diff -q "$TMP/concept.xml" "$ROOT/test/docdemotegolden_for.xml" >/dev/null \ && ok "(f) conceptual --for byte-identical to the pre-change golden" \ || no "(f) conceptual --for drifted from test/docdemotegolden_for.xml" +# (f) one row spelling: every r= row of a --for answer omits a zero cx=/ccx=/in= (the lens legend: absent = 0), the rows +# the code-above-docs reorder moved and re-headed (serialize.h reorderDocsAfterCode) included. This routed question +# moves the two notes/paging_design.md rows below the code. +if [ -s "$TMP/concept.xml" ] && grep -q ']*>' "$TMP/concept.xml" | grep -cE ' (cx|ccx|in)="0"' )" + if [ "$zrows" = "0" ]; then ok "(f) no --for row spells a zero cx=/ccx=/in= (moved rows included)" + else no "(f) $zrows --for row(s) still spell a zero cx=/ccx=/in= (a re-headed row lost the lens spelling)"; fi +else + no "(f) conceptual --for produced no root, so the row-spelling arm proves nothing" +fi # (f) present-only via clause: the header's via="name" reading rides only when a row carries via="name". On this # fixture the one hop whose only callee is name-only (request_page -> evict_one) takes no slot (the hop-slot rule, # test/forsigspancheck.sh (H)), so no row is left to carry the attribute and the clause must not ride either. diff --git a/test/mcpincrementalcheck.sh b/test/mcpincrementalcheck.sh index 69b32fd2a..b18be999c 100755 --- a/test/mcpincrementalcheck.sh +++ b/test/mcpincrementalcheck.sh @@ -173,7 +173,7 @@ import sys, json, re w, c = [ json.loads( l ) for l in sys.stdin.read().splitlines()[:2] ] A = re.compile( r"\s([\w:.-]+)=\"([^\"]*)\"" ) def rows( t ): - m = re.search( r".*?]*>.*?", t, re.S ); return m.group( 0 ) if m else None + m = re.search( r"]*>.*?]*>.*?", t, re.S ); return m.group( 0 ) if m else None # may carry docs_after_code= ab = re.search( r"]*/>", w ); rt = re.search( r"]*>", c ) if not ab or "legend=\"ref\"" not in ab.group( 0 ): print( "NOREF" ); sys.exit() wa = { k: v for k, v in A.findall( ab.group( 0 ) ) if k not in ( "legend", "dict", "dictv" ) } From f0e5a4ca6497e83bb583c7fd6a076b6a6d5b74d7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:14:00 -0400 Subject: [PATCH 202/229] test(docdemote): re-pin the routed golden with the moved rows' zero elision The two notes/paging_design.md rows the docs reorder moves now omit their zero cx=/ccx=/in= like every other row: 5902 -> 5851 B (est_tokens 2361 -> 2340). Undoing only the zero elision still gives the train-side golden exactly. Co-Authored-By: Claude Opus 5.5 --- test/docdemotecheck.sh | 6 ++++-- test/docdemotegolden_for.xml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/docdemotecheck.sh b/test/docdemotecheck.sh index d03f2aa70..a4fbdabc7 100755 --- a/test/docdemotecheck.sh +++ b/test/docdemotecheck.sh @@ -99,9 +99,11 @@ PY "$BIN" docdemotefix --for="$BUGQ" --no-route --format=candidates --no-cache >"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null -# RE-PIN 2026-10-08 (train 26b: lean-answers merged): docdemotegolden_for.xml 6167 -> 5902 B (est_tokens "2467" -> "2361") +# RE-PIN 2026-10-08 (train 26b: lean-answers merged): docdemotegolden_for.xml 6167 -> 5851 B (est_tokens "2467" -> "2340") # and docdemotegolden_noroute.xml 9695 -> 9364 B ("3441" -> "3309"). Only the zero elision and its legend reading moved: -# undoing it (zeros back, reading removed, est_tokens=/at= masked) gives the train-side goldens exactly, on both fixtures. +# undoing it (zeros back, reading removed, est_tokens=/at= masked) gives the train-side goldens exactly, on both fixtures, +# and no r= row spells a zero (arm (f) one row spelling). The routed golden was first re-pinned at 5902 B with the two +# rows the docs reorder moved still spelling cx="0" ccx="0" in="0"; the reorder now keeps the spelling (serialize.h). # RE-PIN 2026-10-08 (train 26b: FE-B merged beside for-spine-span's hop-slot rule): docdemotegolden_for.xml 6268 -> 6167 B # (est_tokens "2507" -> "2467"). FE-B binds request_page's only callee `evict_one( self )` by name alone, so the hop-slot # rule gives request_page no slot: with its one row becomes def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):def flush_cache( self ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file From 5a8db32b0424b022e54bf252f202c533425b77a2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:27:24 -0400 Subject: [PATCH 203/229] test(pathgap): the MCP arm runs both legend postures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With lean-answers in, a stdio session's answers after the first take the legend="ref" posture and carry their root facts on the closing , so arm (M) read no reachable=/searched= off the root. As in mcpincrementalcheck and legendrefcheck, the unchanged assertions now run on --mcp-legend=inline, and a twin on the default session posture reads the same facts off plus and must match the CLI too; each posture's premise (ref answers or none) is checked. Co-Authored-By: Claude Opus 5.5 --- test/pathgapcheck.sh | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index 1971a9bd8..4ee3b3bae 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -264,15 +264,21 @@ fi cleanArm "(N5) a value the search also calls" "$TMP/valin" "handleRequest,other" echo "(M) MCP path_between carries the same clause" -python3 - "$BIN" "$TMP" <<'PY' +# Two MCP legend postures (lean-answers): the unchanged assertions run on --mcp-legend=inline, the pre-posture spelling +# where every answer carries its root facts on ; a TWIN runs the default session posture, whose answers after the +# first are legend="ref" and carry their root facts on the closing instead. Both must match the CLI. +for MPOST in inline session; do +python3 - "$BIN" "$TMP" "$MPOST" <<'PY' import json, re, subprocess, sys -binp, tmp = sys.argv[1], sys.argv[2] +binp, tmp, posture = sys.argv[1], sys.argv[2], sys.argv[3] cases = [ ( "decl", "Machine_run", "Process_update", True ), ( "val", "listen", "respond", True ), ( "thr", "start", "target", True ), ( "clean", "a", "d", False ) ] reqs = [ { "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": { "name": "g", "version": "1" } } } ] for i, ( d, a, b, _ ) in enumerate( cases ): reqs.append( { "jsonrpc": "2.0", "id": i + 1, "method": "tools/call", "params": { "name": "path_between", "arguments": { "path": tmp + "/" + d, "from": a, "to": b } } } ) -p = subprocess.run( [ binp, "--mcp" ], input = "".join( json.dumps( r ) + "\n" for r in reqs ), capture_output = True, text = True, timeout = 120 ) +argv = [ binp, "--mcp" ] + ( [ "--mcp-legend=inline" ] if posture == "inline" else [] ) +p = subprocess.run( argv, input = "".join( json.dumps( r ) + "\n" for r in reqs ), capture_output = True, text = True, timeout = 120 ) +refs = 0 resp = {} for line in p.stdout.splitlines(): try: @@ -284,12 +290,16 @@ for line in p.stdout.splitlines(): def head( t ): m = re.search( r']*>', t ) return m.group( 0 ) if m else "" +def facts( t ): # the root's facts: , plus a legend="ref" answer's closing (where they move) + ab = re.search( r']*legend="ref"[^>]*/>', t ) + return head( t ) + ( ab.group( 0 ) if ab else "" ) def pick( t, name ): - m = re.search( r'\s' + name + r'="([^"]*)"', head( t ) ) + m = re.search( r'\s' + name + r'="([^"]*)"', facts( t ) ) return m.group( 1 ) if m else None bad = 0 for i, ( d, a, b, gap ) in enumerate( cases ): mcp = resp.get( i + 1, "" ) + refs += 1 if re.search( r']*legend="ref"', mcp ) else 0 cli = subprocess.run( [ binp, tmp + "/" + d, "--no-cache", "--path=%s,%s" % ( a, b ) ], capture_output = True, text = True ).stdout if not head( mcp ): print( " MCP gave no root for", d, repr( mcp[:300] ), p.stderr[:300] ); bad = 1; continue @@ -302,14 +312,20 @@ for i, ( d, a, b, gap ) in enumerate( cases ): print( " %s: MCP hint still claims no path" % d ); bad = 1 if not gap and "no directed call path" not in mcp: print( " %s: MCP lost the plain no-path hint" % d ); bad = 1 +# the posture premise: inline answers never take the ref posture; the session twin's answers after the first all do +if posture == "inline" and refs != 0: + print( " inline posture: %d answer(s) came back legend=\"ref\"" % refs ); bad = 1 +if posture == "session" and refs != len( cases ) - 1: + print( " session posture: %d of the %d later answers are legend=\"ref\" (the twin tests nothing new)" % ( refs, len( cases ) - 1 ) ); bad = 1 sys.exit( bad ) PY mrc=$? if [ "$mrc" -eq 0 ]; then - ok "(M) path_between: same searched/gaps/gap_syms/rows/next as the CLI on P1-P3, plain hint on N1" + ok "(M) path_between ($MPOST legend posture): same searched/gaps/gap_syms/rows/next as the CLI on P1-P3, plain hint on N1" else - no "(M) MCP path_between disagrees with the CLI or the helper crashed (rc=$mrc, details above)" + no "(M) MCP path_between ($MPOST legend posture) disagrees with the CLI or the helper crashed (rc=$mrc, details above)" fi +done if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAIL"; fi exit "$fail" From 9a84414b46e15a4968b190da23e0640f8eb11f00 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:27:40 -0400 Subject: [PATCH 204/229] docs(path): a call bound by name alone is named among the calls a path search does not count After receiver evidence, a dynamic-receiver call the resolver used to pin silently is via="name" and lists only its same-file and same-directory candidates; --path follows those edges, so a namesake it does not list (a bridge in another directory) is never searched and the answer still reads "no directed call path" (probed: run(obj) -> obj.process() with B.process in another directory calling the target). The path-gap legend, its compact reading and the CHANGELOG entry now name such calls among those not counted. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +++- src/compactlegend.h | 2 +- src/pathgaps.h | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 996978526..51641eb01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -822,7 +822,9 @@ value (`.then( handleResponse )`), or a call through a parameter. Any of those c says the search was incomplete: `searched=` (the symbols it reached), `gaps="declined:D,unresolved:U,value:V,through:T"`, `gap_syms=` with up to three `` rows (nearest `from=` first, `gap_syms_capped="1"` past that) and a `next=` that expands their bodies. A `` row is where the search could not see, never a hop. Ambiguous calls are not -counted (every candidate has an edge the search follows), nor are calls to names defined nowhere in the tree. When the +counted (every candidate has an edge the search follows), nor are calls bound by name alone (`via="name"`: the search +follows the candidates such a call lists, so a namesake in another directory that it does not list is never searched, +and an unreached `to=` behind one still reads "no directed call path"), nor calls to names defined nowhere in the tree. When the search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. A floor: the `next=` names a gap row by file and name, so where one file holds two definitions of that name it serves both. Gate: `test/pathgapcheck.sh`. diff --git a/src/compactlegend.h b/src/compactlegend.h index d2f9835b4..281e0cc4e 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -722,7 +722,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // PATH-GAP (src/pathgaps.h kPathGapsLegend), present only on a reachable=0 answer whose search met a gap. Each says what it does NOT // mean in the same breath (checklist 3): not proof of no path, a row is never a hop, and which calls are not counted. { "searched", "searched=N: reachable=0, but the search from from= (N symbols over resolved call edges) is INCOMPLETE: not proof of no path", false, "path", MapHeaderRead::No, {}, "path" }, - { "gaps", "gaps=kind:N,...: calls there with no edge (declined: nothing chose a candidate; unresolved), a function handed off as a value, a call through a slot; ambiguous and undefined-name calls not counted", true, {}, MapHeaderRead::No, {}, "path" }, + { "gaps", "gaps=kind:N,...: calls there with no edge (declined: nothing chose a candidate; unresolved), a function handed off as a value, a call through a slot; ambiguous, via=name (only listed candidates followed) and undefined-name calls not counted", true, {}, MapHeaderRead::No, {}, "path" }, { "gap_syms", "gap_syms=N: searched symbols carrying gaps; rows, nearest from= first, are where the search could not see, never a hop; gap_syms_capped=1 more than shown", false, "path", MapHeaderRead::No, {}, "path" }, // quality-delta: src/verbs_quality.h (root emit) + src/quality.h identityDisclosure { "stale", "stale=N: ack ledger rows whose target no longer applies (sa rows); never gating", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/pathgaps.h b/src/pathgaps.h index 9bee3ac7a..f1be43f3b 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -183,8 +183,9 @@ inline constexpr std::string_view kPathGapsLegend = "keeps no edge for (gaps= counts them: declined = several candidates and nothing chose one, unresolved = an in-repo name every definition of which was " "filtered out, value = a function stored or passed as a value and not otherwise reached, through = a call through a parameter or slot), so this is NOT " "proof that no path exists. rows are the symbols that carry them, nearest to from= first, gap_syms_capped=1 = more than shown; a row " - "is where the search could not see, never a hop. Not counted: ambiguous calls (every candidate has an edge the search followed) and calls to names " - "defined nowhere in the tree. next= reads the rows' bodies. "; + "is where the search could not see, never a hop. Not counted: ambiguous calls (every candidate has an edge the search followed), calls bound by " + "name alone (via=name: the search follows the candidates such a call lists, never a namesake it does not list) and calls to names defined nowhere " + "in the tree. next= reads the rows' bodies. "; inline std::string_view pathGapsLegend( bool on ) noexcept { return on ? kPathGapsLegend : std::string_view(); From 1a4f5d2447dcc1b664195939971e01342bf06c38 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:48:41 -0400 Subject: [PATCH 205/229] test(compactlegend): re-anchor the ten-verb loop bill 7,700 -> 7,800 B (measured 7,711) The train crosses the ceiling only in combination: 7,622 at the phase-A head, 7,688 with receiver evidence, 7,711 with lean-answers (+23 B, the --for probe's zero reading, already pinned in that lane's ripwire.for/v1 row). Same rule as every earlier anchor: the next multiple of 100 B. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 4da6fe945..19d8c7acf 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -1020,7 +1020,11 @@ echo # RE-ANCHORED 2026-09-26 (lane impact-depth-065, depth-labelled --impact): 7,500 → 7,700 B, measured 7,622 (7,495 on the # base binary b343b988). The loop's --impact=distance probe now carries by_depth= and d=, and its compact legend reads both # (+127 B, the (U) table's ripwire.impact/v1 row); the other nine verbs unmoved. Same rule: the next multiple of 100 B. -echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 7,700 B (34,431 B in full on the fixture) ===" +# RE-ANCHORED 2026-10-08 (train 26b): 7,700 → 7,800 B, measured 7,711. Attributed on this fixture by merge step: 7,622 at +# the train's phase-A head, 7,688 with receiver evidence merged (its present-only via= readings; under the old ceiling), +# 7,711 with lean-answers merged: +23 B, all on the --for probe, the " (absent cx/ccx/in = 0)" reading the lane pinned +# in the ripwire.for/v1 row. The other verbs are unmoved by that step. Same rule: the next multiple of 100 B. +echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 7,800 B (34,716 B in full on the fixture) ===" loopBytes=0; fullBytes=0 for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--uses=distance" "--edit-check=total_area" \ "--quality-delta" "--test-gate=geometry.cpp" "--affected=geometry.cpp" "--safe-delete=total_area" "--slice=total_area"; do @@ -1029,8 +1033,8 @@ for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--u b="$( leg bytes "$TMP/l.c" )"; f="$( leg bytes "$TMP/l.f" )" loopBytes=$(( loopBytes + b )); fullBytes=$(( fullBytes + f )) done -[ "$loopBytes" -le 7700 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ - || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 7,700 B; full: $fullBytes B)" +[ "$loopBytes" -le 7800 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ + || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 7,800 B; full: $fullBytes B)" echo echo "=== (M) MCP: legend:\"compact\" on edit_check answers in ≤ 900 B on a clean tree; every XML verb takes the argument, within its per-verb legend pin ===" From 41854f8808c333a6da1c3826fa983dfad3de7ca2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 11:59:22 -0400 Subject: [PATCH 206/229] refactor(path): one helper answers whether a path runs through a name-only hop --path and MCP path_between each spelled the same loop over the path's hops to decide the via="name" legend sentence; with the path-gap and depth disclosures merged beside it, MCP pathText measured complexity 19 (receiver evidence acked 18). graph.h pathHasNameOnlyHop is now the one spelling both transports call. Behaviour-identical. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 14 ++++++++++++++ src/mcpverbs.h | 6 +----- src/verbs_navigate.h | 6 +----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/graph.h b/src/graph.h index 31306e372..c5288841d 100644 --- a/src/graph.h +++ b/src/graph.h @@ -199,6 +199,20 @@ inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept return edgeNameOnlyInCsr( g.outOff, g.outTargets, g.outNameOnly, from, to ); } +// FE-B: does any hop of a directed call path (path[i-1] → path[i]) run through a name-only edge? --path and MCP +// path_between ask it once to decide their via="name" legend sentence (one spelling for both transports). +inline bool pathHasNameOnlyHop( const Graph& g, std::span path ) noexcept +{ + for( std::size_t i = 1; i < path.size(); ++i ) + { + if( edgeNameOnly( g, path[ i - 1 ], path[ i ] ) ) + { + return true; + } + } + return false; +} + // FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) inline bool edgeExists( const Graph& g, NodeId from, NodeId to ) noexcept { diff --git a/src/mcpverbs.h b/src/mcpverbs.h index ee7411d4d..9e2ab26e4 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3286,11 +3286,7 @@ inline std::optional pathText( const std::string& root, const std:: const ToValueRefs ptToValueRefs = toValueRefs( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); // PATH-GAP: the CLI --path's gap clause, by the same analysis and emitter (src/pathgaps.h). const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; - bool ptHasVia = false; // FE-B: exactly when a hop of this path is via="name", as on the CLI - for( std::size_t i = 1; i < pth.size() && !ptHasVia; ++i ) - { - ptHasVia = edgeNameOnly( g, pth[ i - 1 ], pth[ i ] ); - } + const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) ).c_str(), ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index c113b3af5..7f6189b52 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -2312,11 +2312,7 @@ std::optional runPath( const MainDispatch& d ) } const rw::ToValueRefs pthToValueRefs = path.empty() ? rw::toValueRefs( ing, true, dstDefs, &*pthValueIdx ) : rw::ToValueRefs{}; const rw::PathSearchGaps pthGaps = path.empty() ? rw::pathSearchGaps( ing, g, srcDefs, *pthValueIdx ) : rw::PathSearchGaps{}; - bool pthHasVia = false; // FE-B: exactly when a hop of THIS path is via="name" - for( std::size_t i = 1; i < path.size() && !pthHasVia; ++i ) - { - pthHasVia = edgeNameOnly( g, path[ i - 1 ], path[ i ] ); - } + const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) ).c_str(), ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), From 2164c6662685c6858df6d9dfc35be87033a8e0d6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 12:02:54 -0400 Subject: [PATCH 207/229] refactor(path): pathHasNameOnlyHop reads as adjacent_find over the hops Same answer; the hand loop matched three unrelated small loops as a clone. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/graph.h b/src/graph.h index c5288841d..276acddb9 100644 --- a/src/graph.h +++ b/src/graph.h @@ -203,14 +203,7 @@ inline bool edgeNameOnly( const Graph& g, NodeId from, NodeId to ) noexcept // path_between ask it once to decide their via="name" legend sentence (one spelling for both transports). inline bool pathHasNameOnlyHop( const Graph& g, std::span path ) noexcept { - for( std::size_t i = 1; i < path.size(); ++i ) - { - if( edgeNameOnly( g, path[ i - 1 ], path[ i ] ) ) - { - return true; - } - } - return false; + return std::adjacent_find( path.begin(), path.end(), [ & ]( NodeId from, NodeId to ) { return edgeNameOnly( g, from, to ); } ) != path.end(); } // FE-B: does the call edge from → to exist at all (the out-CSR is ascending within a source) From 53f31d37fefd9b7530627f635e7cf61c485ce56e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 12:30:19 -0400 Subject: [PATCH 208/229] test(mcpreloadasan): the reload session runs the inline legend posture its CLI oracle spells With lean-answers in, a stdio session's answers after the first are legend="ref" and carry their root facts on a closing , so every cycle's MCP-vs-one-shot-CLI equality failed on spelling alone (55 answers, cycle 0). The session now runs --mcp-legend=inline; the default posture's equality with the CLI stays held by the posture twins in mcpincrementalcheck, pathgapcheck and legendrefcheck. 20 cycles kept. Co-Authored-By: Claude Opus 5.5 --- test/mcpreloadasancheck.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index 9d56eae68..2e195375d 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -188,7 +188,10 @@ class Server: env.update(extra_env) self.err = open(errpath, "w+") self.errpath = errpath - self.p = subprocess.Popen([binp, "--mcp"] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.err, + # --mcp-legend=inline: every answer carries its root facts on its own root, the spelling the one-shot CLI oracle + # writes. The default session posture moves them to a closing after the first answer; its + # equality with the CLI is held by mcpincrementalcheck's and pathgapcheck's posture twins and by legendrefcheck. + self.p = subprocess.Popen([binp, "--mcp", "--mcp-legend=inline"] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.err, text=True, env=env) self.id = 0 self.timing_seen = 0 From d82e4cf1557e0990aa658ddbea7bf9df37f92934 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 12:48:11 -0400 Subject: [PATCH 209/229] test(mcpreloadasan): a memory refusal on any call after the release is the named premise The release arm accepted the 64M guard's refusal by name only on the first call on tree A. With the receiver-evidence index the footprint can reach the floor later in that sequence (seen on cycle 16: find_referencing_symbols refused by name after the release was proved), which read as an answer mismatch. Any call refused by name now counts as the premise, and every call the guard did answer must still equal the one-shot CLI (stricter than before, when the rest went unchecked). Co-Authored-By: Claude Opus 5.5 --- test/mcpreloadasancheck.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/mcpreloadasancheck.sh b/test/mcpreloadasancheck.sh index 2e195375d..8730922bb 100644 --- a/test/mcpreloadasancheck.sh +++ b/test/mcpreloadasancheck.sh @@ -631,8 +631,16 @@ def release_attempt(k, refR, refA, kk): stats["release"] += 1 check(v2 == refR, "cycle %d RL: the unchanged tree R, released and rebuilt, answers equal the one-shot CLI" % k, "cli=%s mcp=%s" % (short(refR), short(v2))) gotA, fA = mcp_views(s, A, symbols_at(kk), PATHS, GREPS, RANKBYS) - if gotA[("fs", "cb")][0] == "MCPERR" and "memory limit" in str(gotA[("fs", "cb")][1]): - row("NOTE", "cycle %d RL: the release was proved; the call on A after it was refused by name (the footprint stayed over the floor)" % k) + # the memory guard may refuse ANY call on A by name, not only the first (a bigger index reaches the floor later in the + # sequence): such a refusal is the named premise "the footprint stayed over the floor", and every call it did + # answer must still equal the one-shot CLI + refusedA = [key for key, v in gotA.items() if v and v[0] == "MCPERR" and "memory limit" in str(v[1])] + if refusedA: + row("NOTE", "cycle %d RL: the release was proved; %d call(s) on A after it were refused by name (the footprint stayed over the floor)" + % (k, len(refusedA))) + badA = [b_ for b_ in compare("RL A", refA, gotA) if b_[0] not in refusedA] + check(not badA, "cycle %d RL: after the release, every call on tree A the guard answered equals the one-shot CLI" % k, + "; ".join("%s: cli=%s mcp=%s" % (a_, short(b_), short(c_)) for a_, b_, c_ in badA[:2])) return "ok" bad = compare("RL A", refA, gotA) check(not bad, "cycle %d RL: after the release, tree A answers equal the one-shot CLI" % k, From 59165d75c9aa435427d3d8564fdd4b078102f44f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 13:17:27 -0400 Subject: [PATCH 210/229] test: compactlegendcheck and forrankordercheck take BIN as $1; four budget gates print BIN first Both gates read only RIPWIRE_BIN, so a run passing a mutant or base binary as $1 silently tested build/ripwire (false green). BIN is now $1, else RIPWIRE_BIN, else build/ripwire, and the first output line of compactlegendcheck, forrankordercheck, estchargecheck and fornotesbudgetcheck names the binary used. A missing binary exits 2 before any arm runs. Co-Authored-By: Claude Opus 5.5 --- test/compactlegendcheck.sh | 8 ++++++-- test/estchargecheck.sh | 2 +- test/fornotesbudgetcheck.sh | 2 +- test/forrankordercheck.sh | 7 ++++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 24bf9b128..7e575a652 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -44,13 +44,17 @@ # the map header, and every absence-marked row field of the map legend, read from source, has a compact reading — so the # next one cannot land undefined. # -# Usage: RIPWIRE_BIN=build/ripwire bash test/compactlegendcheck.sh +# Usage: bash test/compactlegendcheck.sh [BIN] | RIPWIRE_BIN=build/ripwire bash test/compactlegendcheck.sh +# BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used (a mutant run against the +# wrong binary gave false greens when this gate read RIPWIRE_BIN only — knob-honesty-068 delta review N7). set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" -BIN="${RIPWIRE_BIN:-$ROOT/build/ripwire}" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "compactlegendcheck: BIN=$BIN" +[ -x "$BIN" ] || { echo "compactlegendcheck: no ripwire binary at $BIN — build first"; exit 2; } FIX="$ROOT/test/fixture" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT # HERMETIC HOME (0.6.6 review M2): the (U)/(UG) probe runs bare --scan-skills, which walks the skill homes under $HOME diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 68739c737..05117459d 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -31,6 +31,7 @@ ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "estchargecheck: BIN=$BIN" fail=0 ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; } @@ -38,7 +39,6 @@ skip(){ printf ' SKIP %s\n' "$*"; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } cd "$ROOT" -echo "estchargecheck: BIN=$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT diff --git a/test/fornotesbudgetcheck.sh b/test/fornotesbudgetcheck.sh index 54d0fe262..d0697b819 100755 --- a/test/fornotesbudgetcheck.sh +++ b/test/fornotesbudgetcheck.sh @@ -176,6 +176,7 @@ ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "fornotesbudgetcheck: BIN=$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT CORPUS="$TMP/corpus" fail=0 @@ -187,7 +188,6 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } command -v python3 >/dev/null 2>&1 || { echo "fornotesbudgetcheck: python3 is required (JSON arms)"; exit 2; } command -v git >/dev/null 2>&1 || { echo "fornotesbudgetcheck: git is required (notes carry a sha/branch stamp)"; exit 2; } -echo "fornotesbudgetcheck: BIN=$BIN" # ── the sandbox corpus: 72 small symbols across 12 files, one long note on every one ─────────────── mkdir -p "$CORPUS/src" || { echo "fornotesbudgetcheck: cannot create corpus under $TMP"; exit 2; } diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index e5cc9ba30..ebdca3039 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -29,14 +29,16 @@ # (6) shown= on a capped equals the number of rows printed; two runs are byte-identical; # full and compact bundles are xmllint-clean. # -# Usage: bash test/forrankordercheck.sh | RIPWIRE_BIN=asan/ripwire bash test/forrankordercheck.sh +# Usage: bash test/forrankordercheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire bash test/forrankordercheck.sh +# BIN is $1, else RIPWIRE_BIN, else build/ripwire; the first output line names the binary used (knob-honesty-068 N7). # Exits non-zero on any failure. set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" . "$ROOT/test/lib/clean-env.sh" -BIN="${RIPWIRE_BIN:-$ROOT/build/ripwire}" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "forrankordercheck: BIN=$BIN" fail=0 ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; } @@ -45,7 +47,6 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } command -v git >/dev/null 2>&1 || { echo "git required"; exit 2; } cd "$ROOT" -echo "forrankordercheck: BIN=$BIN" TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT From 93d2fcf8e0048b212ff6b6da1d547d9b19ee1464 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 13:59:54 -0400 Subject: [PATCH 211/229] test(forrankorder): (10) over-either-way MCP budget re-pinned 3000 -> 2600 on the merged tree lean-answers' zero elision shrank every --for row, so at budget_tokens=3000 the pre-continuation train binary already fits (25 rows, est 2945) and the merged answer moves into the payable regime (unpaid + handle over, paid 23 rows fits), served paid as ruled. 2600 is over either way (21 rows est 2684 before any handle; served 21 + the unpaid handle, est 2793, over_ceiling="1"). Assertions unchanged. Co-Authored-By: Claude Opus 5.5 --- test/forrankordercheck.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index ebdca3039..f260451e1 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -553,7 +553,13 @@ fi # Where paying IS what makes it fit, it pays (unchanged). Fixture: (9)'s 40 gadget functions, called by a RELATIVE path from # $TMP so no checkout or temp-dir path rides in the header bytes the rows are budgeted against (root="fxcut" everywhere). # over-anyway @2000 / @3000: 255dc199 serves 14 / 25 of 40 (est 2270 / 3125, over); dd6e4c8e paid 2 rows (12 / 23) and was -# still over (est 2206 / 3071) — RED there. Rows >= 255dc199's (re-pin from a pre-continuation binary if row bytes or the +# still over (est 2206 / 3071) — RED there. +# train 26b re-pin (2026-10-08): lean-answers' zero elision made every row smaller, so @3000 is no longer over either way +# on the merged tree — the pre-continuation train binary (d82e4cf1) serves 25 rows at est 2945, FITTING; the unpaid +# answer + handle lands over and the paid one (23 rows, est 2918) fits, i.e. @3000 moved into the payable regime and is +# served paid, as ruled. The over-either-way budget is now @2600: d82e4cf1 serves 21 rows at est 2684 (over before any +# handle); the merged binary serves those 21 + the unpaid handle, est 2793, over_ceiling="1" (paying ~2 rows, ~90 tokens, +# cannot reach 2600). @2000 is unchanged (d82e4cf1 14 rows est 2178 over). Rows >= 255dc199's (re-pin from a pre-continuation binary if row bytes or the # header change; smaller rows only raise it), the handle rides, over_ceiling="1". (Budgets with a margin on both sides of # the ceiling: near one, a few bytes of legend decide whether paying fits, and the arm would pin that, not the rule.) # payable @3800: 255dc199 serves 35 rows at est 3824 — OVER; paying the handle from rows (33, est 3773) is what makes it fit: @@ -566,7 +572,7 @@ for line in sys.stdin: d=json.loads(line) if d.get("id")==1: print(d["result"]["content"][0]["text"])' 2>/dev/null | cut_arm; } c3row(){ printf '%s' "$1" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["shown"], d["capped"], d["next_budget_tokens"] is not None, d["over"], d["est_tokens"])' 2>/dev/null; } -for spec in 2000:14 3000:25; do +for spec in 2000:14 2600:21; do bt="${spec%%:*}"; want="${spec##*:}" set -- $( c3row "$( mcprel "$bt" )" ) if [ "$#" -eq 5 ] && [ "$1" -ge "$want" ] && [ "$2" = True ] && [ "$3" = True ] && [ "$4" = True ]; then From 7dd380a9d4f7887ddec2d2ea728687ffd3620ac8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 14:33:52 -0400 Subject: [PATCH 212/229] test(pathgap): a call bound by name alone hides a same-language namesake from --path (red first) (P7) run(obj) calls obj.process(), bound by name alone (via="name") to the same-directory A.process only; other/B.process (Python, a method) calls the target and is never searched, so --path=run,target must report gaps="name:1" with the gap row run, not "no directed call path". (N6) the near miss: the only other process definitions are a Ruby method and a Python variable, so the plain hint stays. The MCP path_between twin covers P7. Red on d82e4cf1 and on the pre-fix train binary. Co-Authored-By: Claude Opus 5.5 --- test/pathgapcheck.sh | 63 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index 4ee3b3bae..ade44231e 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -20,12 +20,17 @@ # (P4) unresolved: a call whose only same-named definition is in another language — gaps=unresolved. # (P5) runaway guard: 6 gap symbols — 3 rows, gap_syms="6", gap_syms_capped="1", nearest first. # (P6) next= is pasteable: running it serves the gap row's body. +# (P7) name: run(obj) calls obj.process(), bound by name alone (via="name") to the same-directory A.process only; the +# tree's other/B.process (same language, same kind, calls the target) is never searched — gaps=name, row run. # (N1) a fully resolved graph with truly no path keeps "no directed call path" and no gap attribute. # (N2) a gap OUTSIDE the search cone (a declined call reachable only from elsewhere) does not count. # (N3) ambiguous-only cone (a k-way split the search followed) with no path keeps "no directed call path". # (N4) a found path (reachable="1") through a cone with gaps carries no gap clause. # (N5) a function passed as a value that the search ALSO reaches by a call is not a gap. -# (M) MCP path_between carries the same searched/gaps/gap_syms/rows/next as the CLI, and the same no-gap hint shape. +# (N6) a via="name" call whose namesakes are a DIFFERENT language (Ruby) or a different KIND (a Python variable) is not a gap. +# (N3) above doubles as the name-gap near miss: run's obj.process() is via="name" too, but it has an edge to EVERY +# same-language definition of process, so nothing was left unsearched. +# (M) MCP path_between carries the same searched/gaps/gap_syms/rows/next as the CLI (P1-P3, P7), and the same no-gap hint shape. # # Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -43,7 +48,7 @@ command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } # ── fixtures ──────────────────────────────────────────────────────────────────────────────────────────────────────── mkdir -p "$TMP/decl/core" "$TMP/decl/linux" "$TMP/decl/darwin" "$TMP/val" "$TMP/thr" "$TMP/unr" "$TMP/clean" \ "$TMP/out/core" "$TMP/out/linux" "$TMP/out/darwin" "$TMP/amb" "$TMP/many/core" "$TMP/many/linux" "$TMP/many/darwin" \ - "$TMP/found" "$TMP/valin" + "$TMP/found" "$TMP/valin" "$TMP/lpy/pkg/x" "$TMP/lpy/other" "$TMP/lpyx/pkg/x" "$TMP/lpyx/other" cat > "$TMP/decl/core/machine.c" <<'EOF' void Machine_scan( void ); void Machine_run( void ) { Machine_scan(); } @@ -166,6 +171,41 @@ function handleRequest(ctx) { handleResponse(ctx); return runMiddleware(ctx).the module.exports = { handleRequest, other }; EOF +# P7 / N6: a call bound by name alone (FE-B via="name"). In lpy the namesake other/B.process is Python, a method, and +# calls the target; in lpyx the only other process definitions are a Ruby method and a Python variable. +cat > "$TMP/lpy/pkg/x/a.py" <<'EOF' +class A: + def process(self): + return 1 +EOF +cat > "$TMP/lpy/pkg/x/c.py" <<'EOF' +def run(obj): + return obj.process() +EOF +cat > "$TMP/lpy/other/b.py" <<'EOF' +def target(): + return 2 + +class B: + def process(self): + return target() +EOF +cp "$TMP/lpy/pkg/x/a.py" "$TMP/lpy/pkg/x/c.py" "$TMP/lpyx/pkg/x/" +cat > "$TMP/lpyx/other/b.rb" <<'EOF' +def target + 2 +end + +class B + def process + target + end +end +EOF +cat > "$TMP/lpyx/other/v.py" <<'EOF' +process = 3 +EOF + # One --path answer, run unpiped so rc is ripwire's; the head and the rest are read off the saved file. path(){ # $1 dir, $2 FROM,TO, $3 out file "$BIN" "$1" --no-cache --path="$2" > "$3" 2>&1 @@ -216,6 +256,15 @@ gapArm "(P2) value" "$TMP/val" "listen,respond" "value:1" "ha gapArm "(P3) through" "$TMP/thr" "start,target" "through:1" "apply" gapArm "(P4) unresolved" "$TMP/unr" "begin,finish" "unresolved:1" "begin" +# P7's premise: the call really is bound by name alone to A.process only (else the arm would test the old resolver) +f="$TMP/lpycallees.xml"; "$BIN" "$TMP/lpy" --no-cache --callees=run > "$f" 2>&1 +if [ "$( grep -o ']*n="process"[^>]*/>' "$f" | wc -l | tr -d ' ' )" = "1" ] && grep -q ']*n="process" p="pkg/x/a.py:2" via="name"' "$f"; then + ok "(P7) premise: run's obj.process() is via=\"name\" to pkg/x/a.py's A.process only" + gapArm "(P7) name" "$TMP/lpy" "run,target" "name:1" "run" +else + no "(P7) premise: run's callees are not the one via=name A.process row: $( grep -o ' "$f" 2>&1 +if grep -q ']*n="process" p="pkg/x/a.py:2" via="name"' "$f"; then + cleanArm "(N6) via=name, namesakes of another language/kind" "$TMP/lpyx" "run,target" +else + no "(N6) premise: run's obj.process() is not via=name to A.process here: $( grep -o ' Date: Thu, 8 Oct 2026 14:35:54 -0400 Subject: [PATCH 213/229] fix(path): a call bound by name alone with an unlisted namesake is a path gap, never "no directed call path" FE-B binds a call nothing proves by name alone (via="name") to the candidates in reach of the caller; a same-language, same-kind definition of that name elsewhere (another directory) gets no edge and --path / MCP path_between never searched it, yet an unreached to= read "no directed call path". pathSearchGaps now counts a fifth population, name: per cone symbol, each callee name it reached only by name for which the tree holds a definition (same language and kind) the caller has no edge to. Such an answer reports reachable="0" with the gap clause (gaps="...,name:N", rows, next=); a via=name call that already lists every namesake is not a gap. The gap hint says "calls or hand-offs it could not follow in full"; the legend, the compact gaps entry and the CHANGELOG path-gap entry move "via=name" from "not counted" to the counted list. Orchestrator ruling on honesty-small condition 3 (train 26b). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++-- src/compactlegend.h | 2 +- src/pathgaps.h | 103 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26ea08f1..f4c924f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -869,12 +869,14 @@ which puts the 12 back on `Core`'s def as single edges. resolved call edges did not reach `B`, even when that search had passed a call the graph keeps no edge for: a call the resolver declined (several candidates, none local, nothing chose one), an unresolved call, a function handed off as a value (`.then( handleResponse )`), or a call through a parameter. Any of those can be the missing hop. Such an answer now -says the search was incomplete: `searched=` (the symbols it reached), `gaps="declined:D,unresolved:U,value:V,through:T"`, +says the search was incomplete: `searched=` (the symbols it reached), `gaps="declined:D,unresolved:U,value:V,through:T,name:M"`, `gap_syms=` with up to three `` rows (nearest `from=` first, `gap_syms_capped="1"` past that) and a `next=` that expands their bodies. A `` row is where the search could not see, never a hop. Ambiguous calls are not -counted (every candidate has an edge the search follows), nor are calls bound by name alone (`via="name"`: the search -follows the candidates such a call lists, so a namesake in another directory that it does not list is never searched, -and an unreached `to=` behind one still reads "no directed call path"), nor calls to names defined nowhere in the tree. When the +counted (every candidate has an edge the search follows), nor calls to names defined nowhere in the tree. A call bound by +name alone (`via="name"`) counts as a `name` gap when the tree defines that name, same language and kind, somewhere the +call has no edge to: the search follows only the candidates such a call lists, so a namesake in another directory is +never searched and may be the missing hop (`run(obj)` calling `obj.process()`, bound to the same-directory +`A.process`, while `other/B.process` calls the target). When the search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. A floor: the `next=` names a gap row by file and name, so where one file holds two definitions of that name it serves both. Gate: `test/pathgapcheck.sh`. diff --git a/src/compactlegend.h b/src/compactlegend.h index 98ee3b4a4..0f8a27cdb 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -724,7 +724,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // PATH-GAP (src/pathgaps.h kPathGapsLegend), present only on a reachable=0 answer whose search met a gap. Each says what it does NOT // mean in the same breath (checklist 3): not proof of no path, a row is never a hop, and which calls are not counted. { "searched", "searched=N: reachable=0, but the search from from= (N symbols over resolved call edges) is INCOMPLETE: not proof of no path", false, "path", MapHeaderRead::No, {}, "path" }, - { "gaps", "gaps=kind:N,...: calls there with no edge (declined: nothing chose a candidate; unresolved), a function handed off as a value, a call through a slot; ambiguous, via=name (only listed candidates followed) and undefined-name calls not counted", true, {}, MapHeaderRead::No, {}, "path" }, + { "gaps", "gaps=kind:N,...: calls there with no edge (declined: nothing chose a candidate; unresolved), a function handed off as a value, a call through a slot, a via=name call whose name is also defined where it has no edge (name); ambiguous and undefined-name calls not counted", true, {}, MapHeaderRead::No, {}, "path" }, { "gap_syms", "gap_syms=N: searched symbols carrying gaps; rows, nearest from= first, are where the search could not see, never a hop; gap_syms_capped=1 more than shown", false, "path", MapHeaderRead::No, {}, "path" }, // quality-delta: src/verbs_quality.h (root emit) + src/quality.h identityDisclosure { "stale", "stale=N: ack ledger rows whose target no longer applies (sa rows); never gating", false, "quality-delta", MapHeaderRead::No, {}, "quality-delta" }, diff --git a/src/pathgaps.h b/src/pathgaps.h index f1be43f3b..4ebc4bf83 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -7,6 +7,10 @@ // UNRESOLVED (an in-repo name whose every definition was filtered out), a function handed off as a VALUE (stored or // passed, then invoked by whoever holds it) and a call made THROUGH a parameter or slot. Any of those can be the hop // that joins A to B, so "no path" there was a wholeness claim the search could not make. +// A fifth population (train 26b, honesty-small condition 3): a call bound by NAME ALONE (FE-B via="name") has edges only to +// the candidates in reach of the caller, so a same-language, same-kind definition of that name elsewhere (another +// directory) is never searched — the fx/lpy case: run(obj) calls obj.process(), the edge goes to the same-directory +// A.process, and other/B.process, which calls the target, was invisible behind a plain "no directed call path". // // THE CONTRACT. pathSearchGaps walks the same directed cone the BFS walked (every symbol reachable from the from= // definitions over resolved call edges) and counts those four populations inside it. When the count is non-zero the @@ -19,7 +23,8 @@ // (graph.h buildGraph's splitPick), and the BFS follows all of them, so ambiguity never hides a path (it can only make // a FOUND path a guess, which prov="split" / amb= already disclose). Calls to names with no in-repo definition at all // (Undefined) are not counted either: the graph keeps no per-caller tally of them, and nearly all are library calls. -// Both are floors the legend sentence states. +// Both are floors the legend sentence states. A via="name" call whose every same-language, same-kind definition of the +// name already has an edge from the caller is not a gap either: nothing it could bind to was left unsearched. // // ONE analysis and ONE emitter for both transports (verbs_navigate.h runPath, mcpverbs.h pathText): the two used to // carry their own copies of the hint and must not drift on this clause. @@ -47,9 +52,9 @@ namespace rw inline constexpr std::size_t kPathGapRows = 3; // rows printed; gap_syms= counts them all, gap_syms_capped="1" says so // The four gap populations, in the order gaps= lists them. A declarative table (CONTRIBUTING §3): the spelling lives once. -enum class PathGapKind : std::uint8_t { Declined, Unresolved, Value, Through }; -inline constexpr std::size_t kPathGapKindCount = 4; -inline constexpr std::array kPathGapKindNames = { "declined", "unresolved", "value", "through" }; +enum class PathGapKind : std::uint8_t { Declined, Unresolved, Value, Through, Name }; +inline constexpr std::size_t kPathGapKindCount = 5; +inline constexpr std::array kPathGapKindNames = { "declined", "unresolved", "value", "through", "name" }; using PathGapCounts = std::array; struct PathGapSym @@ -70,6 +75,78 @@ struct PathSearchGaps bool any() const noexcept { return calls > 0; } }; +// NAME gaps: per cone symbol, the callee names it reaches only by name (an edge whose Graph::outNameOnly bit is set) for +// which the tree holds a same-language, same-kind DEFINITION the caller has no edge to — a namesake the search never +// walked. Counted once per such name per caller. Only the names behind the cone's name-only edges are looked up. +inline void countUnlistedNamesakes( const IngestResult& ing, const Graph& g, std::span cone, std::vector& per ) +{ + EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits one per flattened edge" ); + struct Key + { + Lang lang; + SymKind kind; + std::string_view name; + auto operator<=>( const Key& ) const = default; + }; + const auto keyOf = [ & ]( NodeId t ) { const Symbol& s = ing.symbols[t]; return Key{ s.lang, s.kind, s.name }; }; + const auto hasHedge = [ & ]( NodeId u ) { return std::any_of( g.outNameOnly.begin() + g.outOff[u], g.outNameOnly.begin() + g.outOff[u + 1], []( std::uint8_t b ) { return b != 0; } ); }; + const auto isSym = [ & ]( std::uint32_t k ) { return g.outTargets[k] < ing.symbols.size(); }; + std::vector names; + for( const NodeId u : cone ) + { + for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) + { + if( g.outNameOnly[k] != 0 && isSym( k ) ) + { + names.push_back( ing.symbols[ g.outTargets[k] ].name ); + } + } + } + if( names.empty() ) + { + return; + } + std::ranges::sort( names ); + names.erase( std::ranges::unique( names ).begin(), names.end() ); + std::vector defs; // the tree's definitions of exactly those names + for( const Symbol& s : ing.symbols ) + { + if( isDefinitionNotDeclaration( s ) && std::ranges::binary_search( names, std::string_view( s.name ) ) ) + { + defs.push_back( Key{ s.lang, s.kind, s.name } ); + } + } + std::ranges::sort( defs ); + for( const NodeId u : cone ) + { + if( !hasHedge( u ) ) + { + continue; + } + std::vector listed, hedged; // every target u has an edge to; the targets it reached by name alone + for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) + { + if( !isSym( k ) ) + { + continue; + } + listed.push_back( keyOf( g.outTargets[k] ) ); + if( g.outNameOnly[k] != 0 ) + { + hedged.push_back( keyOf( g.outTargets[k] ) ); + } + } + std::ranges::sort( listed ); + std::ranges::sort( hedged ); + hedged.erase( std::ranges::unique( hedged ).begin(), hedged.end() ); + for( const Key& key : hedged ) + { + const bool unlisted = std::ranges::equal_range( defs, key ).size() > std::ranges::equal_range( listed, key ).size(); + per[u][ std::size_t( PathGapKind::Name ) ] += unlisted ? 1u : 0u; + } + } +} + // The directed cone of `srcs` (the same out-edges shortestPathAny walks, every one of them — no early stop at a target, // because this runs only when there was none) and the gap populations inside it. Deterministic: BFS in id-ascending // out-edge order, a reference pass in index order, and a total order on the rows. @@ -133,6 +210,10 @@ inline PathSearchGaps pathSearchGaps( const IngestResult& ing, const Graph& g, s } } } + if( !g.outNameOnly.empty() ) // FE-B's hedge bits (absent on a graph built without them: no name population) + { + countUnlistedNamesakes( ing, g, queue, per ); + } for( const NodeId u : queue ) { std::size_t here = 0; @@ -163,7 +244,7 @@ inline PathSearchGaps pathSearchGaps( const IngestResult& ing, const Graph& g, s return out; } -// gaps="declined:D,unresolved:U,value:V,through:T" — zero entries left out, table order. +// gaps="declined:D,unresolved:U,value:V,through:T,name:M" — zero entries left out, table order. inline std::string pathGapCountsValue( const PathGapCounts& c ) { std::string v; @@ -181,11 +262,11 @@ inline std::string pathGapCountsValue( const PathGapCounts& c ) inline constexpr std::string_view kPathGapsLegend = "searched=N gaps= gap_syms=: reachable=0 but the search is INCOMPLETE — the N symbols reachable from from= over resolved call edges hold calls the graph " "keeps no edge for (gaps= counts them: declined = several candidates and nothing chose one, unresolved = an in-repo name every definition of which was " - "filtered out, value = a function stored or passed as a value and not otherwise reached, through = a call through a parameter or slot), so this is NOT " - "proof that no path exists. rows are the symbols that carry them, nearest to from= first, gap_syms_capped=1 = more than shown; a row " - "is where the search could not see, never a hop. Not counted: ambiguous calls (every candidate has an edge the search followed), calls bound by " - "name alone (via=name: the search follows the candidates such a call lists, never a namesake it does not list) and calls to names defined nowhere " - "in the tree. next= reads the rows' bodies. "; + "filtered out, value = a function stored or passed as a value and not otherwise reached, through = a call through a parameter or slot, name = a call " + "bound by name alone (via=name) whose name the tree also defines, same language and kind, where the call has no edge: that namesake was never " + "searched), so this is NOT proof that no path exists. rows are the symbols that carry them, nearest to from= first, " + "gap_syms_capped=1 = more than shown; a row is where the search could not see, never a hop. Not counted: ambiguous calls (every candidate has an " + "edge the search followed) and calls to names defined nowhere in the tree. next= reads the rows' bodies. "; inline std::string_view pathGapsLegend( bool on ) noexcept { return on ? kPathGapsLegend : std::string_view(); @@ -234,7 +315,7 @@ inline PathGapsXml pathUnreachedXml( const IngestResult& ing, const PathSearchGa } out.rootAttrs = " searched=\"" + std::to_string( gaps.searched ) + "\" gaps=\"" + pathGapCountsValue( gaps.totals ) + "\" gap_syms=\"" + std::to_string( gaps.syms.size() ) + "\"" + ( gaps.syms.size() > shown ? " gap_syms_capped=\"1\"" : "" ) - + " hint=\"no path through resolved call edges, but the search is incomplete: it met calls with no edge (gaps=) — read the gap rows, or try " + + " hint=\"no path through resolved call edges, but the search is incomplete: it met calls or hand-offs it could not follow in full (gaps=) — read the gap rows, or try " + ex( say.connect ) + " (undirected: finds a shared caller)" + std::string( say.tail ) + "\"" + nextAttrXml( next ); return out; } From 18b123aed16dab06c1cb662e11f711a75ea73a18 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 15:37:52 -0400 Subject: [PATCH 214/229] docs/test: the train 26b final regeneration, once, from the merged binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From build/ripwire at f1e5c2e7 (all lanes merged, the path-gap name fix on top): - docs/captures/COMMANDS_showcase_2026-10-08.md re-captured (test/showcase_capture.py: re-derives the --at seed, now src/graph.h:7926, and records dictv=593769ab691623d8 entries=806); docs/COMMANDS.md rebuilt from it (184 flags, 168 samples) — the re-recorded dated showcase samples cr-qd-kinds' signoff asked for. - docs/LIMITS.md (240 caps, 7 parameters) and README's cap sentence; docs/TUNING.md (capsweep emit); the deck's recorded-invocation count 168. - test/qschemetrip.hash re-derived once (kParserVer 156/157, kCacheVersion 29, kQSnapCacheScheme 19); the RE-PIN LOG entry replaces the PENDING line. - test/printf_parity.manifest help and help_all (UPDATE_GOLDEN_EXPECT="help help_all"). - test/mcpmanifestcheck.sh ceiling 46,890 -> 46,950 (measured 46,930, 20 B headroom), CHANGELOG's manifest figure to match; CHANGELOG versions paragraph dictv. - test/estcalib.manifest: the for-budgeted row re-measured (pin.py), other rows kept. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +- README.md | 2 +- docs/COMMANDS.md | 1268 +++--- docs/LIMITS.md | 19 +- docs/TUNING.md | 16 +- docs/captures/COMMANDS_showcase_2026-10-08.md | 4010 +++++++++-------- present/deck5_ripwire_build.js | 2 +- test/estcalib.manifest | 3 +- test/mcpmanifestcheck.sh | 7 +- test/printf_parity.manifest | 4 +- test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 10 +- 12 files changed, 2712 insertions(+), 2635 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c924f46..aae4dbd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,7 @@ false rows on a review's 76 adversarial candidate shapes plus the gate's 19 macr unchecked run; sample 30/30, Wilson 95% lower bound 0.89. An absence beside a positive on the same capture — an `if`/`elif` condition of `&&`-joined greps, or a grep through a function the script defines — is guarded (a crash fails the positive); before that guard the same tree gave 49 rows, of which 3 were such false positives (a seeded -sample of 28/30, lower bound 0.79). The `tools/list` manifest grows 46,732 → 46,793 B: the quality_delta description counts 12 kinds and +sample of 28/30, lower bound 0.79). The `tools/list` manifest grows 46,869 → 46,930 B on this release's tree (46,732 → 46,793 B on the lane's base, +61 B both ways): the quality_delta description counts 12 kinds and states the format-arity exception. Not checked, so no row there is no verdict: Python %-formatting and f-strings; a format in a named constant, a macro @@ -1628,7 +1628,7 @@ whose full-use file tag is 155, so no cache such a build wrote is read as this r `memberPath`/`memberCtor`; the Ruby branch's appended binding kinds need no bump of their own) and `kQSnapCacheScheme` 15 → 19 (the `--quality-delta` error-masking and placeholder changes, then the dead kind agreeing with `--dead-code` on functions held as values, then on a function that only stores itself, then the defect-shape records of the twelfth kind). Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=cbadf4aa6acda0c2 entries=787`. +cached quality snapshot is recomputed. The session legend dictionary is `dictv=593769ab691623d8 entries=806`. ### Fixed — a call the language resolves outside the tree no longer binds to a same-named in-repo definition diff --git a/README.md b/README.md index 0722d6e94..0f9f2afd6 100644 --- a/README.md +++ b/README.md @@ -2841,7 +2841,7 @@ 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. The tool has 237 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 240 compile-time caps and 7 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. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index d70ce9b03..b4d03761b 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -44,7 +44,7 @@ Two limits apply to nearly everything here and are not repeated in every section **zoom the detail ladder** — [`--detail`](#--detailn) · [`--pack-signatures`](#--pack-signatures) · [`--outline`](#--outlineab) · [`--expand`](#--expandab) · [`--compress`](#--compress) · [`--pack-top-n`](#--pack-top-nn) · [`--no-redact`](#--no-redact) -**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--biggest-first`](#--biggest-first) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--lint-max-per-rule`](#--lint-max-per-rulen) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--whereis-listing`](#--whereis-listingwhich) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) **self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) @@ -68,21 +68,21 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - + - - + + - + - + + - -... [7 more line(s); run it to see the whole thing] +... [11 more line(s); run it to see the whole thing] ``` **Shaped by:** `--token-budget`, `--for`, `--recall`, `--graph-query`, `--pack-signatures`, `--expand`, `--from-trace`, `--run-trace` @@ -106,10 +106,10 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ $ ./build/ripwire . --max-tokens=1500 - + - + @@ -136,7 +136,7 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--recall`, `--handoff`, `--pr-context`, `--from-trace`, `--run-trace` @@ -181,19 +181,19 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` - + & p, float alpha = 0.85f ) ... [17 more line(s); run it to see the whole thing] ``` @@ -218,19 +218,17 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -... [19 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for` @@ -247,14 +245,14 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -279,19 +277,18 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -... [19 more line(s); run it to see the whole thing] +struct FileHealth +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for` @@ -313,19 +310,18 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - - - - - + + + + + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -inline ChurnRanking churnRankedGraph( const MainDispatch& d ) - +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -struct RankedGraph -... [19 more line(s); run it to see the whole thing] +... [20 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -346,19 +342,17 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -... [19 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--detail` @@ -379,18 +373,17 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - - - - + + + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) - + P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() -inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) - -calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f -... [20 more line(s); run it to see the whole thing] +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +... [21 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -411,17 +404,16 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - - - - -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] - + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - -H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… -... [21 more line(s); run it to see the whole thing] + +... [22 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -442,7 +434,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -471,8 +463,8 @@ _The repo's best-in-class instance to imitate before writing new code (picked by ``` $ ./build/ripwire . --exemplar="format byte sizes for humans" - + to imitate. window: shown= total= capped= (capped=1 cut). root=: p= relative to it. : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. candidates=N: instances of kind= under the ccx ceiling the pick was ranked from. low_confidence=1: weak t … [line truncated: 81 more bytes on this line] + f, A&&... a ) @@ -501,9 +493,9 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ ``` $ ./build/ripwire . --recall="quality delta gating exit codes" -ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 +ripwire recall — "quality delta gating exit codes" — 113 relevant of 216 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.970) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 8.038) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 214142 B; lines="2710-2722,2723-2730"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -566,25 +558,20 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1411912-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1416688-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -━━ commands.md (relevance 11.013) ━━ [sections: 1 of 159 selected (187 in doc), section-granular; whole doc 546809 B; lines="528-553"; dropped_by_budget=158] [truncated: 1854 of 4293 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 10.145) ━━ [sections: 1 of 159 selected (188 in doc), section-granular; whole doc 551924 B; lines="3754-3758"; dropped_by_budget=158] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] -... [17 more line(s); run it to see the whole thing] +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ``` ### `--tree` @@ -600,13 +587,13 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + - + @@ -671,7 +658,7 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - + @@ -682,10 +669,10 @@ $ ./build/ripwire . --order=stable --top-k=5 - - - -... [7 more line(s); run it to see the whole thing] + + + +... [11 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-stable` @@ -702,16 +689,17 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable ``` $ ./build/ripwire . --no-stable --top-k=3 - + - - + + - + - + + @@ -733,20 +721,20 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - + - - + + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -786,15 +774,15 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - - - - - - - - + + + + + + + + + ``` @@ -818,16 +806,16 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + - - - - - + + + + + ``` @@ -852,16 +840,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - + - - + + - + @@ -889,19 +877,19 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -924,12 +912,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - - + + + + + + ``` @@ -946,10 +934,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -972,18 +960,18 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - - - - - + + + + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1056,7 +1044,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1118,7 +1106,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: c7920353a +at: f1e5c2e76 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1136,19 +1124,19 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - - + + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1172,7 +1160,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1196,8 +1184,8 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the ``` $ ./build/ripwire . --grep=DISCLOSE - + - - + + - + - + + - ... [17 more line(s); run it to see the whole thing] ``` @@ -1499,19 +1487,17 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f - + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - + The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] - -What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -... [19 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--max-tokens`, `--for`, `--signatures-only`, `--auto-bodies`, `--compress`, `--owners`, `--plan`, `--abi` @@ -1534,14 +1520,14 @@ _Body-elided decl skeletons — recounted on this corpus. Measured as element by $ ./build/ripwire . --pack-signatures --top-k=10 - - + + - + - + @@ -1567,7 +1553,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1594,11 +1580,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + @@ -1699,14 +1685,14 @@ _--no-redact: emit bodies verbatim (credential redaction is on by default)._ ``` $ ./build/ripwire . --expand=readAckRecords --top-k=0 --no-redact - + + - - + + - + - + - + @@ -1770,15 +1756,15 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - + + + + + - - - - + + + @@ -1807,18 +1793,18 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - - - - - - - - - + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1835,17 +1821,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1866,13 +1852,13 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - + + - - - - + + + + @@ -1899,15 +1885,15 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - - + - + @@ -1934,18 +1920,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1965,18 +1951,18 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - - + - - - - - + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2002,13 +1988,13 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - - - - - - + + + + + + ... [23 more line(s); run it to see the whole thing] ``` @@ -2033,9 +2019,9 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - - - + + + @@ -2068,18 +2054,18 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - - - + + + + + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2104,20 +2090,20 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - - + + + - + - - + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2140,11 +2126,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - - + + @@ -2173,16 +2159,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - - + + + + - - - + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2205,16 +2191,16 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - + - - - - + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2231,19 +2217,19 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2260,19 +2246,19 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2298,7 +2284,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2328,7 +2314,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2354,7 +2340,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2380,20 +2366,20 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - - + + + - + - - + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2489,20 +2475,20 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - - - + + - + - - + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2604,19 +2590,19 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - - - + + + - + - + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2635,7 +2621,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2660,19 +2646,19 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - + + - + - + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2697,16 +2683,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2484 files · 23851 symbols · 34236 edges · 2324 modules (10910 call-graph isolated) +2688 files · 25558 symbols · 40392 edges · 2494 modules (11469 call-graph isolated) Root: `.` -Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3087 declaration, 1678 header, 2119 source, 4585 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 2324) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1374 symbols -- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1135 symbols -- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 534 symbols +## Modules (call-graph clusters; showing 12 of 2494) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1685 symbols +- **src::append@elixir_resolve.h:111:4875 [add,resolve,compute]** — 1021 symbols +- **src::emplace@infra/svector.h:408:22477 [resolve,scan,find]** — 605 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2723,13 +2709,13 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - - - + + + - + + @@ -2754,17 +2740,17 @@ $ ./build/ripwire . --mermaid %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
7066"] - n102["src/infra
1276"] + n101["src
7731"] + n102["src/infra
1280"] end subgraph sg1 ["test"] - n103["test
4853"] - n328["test/fixtures/recallpassage
314"] + n103["test
5057"] + n333["test/fixtures/recallpassage
314"] n280["test/expandmodefix
151"] n282["test/expandsibsfix
149"] - n458["test/rubyattrsfix
85"] - n395["test/massfix
77"] - n360["test/javamethodreffix
62"] + n535["test/rubyattrsfix
85"] + n411["test/massfix
77"] + n492["test/receiverevidencefix/js/lib
74"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2783,19 +2769,19 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + - - + + - + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2813,8 +2799,8 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - - + + ``` @@ -2878,8 +2864,8 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + @@ -2915,8 +2901,8 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + @@ -2953,7 +2939,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -3022,8 +3008,8 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. r surface=: the api-surface tier, new-symbol or contract-change. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -3053,13 +3039,13 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - - - - + + + + + ``` @@ -3107,12 +3093,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3128,12 +3114,12 @@ _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 bec ``` $ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3179,12 +3165,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3243,11 +3229,11 @@ _The same plan committed: per-file locks, re-verify-before-write, atomic rename, ``` $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 529 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true}}]} +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3293,19 +3279,19 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - - - - - - - - - - - - - + + + + + + + + + + + + + ... [3 more line(s); run it to see the whole thing] ``` @@ -3330,17 +3316,17 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + ... [22 more line(s); run it to see the whole thing] @@ -3384,11 +3370,10 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:7397 - - - - +$ ./build/ripwire . --at=src/graph.h:7926 + + + ``` @@ -3412,7 +3397,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3438,11 +3423,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3517,7 +3502,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3551,19 +3536,19 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - - - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -3587,20 +3572,20 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - - - - + + - - + + + - - - -... [17 more line(s); run it to see the whole thing] + + + + +... [2 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -3622,8 +3607,8 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - + + - - + + - + - + - + @@ -3764,8 +3749,8 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - + @@ -3776,7 +3761,7 @@ $ ./build/ripwire . --field-affinity - + ... [17 more line(s); run it to see the whole thing] ``` @@ -3799,17 +3784,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - + + - - - - - - - - + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3834,17 +3819,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - + + - - - - - - - - + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3868,18 +3853,18 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - + + + - - - - - - - - + + + + + + + + ... [18 more line(s); run it to see the whole thing] ``` @@ -3904,7 +3889,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3939,10 +3924,10 @@ $ ./build/ripwire . --from-trace=- AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:5567 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 - #3 0x102e01a44 in main src/main.cpp:3975 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:7926 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:7967 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1634 + #3 0x102e01a44 in main src/main.cpp:3999 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -3969,7 +3954,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4059,17 +4044,17 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - + + (run= when derivable). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : that callee matched by name alone (receiver unprove … [line truncated: 552 more bytes on this line] + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4096,13 +4081,13 @@ _Fan-out form: one shared core + 3 per-agent slices carved along call-graph comm ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3 - wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> - - - + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> + + + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -4129,18 +4114,17 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f - + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - + The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] - -... [20 more line(s); run it to see the whole thing] +... [21 more line(s); run it to see the whole thing] ``` **Shaped by:** `--partition` @@ -4204,14 +4188,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + @@ -4238,14 +4222,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4273,7 +4257,7 @@ $ ./build/ripwire . --skipped - @@ -4359,21 +4343,21 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + - - + + - + + - + - -... [7 more line(s); run it to see the whole thing] +... [9 more line(s); run it to see the whole thing] ``` **Shaped by:** `--since`, `--in` @@ -4394,22 +4378,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~4 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - - -... [6 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [11 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4470,8 +4454,8 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - - + @@ -4499,7 +4483,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=cbadf4aa6acda0c2 entries=787 +ripwire legend dictionary ripwire.dict/v1 dictv=593769ab691623d8 entries=806 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -4551,18 +4535,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4585,21 +4569,21 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + - - + + - + - + + - -... [7 more line(s); run it to see the whole thing] +... [11 more line(s); run it to see the whole thing] ``` **Shaped by:** `--skipped`, `--in`, `--index-out` @@ -4616,21 +4600,21 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - + - - + + - + - + + - -... [7 more line(s); run it to see the whole thing] +... [11 more line(s); run it to see the whole thing] ``` **Shaped by:** `--json`, `--limit` @@ -4650,16 +4634,17 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + - - + + - + - + + @@ -4698,16 +4683,17 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co ``` $ ./build/ripwire . --no-cache --top-k=3 - + - - + + - + - + + @@ -4727,16 +4713,17 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses ``` $ ./build/ripwire . --no-ignore --top-k=3 - + - - + + - + - + + @@ -4759,20 +4746,20 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea ``` $ ./build/ripwire . --max-file-size=8K --top-k=3 - + - - + + - + - - + + - - + + ... [3 more line(s); run it to see the whole thing] ``` @@ -4839,16 +4826,17 @@ _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and th ``` $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - + - - + + - + - + + @@ -4893,10 +4881,10 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" + - - + + - + - + - + + + + + - + - - - - + - - -… [800 more display lines; full output is 22162 bytes on 1 raw line(s)] + +… [726 more display lines; full output is 21893 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --top-k=5` @@ -62,24 +64,28 @@ *Same map, capped to the 5 highest-ranked symbols.* ````` - + - - + + - + - + + - + - - + + + + + @@ -90,11 +96,11 @@ *NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* ````` - + - + - + @@ -166,53 +172,53 @@ ripwire: --top-k=0 means "no ranked map, payload only" — pass a payload verb ( ````` - + ````` stderr: ````` -ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 +ripwire: --token-budget exceeded: withheld_est_tokens=8816 > budget=100 ````` ## `./build/ripwire . --for="incremental cache invalidation when a file content hash changes"` *The task lens: ranked signatures + quality metrics framed for the task.* -**wall time: 1.80s** +**wall time: 3.05s** ````` - - - - + + + + incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 - + and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 - + persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex - + the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no gra…inline const McpIndex& getIndex( const std::string& root ) -inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) -inline std::uint64_t contentHash64( std::string_view s ) noexcept -struct CacheEntry -inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFileScan& … [line truncated: 12 more bytes on this line] -inline std::uint64_t blobChecksum( std::string_view s ) noexcept -inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) -inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… -inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes, const s … [line truncated: 16 more bytes on this line] -inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, … [line truncated: 54 more bytes on this line] -inline std::string reAbsolutize( std::string_view rel, std::string_view root ) -inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) -struct IngestFileScan -inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t>& f … [line truncated: 17 more bytes on this line] -inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) -inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, cons … [line truncated: 91 more bytes on this line] -inline std::string spanTierMemoPath( const std::string& diskPath ) -inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) -inline NodeId resolveHandleAll( const McpIndex& ix, std::uint64_t idHash, std::vector< NodeId >& matches ) -… [46 more display lines; full output is 8961 bytes on 1 raw line(s)] +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFile … [line truncated: 20 more bytes on this line] +inline std::uint64_t blobChecksum( std::string_view s ) noexcept +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult& … [line truncated: 7 more bytes on this line] +inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes … [line truncated: 25 more bytes on this line] +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& impl … [line truncated: 63 more bytes on this line] +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t> … [line truncated: 25 more bytes on this line] +struct IngestFileScan +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +inline std::string spanTierMemoPath( const std::string& diskPath ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cac … [line truncated: 99 more bytes on this line] +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +inline std::uint64_t byteHash( const char* data, std::size_t n ) noexcept +… [45 more display lines; full output is 9403 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="rankGraphTeleport"` @@ -220,19 +226,19 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Name-shaped query: the router picks name-exact BM25 (header says which/why).* ````` - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -250,7 +256,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 } if( teleportMass > 0.0 ) { -… [12 more display lines; full output is 4288 bytes on 29 raw line(s)] +… [12 more display lines; full output is 4505 bytes on 29 raw line(s)] ````` ## `./build/ripwire . --for="rankGraphTeleport" --no-route` @@ -258,37 +264,37 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* ````` - - - - - + + + + + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -inline ChurnRanking churnRankedGraph( const MainDispatch& d ) - +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -struct RankedGraph -inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) -inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) -inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago", bo … [line truncated: 11 more bytes on this line] -inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) -PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… -inline std::string didYouMean( const IngestResult& ing, std::string_view name ) -inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = nullptr ) -PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double& … [line truncated: 37 more bytes on this line] -inline const McpIndex& getIndex( const std::string& root ) -inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = nullptr )< … [line truncated: 3 more bytes on this line] -struct DataSectionsCut -inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) -inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) -inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 -### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] -### 2. Gates for A -#### The convergence disclosure contract -… [102 more display lines; full output is 14999 bytes on 72 raw line(s)] +struct RankedGraph +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago& … [line truncated: 20 more bytes on this line] +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… +inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = n… +inline std::string didYouMean( const IngestResult& ing, std::string_view name ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span< … [line truncated: 45 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = n … [line truncated: 12 more bytes on this line] +struct DataSectionsCut +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### 2. Gates for A + +… [101 more display lines; full output is 15467 bytes on 72 raw line(s)] ````` ## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` @@ -296,14 +302,14 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* ````` - - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -315,37 +321,37 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* ````` - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -struct SliceScan -inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept -constexpr std::array<LangEntry, 51> kLangTable = -constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u -inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept -std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) -inline ModuleScan scanModule( std::string_view source, std::string_view path ) -std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] -bool jsonNestsTooDeep( std::string_view bytes ) noexcept -constexpr std::uint32_t kMaxYamlNestDepth = 64u -SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline bool hasMainGuard( std::string_view source ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) -struct LintRule -enum class DisclosureWhy : std::uint8_t -inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) -… [55 more display lines; full output is 8943 bytes on 1 raw line(s)] +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +constexpr std::array<LangEntry, 53> kLangTable = +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +struct AstQuerySpec +… [52 more display lines; full output is 9566 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` @@ -353,37 +359,37 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* ````` - - - - + + + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) - + P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() -inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) - +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f -inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) -inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) -inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) -inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) -inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) -struct NameCorpusStats -struct LensRanking -inline bool hasIdentifierShape( std::string_view w ) noexcept -inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) -struct Bm25Params -inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept -inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] -rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] -struct CandidateProvenance -std::optional<int> runEvalViews( const MainDispatch& d ) -inline constexpr int kLexWeightDoc = 2 -inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) -struct RecallSectionPick -inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) -… [55 more display lines; full output is 9161 bytes on 1 raw line(s)] +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct NameCorpusStats +struct LensRanking +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, … [line truncated: 27 more bytes on this line] +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +… [55 more display lines; full output is 10030 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` @@ -391,37 +397,37 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Same task with the anchor disabled — the contrast the flag exists for.* ````` - - - - + + + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) - + P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() -inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) - +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f -inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) -inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) -inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) -inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) -struct NameCorpusStats -struct LensRanking -inline bool hasIdentifierShape( std::string_view w ) noexcept -inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) -inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) -struct Bm25Params -inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept -inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] -rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] -struct CandidateProvenance -std::optional<int> runEvalViews( const MainDispatch& d ) -inline constexpr int kLexWeightDoc = 2 -inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) -struct RecallSectionPick -inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) -… [58 more display lines; full output is 9391 bytes on 1 raw line(s)] +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct NameCorpusStats +struct LensRanking +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, … [line truncated: 27 more bytes on this line] +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +struct RecallSectionPick +… [57 more display lines; full output is 9925 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lego=Vehicle` @@ -431,7 +437,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 ````` - + @@ -446,8 +452,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 ````` - + to imitate. window: shown= total= capped= (capped=1 cut). root=: p= relative to it. : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. candidates=N: instances of kind= under the ccx ceiling the pick was ranked from. low_confidence=1: weak t … [line truncated: 81 more bytes on this line] + f, A&&... a ) @@ -462,7 +468,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 && !std::is_const_v>> ) ), "pass rw::cstr( buf ) for a fixed char buffer: {} on a char[N] is not printf's %s" ); std::fputs( std::format( f, std::forward( a )... ).c_str(), stream ); -}]]>[[nodiscard]] const char16_t* c_str() const noexcept +}]]>[[nodiscard]] LPCWSTR c_str() const[[nodiscard]] const char16_t* c_str() const noexcept ````` ## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` @@ -495,9 +501,9 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* ````` -ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 +ripwire recall — "quality delta gating exit codes" — 113 relevant of 216 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.970) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 8.038) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 214142 B; lines="2710-2722,2723-2730"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -521,7 +527,7 @@ refuses by default. -━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.324) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] +━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.384) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] ### Root spelling (maintainer-owned; reproduce it once so you recognise it) ```bash @@ -530,38 +536,17 @@ refuses by default. ## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` -*The directory-as-knowledge-base pattern: --recall pointed at a 1411912-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* +*The directory-as-knowledge-base pattern: --recall pointed at a 1416688-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* ````` -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 - -━━ commands.md (relevance 11.013) ━━ [sections: 1 of 159 selected (187 in doc), section-granular; whole doc 546809 B; lines="528-553"; dropped_by_budget=158] [truncated: 1854 of 4293 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +━━ commands.md (relevance 10.145) ━━ [sections: 1 of 159 selected (188 in doc), section-granular; whole doc 551924 B; lines="3754-3758"; dropped_by_budget=158] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd - `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, - `.log`, `.json` and extensionless files are **not** documents to it. A directory of those - answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched - your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, - never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir - holds five dumps, and the header says `2 relevant of 2 document files` because only the two - `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not - in the population at all. - -2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so - an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document - SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R - selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got - and what the ceiling cost. … +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] (capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ````` @@ -578,13 +563,13 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas ````` - + - + @@ -594,27 +579,27 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas - - + - + + - + - - + + - + + - -… [366 more display lines; full output is 11120 bytes on 1 raw line(s)] +… [367 more display lines; full output is 11200 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --html=/aux/map2.html` *Self-contained HTML force-directed call graph.* -**wall time: 1.08s** +**wall time: 1.38s** ````` (empty) @@ -623,7 +608,7 @@ ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pas Artifact written: ````` - 158883 /aux/map2.html + 159214 /aux/map2.html ````` ## `./build/ripwire . --order=stable --top-k=5` @@ -631,7 +616,7 @@ Artifact written: *Stable (path/id) emit order — provider KV-cache hits across re-runs.* ````` - + @@ -642,16 +627,20 @@ Artifact written: - - + + + + + - + + - + ````` @@ -664,20 +653,18 @@ Artifact written: *Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* ````` - + - - + + - - - - + + @@ -687,14 +674,16 @@ Artifact written: + - - - - -… [225 more display lines; full output is 9149 bytes on 1 raw line(s)] + + + + + +… [195 more display lines; full output is 9060 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` @@ -702,20 +691,18 @@ Artifact written: *The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* ````` - + - - + + - - - - + + @@ -725,14 +712,16 @@ Artifact written: + - - - - -… [2996 more display lines; full output is 79305 bytes on 1 raw line(s)] + + + + + +… [2616 more display lines; full output is 77875 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` @@ -740,20 +729,18 @@ Artifact written: *The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* ````` - + - - + + - - - - + + @@ -763,14 +750,16 @@ Artifact written: + - - - - -… [31 more display lines; full output is 4174 bytes on 1 raw line(s)] + + + + + +… [20 more display lines; full output is 4221 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --callers=rankGraphTeleport` @@ -779,14 +768,14 @@ Artifact written: ````` - + - - + + - - - + + + ````` @@ -797,14 +786,14 @@ Artifact written: ````` - + - - + + - - - + + + ````` @@ -830,15 +819,15 @@ ripwire: --callers symbol not found: DoesNotExist *What SYM calls (1-hop out-edges).* ````` - - - - - - - - - + + + + + + + + + ````` @@ -849,16 +838,16 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - - + + - - - - - + + + + + ````` @@ -868,35 +857,35 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - + - - + + - + - + - + - - - - - - - - - - - - + + + + + + + + + + + + … [28 more display lines; full output is 3649 bytes on 1 raw line(s)] ````` @@ -906,36 +895,36 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - -… [73 more display lines; full output is 5563 bytes on 1 raw line(s)] + + + + +… [73 more display lines; full output is 5583 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --external-surface --include-builtins` @@ -944,36 +933,36 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -… [73 more display lines; full output is 5441 bytes on 1 raw line(s)] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5446 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --path=main,rankGraphTeleport` @@ -982,12 +971,12 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - - - - - + + + + + + ````` @@ -997,10 +986,10 @@ ripwire: --callers symbol not found: DoesNotExist ````` - + - + @@ -1014,36 +1003,36 @@ ripwire: --callers symbol not found: DoesNotExist ````` - - - - - - + + + + + - + - + - + - - - - - - - - - -… [48 more display lines; full output is 5280 bytes on 1 raw line(s)] + + + + + + + + + +… [50 more display lines; full output is 5372 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --mentions=rankGraphTeleport` @@ -1069,7 +1058,7 @@ ripwire: --callers symbol not found: DoesNotExist ````` - @@ -1084,8 +1073,8 @@ ripwire: --callers symbol not found: DoesNotExist + - ````` @@ -1096,7 +1085,7 @@ ripwire: --callers symbol not found: DoesNotExist ````` ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: c7920353a +at: f1e5c2e76 (0 changed files — working tree is clean, nothing to analyze) ````` @@ -1104,11 +1093,11 @@ at: c7920353a *Pre-PR gate on a CLEAN tree: no obligations, exit 0.* -**wall time: 1.87s** +**wall time: 2.11s** ````` - ````` @@ -1118,8 +1107,8 @@ at: c7920353a ````` captured nodes with their enclosing symbol; grammars=/eligible_files= the scope. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). counts_floor=1: every count is a FLOOR, never a total. hits_capped=1: hits= is a floor. root=: p= relative to it. of_files=: indexed files in all (eligible_files= of them are in the query's languages). --> -if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench @@ -1329,36 +1322,36 @@ at: c7920353a ````` - + - - + + - + - + + - - - - + - -… [28 more display lines; full output is 3359 bytes on 1 raw line(s)] + + + +… [24 more display lines; full output is 3484 bytes on 1 raw line(s)] ````` @@ -1370,38 +1363,40 @@ at: c7920353a *Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* +**wall time: 1.01s** + ````` - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f - + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - + The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] - + What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) -struct RankedGraph -inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -inline std::uint32_t sliceRdIterationCeiling() noexcept -struct PageRankRun -inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept -inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… -inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window -inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept -inline std::string powerShellSingleQuote( std::string_view s ) noexcept -inline std::string path_prepend_hint( std::string_view dir ) -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` -### Same answer, a fraction of the tokens — read this table first if your agent is on a budget -### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] -## Background — read these, in this order -## Design space and constraints -COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] -… [106 more display lines; full output is 13660 bytes on 72 raw line(s)] +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / - … [line truncated: 48 more bytes on this line] +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] +## Background — read these, in this order +… [106 more display lines; full output is 14345 bytes on 72 raw line(s)] ````` ## `./build/ripwire . --pack-signatures --top-k=10` @@ -1411,35 +1406,35 @@ at: c7920353a ````` - - + + - + - + - + + - + - + - - - - -… [135 more display lines; full output is 11815 bytes on 1 raw line(s)] + + + +… [132 more display lines; full output is 11518 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` @@ -1451,7 +1446,7 @@ at: c7920353a signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1480,7 +1475,7 @@ at: c7920353a signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1511,11 +1506,11 @@ ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — *Full body + inline callee signatures.* ````` - + @@ -1682,7 +1677,7 @@ ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — // • this type's 2 inline slots, with size() branch-free. // The measurement chose the second. See bench/SVECTORAB.md; the short version is below. // -… [1088 more display lines; full output is 66245 bytes on 1114 raw line(s)] +… [1014 more display lines; full output is 66264 bytes on 1040 raw line(s)] ````` stderr: @@ -1701,37 +1696,37 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Fan-in/out + complexity annotations on the map.* ````` - + - - + + - + - + - + - + + - + - + - - - - -… [18 more display lines; full output is 5295 bytes on 1 raw line(s)] + + + +… [19 more display lines; full output is 5624 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --deps` @@ -1740,15 +1735,15 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan ````` - - - - + + + + + - - - - + + + @@ -1756,141 +1751,141 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - - - - - + + + + + + + - - -… [888 more display lines; full output is 23141 bytes on 1 raw line(s)] +… [894 more display lines; full output is 23411 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --hotspots` *Complexity x recent git churn (maintenance pain).* -**wall time: 1.07s** +**wall time: 1.30s** ````` - - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + - - - - - + + + + - - -… [14 more display lines; full output is 5750 bytes on 1 raw line(s)] + +… [14 more display lines; full output is 5749 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --clones` *Token-normalized duplicate bodies.* -**wall time: 2.55s** +**wall time: 2.03s** ````` - + - + - + - + - + - + - + - + - + -… [307 more display lines; full output is 16180 bytes on 1 raw line(s)] +… [306 more display lines; full output is 16116 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --cochange` *Files that change together in git (hidden coupling).* -**wall time: 1.69s** +**wall time: 1.34s** ````` - - - + + + + - - - + - + + - + - - + + - + + + - - + + - - - - - - + + + + ````` @@ -1898,40 +1893,40 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Hotspots scoped to RECENT churn (the regression lens).* -**wall time: 1.29s** +**wall time: 1.12s** ````` - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - -… [13 more display lines; full output is 5099 bytes on 1 raw line(s)] + + + + + + + + +… [13 more display lines; full output is 5104 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --arch=test/archfix/rules.txt` @@ -1941,7 +1936,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan ````` - @@ -1969,47 +1964,47 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan -… [563 more display lines; full output is 82067 bytes on 1 raw line(s)] +… [640 more display lines; full output is 93738 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lint` *Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* -**wall time: 1.69s** +**wall time: 1.94s** ````` - - - + + + - + - - + + - - + + - - + + - + - + - - - -… [696 more display lines; full output is 69779 bytes on 1 raw line(s)] + + + +… [697 more display lines; full output is 69958 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` @@ -2096,36 +2091,36 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - - - + + + - + - + - - - - + + + + - - - - - - + + + + + + - + - + -… [187 more display lines; full output is 16530 bytes on 1 raw line(s)] +… [186 more display lines; full output is 16645 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --zoom` @@ -2134,36 +2129,36 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - - + + - + - + - + - + - + - + - + - + - + + - - + - + - -… [171 more display lines; full output is 8315 bytes on 1 raw line(s)] + +… [169 more display lines; full output is 8620 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --zoom --zoom-levels=3` @@ -2172,36 +2167,36 @@ The joined finding — past the display cut above, extracted so the join is visi ````` - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + -… [699 more display lines; full output is 27481 bytes on 1 raw line(s)] +… [689 more display lines; full output is 27707 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --report` @@ -2213,33 +2208,33 @@ The joined finding — past the display cut above, extracted so the join is visi # ripwire architecture report -2484 files · 23851 symbols · 34236 edges · 2324 modules (10910 call-graph isolated) +2688 files · 25558 symbols · 40392 edges · 2494 modules (11469 call-graph isolated) Root: `.` -Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3087 declaration, 1678 header, 2119 source, 4585 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 2324) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1374 symbols -- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1135 symbols -- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 534 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [emit,collect,push]** — 256 symbols +## Modules (call-graph clusters; showing 12 of 2494) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1685 symbols +- **src::append@elixir_resolve.h:111:4875 [add,resolve,compute]** — 1021 symbols +- **src::emplace@infra/svector.h:408:22477 [resolve,scan,find]** — 605 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 360 symbols +- **src::pageWindow@pageview.h:129:10574 [run,emit,count]** — 179 symbols - **docs::`--top-k=N`@COMMANDS.md:59:8861 [pack,allow,insert]** — 177 symbols -- **src::realpath@infra/os.h:199:13067 [parse,split,read]** — 63 symbols -- **src::relForHash@model.h:1438:116507 [compute,clone,read]** — 60 symbols -- **src::assign@infra/svector.h:342:19905 [scan,run,resolve]** — 54 symbols -- **src/infra::DYNMAP_ASSUME@dynamic_map.hpp:90:4411 [erase,find,count]** — 51 symbols -- **src/infra::active@profilePmc.h:845:34910 [print,ensure,report]** — 49 symbols -- **src::rowOffsets@infra/sparseCsr.h:248:8990 [compute,connect,verify]** — 38 symbols -- **test::no@decltodefcheck.sh:127:11708 [run]** — 30 symbols - -## God files (most depended-on; showing 10 of 445) -- `test/lib/clean-env.sh` — 181 dependents +- **src::relForHash@model.h:1632:133351 [clone,compute,scan]** — 57 symbols +- **src/infra::DYNMAP_ASSUME@dynamic_map.hpp:90:4411 [erase,find,insert]** — 49 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,compute,verify]** — 39 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [open,handle,fill]** — 39 symbols +- **src/infra::active@profilePmc.h:898:37172 [ensure,read,run]** — 38 symbols +- **test::no@receiverevidencecheck.sh:159:14987 [expand,map,connect]** — 36 symbols + +## God files (most depended-on; showing 10 of 476) +- `test/lib/clean-env.sh` — 185 dependents +- `src/model.h` — 88 dependents - `src/infra/emit.h` — 86 dependents -- `src/model.h` — 85 dependents -- `src/infra/Diagnostics.h` — 58 dependents -- `src/serialize.h` — 39 dependents -… [32 more lines, 3523 bytes total] +- `src/infra/Diagnostics.h` — 60 dependents +- `src/serialize.h` — 40 dependents +… [33 more lines, 3644 bytes total] ````` ## `./build/ripwire . --seams` @@ -2248,13 +2243,13 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 ````` - - - - + + + - + + @@ -2264,11 +2259,11 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 - - - - - + + + + + @@ -2277,7 +2272,7 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 -… [102 more display lines; full output is 13425 bytes on 1 raw line(s)] +… [92 more display lines; full output is 12415 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --mermaid` @@ -2288,22 +2283,23 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
7066"] - n102["src/infra
1276"] + n101["src
7731"] + n102["src/infra
1280"] end subgraph sg1 ["test"] - n103["test
4853"] - n328["test/fixtures/recallpassage
314"] + n103["test
5057"] + n333["test/fixtures/recallpassage
314"] n280["test/expandmodefix
151"] n282["test/expandsibsfix
149"] - n458["test/rubyattrsfix
85"] - n395["test/massfix
77"] - n360["test/javamethodreffix
62"] + n535["test/rubyattrsfix
85"] + n411["test/massfix
77"] + n492["test/receiverevidencefix/js/lib
74"] + n376["test/javamethodreffix
62"] end - n73["docs
831"] - n2[".github
598"] + n73["docs
833"] + n2[".github
604"] subgraph sg4 ["bench"] - n4["bench
531"] + n4["bench
545"] n5["bench/agentloop
331"] n49["bench/locbench/results/r5_pooling
237"] n50["bench/locbench/results/r6_expansion
187"] @@ -2314,30 +2310,30 @@ flowchart LR n62["bench/shotgun
96"] n45["bench/locbench/results/r1cpp_anchorhop
94"] n11["bench/arb
85"] - n56["bench/nestcal/r1-2026-08-07
84"] -… [21 more lines, 1606 bytes total] +… [21 more lines, 1622 bytes total] ````` ## `./build/ripwire . --owners` *Bus-factor: recency-weighted author ownership per file.* -**wall time: 1.56s** +**wall time: 1.31s** ````` - - + + - - + + - + - - + + + @@ -2350,12 +2346,11 @@ flowchart LR + - - -… [884 more display lines; full output is 97571 bytes on 1 raw line(s)] +… [913 more display lines; full output is 100493 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --dead-code=src` @@ -2363,8 +2358,8 @@ flowchart LR *Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* ````` - - + + ````` @@ -2386,7 +2381,7 @@ flowchart LR ````` - ````` @@ -2395,11 +2390,11 @@ flowchart LR *On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* -**wall time: 15.01s** +**wall time: 15.43s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + @@ -2422,13 +2417,13 @@ flowchart LR - - + + + + - - - -… [107 more display lines; full output is 13557 bytes on 1 raw line(s)] + +… [118 more display lines; full output is 14594 bytes on 1 raw line(s)] ````` stderr: @@ -2441,17 +2436,17 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* -**wall time: 6.84s** +**wall time: 8.54s** ````` - - - - - - + + + + + ````` @@ -2462,7 +2457,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g ````` - ````` @@ -2471,55 +2466,55 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* -**wall time: 2.11s** +**wall time: 2.12s** ````` - - + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - -… [96 more display lines; full output is 7654 bytes on 1 raw line(s)] + + + + + + + + + + +… [290 more display lines; full output is 19612 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` *Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* -**wall time: 10.70s** +**wall time: 10.66s** ````` - - + + - + @@ -2531,11 +2526,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* -**wall time: 3.02s** +**wall time: 3.21s** ````` - + @@ -2564,29 +2559,31 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g -… [319 more display lines; full output is 39445 bytes on 1 raw line(s)] +… [186 more display lines; full output is 25429 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` *A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* +**wall time: 1.11s** + ````` - + - - - + + + - + - + @@ -2596,40 +2593,40 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* -**wall time: 59.44s** +**wall time: 60.73s** ````` - + - - - - + - - - - - - - - - + + + + + - - - + -… [115194 more display lines; full output is 12883655 bytes on 1 raw line(s)] + + + + + + + + + +… [39161 more display lines; full output is 5744746 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` @@ -2653,52 +2650,37 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* -**exit code: 2** — **wall time: 14.23s** +**wall time: 15.96s** ````` - - - - + + - - + + + - - - - - - - - - - - - - + + + - - - - - -… [37 more display lines; full output is 4510 bytes on 1 raw line(s)] + + ````` ## `./build/ripwire . --whereis=rankGraphTeleport` *Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.* -**wall time: 9.47s** +**wall time: 12.33s** ````` - - + + -… [35 more display lines; full output is 18444 bytes on 1 raw line(s)] +… [35 more display lines; full output is 18446 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` *The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).* -**wall time: 9.48s** +**wall time: 11.28s** ````` - - - + + - - - - - - - - - - - - + + + + + + + + + + + + - - + + … [34 more display lines; full output is 13695 bytes on 1 raw line(s)] @@ -2773,12 +2755,12 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* -**wall time: 16.05s** +**wall time: 16.83s** ````` - - + + @@ -2806,24 +2788,26 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer -… [36 more display lines; full output is 15188 bytes on 1 raw line(s)] +… [36 more display lines; full output is 15183 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --flags` *The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* +**wall time: 1.01s** + ````` - + - + - + @@ -2844,7 +2828,7 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer -… [366 more display lines; full output is 22644 bytes on 1 raw line(s)] +… [374 more display lines; full output is 23065 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` @@ -2853,7 +2837,7 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer ````` - + @@ -2865,17 +2849,20 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer - + - - - - + + + + + + + ````` @@ -2901,47 +2888,47 @@ ripwire: run `ripwire . --flags` for the gate table *NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* -**wall time: 1.27s** +**wall time: 1.50s** ````` -{"v":1,"verb":"plan-lanes","at":"c7920353a","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23851,"edges":342 … [line truncated: 339 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"f1e5c2e76","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2688,"symbols":25558,"edges":403 … [line truncated: 339 more bytes on this line] "symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, {"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2909,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, -{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":640,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":644,"id":"./src/mcpverbs.h::rw::docDriftText"}, {"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1642,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/gitmine.h","n":"gitWindowBoundarySha","scope":"rw","key":"18627516699d7062","id":"./src/gitmine.h::rw::gitWindowBoundarySha","id_addressable":true,"id_collides_with":0,"l":1614,"ord":0,"overloads":1,"amb":3,"cx":3,"ccx":2,"churn":60,"tested":0}, {"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2685,"ord":0,"overloads":1,"amb":3,"cx":6,"ccx":6,"churn":36,"tested":0}, -{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":2,"cx":8,"ccx":8,"churn":31,"tested":0}, -{"p":"./src/docdrift.h","n":"docDriftNextAttr","scope":"docdrift","key":"7723ed789a1c9c09","id":"./src/docdrift.h::docdrift::docDriftNextAttr","id_addressable":true,"id_collides_with":0,"l":2864,"ord":0,"overloads":1,"amb":0,"cx":6,"ccx":5,"churn":36,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":2,"cx":8,"ccx":8,"churn":32,"tested":0}, {"p":"./src/docdrift.h","n":"recordWeakDisclosures","scope":"docdrift","key":"972779210e49c0cf","id":"./src/docdrift.h::docdrift::recordWeakDisclosures","id_addressable":true,"id_collides_with":0,"l":2430,"ord":0,"overloads":1,"amb":2,"cx":2,"ccx":1,"churn":36,"tested":0}, {"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1344,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":48,"tested":0}, -{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":2863,"ord":0,"overloads":1,"amb":33,"cx":8,"ccx":11,"churn":414,"tested":0}, -{"p":"./src/gitmine.h","n":"resolveSinceScope","scope":"rw","key":"9caaa3dbeaa688d0","id":"./src/gitmine.h::rw::resolveSinceScope","id_addressable":true,"id_collides_with":0,"l":326,"ord":0,"overloads":1,"amb":3,"cx":7,"ccx":7,"churn":59,"tested":0}, -{"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":350,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":18,"tested":0}, -{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":685,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":414,"tested":0}, -{"p":"./src/taskroute.h","n":"testCoverageTaskChoice","scope":"rw::taskroute","key":"bf72991aa28355c7","id":"./src/taskroute.h::rw::taskroute::testCoverageTaskChoice","id_addressable":true,"id_collides_with":0,"l":1365,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":34,"tested":0}, -{"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5191,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":309,"tested":0}], -"files":[{"p":"./src/darkflags.h","symbols":1,"churn":31,"ccx":8,"hotspot_rank":45}, -{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":31}, -{"p":"./src/gitmine.h","symbols":1,"churn":59,"ccx":7,"hotspot_rank":19}, -{"p":"./src/main.cpp","symbols":2,"churn":414,"ccx":12,"hotspot_rank":4}, -{"p":"./src/mcpverbs.h","symbols":1,"churn":309,"ccx":2,"hotspot_rank":5}, -{"p":"./src/pageview.h","symbols":1,"churn":18,"ccx":2,"hotspot_rank":134}, -{"p":"./src/taskroute.h","symbols":1,"churn":34,"ccx":2,"hotspot_rank":41}, -{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":34}, -{"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":8,"hotspot_rank":23}]},"blast_radius":{"reaches":51,"files_total":17,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editplan.h","./src/flipimpact.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./s … [line truncated: 203 more bytes on this line] -"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":51,"module_span":1,"notes":[], -"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"high","rule":"complex-or-wide","basis":"structural-only","signals":{"claims":13,"files":9,"module_span":1,"max_ccx":11,"sum_ccx":59,"ambiguous_calls":49,"blast_reaches":51,"contract_touches":0,"conflicts":0,"untested":51,"tests … [line truncated: 135 more bytes on this line] -… [72 more display lines; full output is 20570 bytes on 1 raw line(s)] +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":2867,"ord":0,"overloads":1,"amb":33,"cx":8,"ccx":11,"churn":424,"tested":0}, +{"p":"./src/gitmine.h","n":"resolveSinceScope","scope":"rw","key":"9caaa3dbeaa688d0","id":"./src/gitmine.h::rw::resolveSinceScope","id_addressable":true,"id_collides_with":0,"l":326,"ord":0,"overloads":1,"amb":3,"cx":7,"ccx":7,"churn":60,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":688,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":424,"tested":0}, +{"p":"./src/taskroute.h","n":"testCoverageTaskChoice","scope":"rw::taskroute","key":"bf72991aa28355c7","id":"./src/taskroute.h::rw::taskroute::testCoverageTaskChoice","id_addressable":true,"id_collides_with":0,"l":1392,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":39,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5390,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":340,"tested":0}, +{"p":"./src/cli.h","n":"validateModifierGuards","scope":"rw","key":"ec2848a4801493a2","id":"./src/cli.h::rw::validateModifierGuards","id_addressable":true,"id_collides_with":0,"l":4571,"ord":0,"overloads":1,"amb":33,"cx":73,"ccx":66,"churn":450,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":1,"churn":450,"ccx":66,"hotspot_rank":6}, +{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":45}, +{"p":"./src/docdrift.h","symbols":2,"churn":36,"ccx":7,"hotspot_rank":31}, +{"p":"./src/gitmine.h","symbols":2,"churn":60,"ccx":9,"hotspot_rank":20}, +{"p":"./src/main.cpp","symbols":2,"churn":424,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":340,"ccx":2,"hotspot_rank":5}, +{"p":"./src/taskroute.h","symbols":1,"churn":39,"ccx":2,"hotspot_rank":38}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":36}, +{"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":8,"hotspot_rank":23}]},"blast_radius":{"reaches":47,"files_total":17,"capped":false,"files":["./src/cli.h","./src/docdrift.h","./src/editplan.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./src/mcpserver.h","./src/mcpv … [line truncated: 194 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":9,"module_span":1,"max_ccx":66,"sum_ccx":118,"ambiguous_calls":85,"blast_reaches":47,"contract_touches":1,"conflicts":0,"untested":47," … [line truncated: 140 more bytes on this line] +{"id":"lane-1","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/main.cpp","n":"kJsonRideAlongFlags","scope":"","key":"2a90806fa5a580c1","id":null,"id_addressable":false,"id_collides_with":0,"l":3079,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx … [line truncated: 28 more bytes on this line] +… [78 more display lines; full output is 21430 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` *NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* -**wall time: 1.34s** +**wall time: 1.55s** Input file: @@ -2952,37 +2939,37 @@ write regression tests for the new filter ````` ````` -{"v":1,"verb":"plan-lanes","at":"c7920353a","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23851,"edges":34236,"ambiguous":10278,"unresolved":12878},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"f1e5c2e76","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2688,"symbols":25558,"edges":40392,"ambiguous":12495,"unresolved":12552},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] "symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] {"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, -{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":640,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":309,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3204,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":644,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":340,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3225,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":450,"tested":0}, {"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, {"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":12,"cx":14,"ccx":15,"churn":36,"tested":0}, -{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":12,"cx":26,"ccx":38,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":13,"cx":26,"ccx":38,"churn":36,"tested":0}, {"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, {"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1642,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":70,"tested":0}, {"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, -{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":67,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":41,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5390,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":340,"tested":0}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], -"files":[{"p":"./src/cli.h","symbols":1,"churn":436,"ccx":0,"hotspot_rank":6}, +"files":[{"p":"./src/cli.h","symbols":1,"churn":450,"ccx":0,"hotspot_rank":6}, {"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":31}, -{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":87}, -{"p":"./src/mcprefusal.h","symbols":1,"churn":41,"ccx":0,"hotspot_rank":54}, -{"p":"./src/mcpverbs.h","symbols":1,"churn":309,"ccx":0,"hotspot_rank":5}, -{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":52}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":92}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":340,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":56}, {"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":4,"hotspot_rank":23}], -"symbols_total":3147,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], -"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":12,"module_span":6,"notes":[], -"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":7,"module_span":6,"max_ccx":38,"sum_ccx":62,"ambiguous_calls":26,"blast_reaches":12,"contract_touches":1,"conflicts":3,"untested":12,"t … [line truncated: 139 more bytes on this line] -{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":107,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] -{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2828,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3204,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, -{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2907,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":436,"tested":0}, -{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":107,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, +"symbols_total":3195,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":13,"module_span":5,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":6,"module_span":5,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":28,"blast_reaches":13,"contract_touches":1,"conflicts":1,"untested":13,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":110,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2849,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":450,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3225,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":450,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2928,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":450,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":110,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, {"p":"./src/slice.h","n":"sliceRdIterationCeiling","scope":"slicev","key":"449132b302bb4324","id":"./src/slice.h::slicev::sliceRdIterationCeiling","id_addressable":true,"id_collides_with":0,"l":1372,"ord":0,"overloads":1,"amb":0,"cx":9,"ccx":12,"churn":51,"tested":0}, {"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":41,"tested":0}, -… [63 more display lines; full output is 19235 bytes on 1 raw line(s)] +{"p":"./src/cli.h","n":"serveHelpSelector","scope":"rw","key":"4b247ee064fc4e94","id":"./src/cli.h::rw::serveHelpSelector","id_addressable":true,"id_collides_with":0,"l":2993,"ord":0,"overloads":1,"amb":3,"cx":4,"ccx":3,"churn":450,"tested":0}, +… [60 more display lines; full output is 18774 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --plan-lanes=99 --task=x` @@ -3007,8 +2994,8 @@ ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out ````` - - + + @@ -3036,7 +3023,7 @@ ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out - + ````` @@ -3063,36 +3050,36 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped ````` - - + +
- - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + - - - - - -… [243 more display lines; full output is 25199 bytes on 1 raw line(s)] + + + + + + +… [243 more display lines; full output is 25243 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --doc-drift --gateability` @@ -3101,36 +3088,36 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped ````` - - + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + - - - - - -… [269 more display lines; full output is 26398 bytes on 1 raw line(s)] + + + + + + +… [269 more display lines; full output is 26442 bytes on 1 raw line(s)] ````` Tail of the same output — the `` section: @@ -3138,13 +3125,13 @@ Tail of the same output — the `` section: ````` - + + - + - - + @@ -3169,36 +3156,36 @@ Tail of the same output — the `` section: ````` - - - + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + - - - - -… [221 more display lines; full output is 25113 bytes on 1 raw line(s)] + + + + + +… [222 more display lines; full output is 25269 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --from-trace=-` @@ -3211,34 +3198,34 @@ Input file: AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:5567 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 - #3 0x102e01a44 in main src/main.cpp:3975 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:7926 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:7967 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1634 + #3 0x102e01a44 in main src/main.cpp:3999 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ````` ````` - - + + - - - - + + + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] - + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) -int runDefaultMap( const MainDispatch& d ) -int main( int argc, char** argv ) +int runDefaultMap( const MainDispatch& d ) +int main( int argc, char** argv ) - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -3250,7 +3237,7 @@ AddressSanitizer:DEADLYSIGNAL if( N ) { double teleportMass = 0.0; -… [18 more display lines; full output is 5100 bytes on 29 raw line(s)] +… [18 more display lines; full output is 5251 bytes on 29 raw line(s)] ````` ## `./build/ripwire . --notes` @@ -3281,17 +3268,17 @@ AddressSanitizer:DEADLYSIGNAL *ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* ````` - + + (run= when derivable). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. : that callee matched by name alone (receiver unprove … [line truncated: 552 more bytes on this line] + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -3301,17 +3288,17 @@ AddressSanitizer:DEADLYSIGNAL mcp-tools pass-through. Written into the server command only where its argument shape is known to be one more plain argument: the JSON args arrays (cursor/windsurf/gemini), opencode's command arrayinline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexcept - -knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedBy makes in full above, which applies here verbatim: firstFlagOutside() walks the rows parseArgs itself minline constexpr std::string_view kInRideAlong[] = +struct AgentTarget - + - - + + -… [47 more display lines; full output is 9435 bytes on 38 raw line(s)] + +… [59 more display lines; full output is 10197 bytes on 51 raw line(s)] ````` ## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` @@ -3319,13 +3306,13 @@ AddressSanitizer:DEADLYSIGNAL *Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* ````` - wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> - - - + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> + + + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) @@ -3335,10 +3322,10 @@ AddressSanitizer:DEADLYSIGNAL mcp-tools pass-through. Written into the server command only where its argument shape is known t…inline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept - + -inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept @@ -3349,54 +3336,52 @@ AddressSanitizer:DEADLYSIGNAL case McpForm::Json: case McpForm::JsonMcpKey: return true; -… [70 more display lines; full output is 23483 bytes on 77 raw line(s)] +… [60 more display lines; full output is 22824 bytes on 67 raw line(s)] ````` ## `./build/ripwire . --for="pagerank power iteration" --with-graph` *Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges.* -**wall time: 1.06s** - ````` - - - - - + + + + + NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f - + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - + The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] - + What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure -inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) -struct RankedGraph -inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -inline std::uint32_t sliceRdIterationCeiling() noexcept -struct PageRankRun -inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept -inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… -inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window -inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept -inline std::string powerShellSingleQuote( std::string_view s ) noexcept -inline std::string path_prepend_hint( std::string_view dir ) -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` -### Same answer, a fraction of the tokens — read this table first if your agent is on a budget -### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] -## Background — read these, in this order -## Design space and constraints -COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] -… [60 more display lines; full output is 9337 bytes on 10 raw line(s)] +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] +## Background — read these, in this order +## Design space and constraints +… [59 more display lines; full output is 9772 bytes on 10 raw line(s)] ````` ## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` *Per-file metrics as CodeCharta cc.json.* -**wall time: 1.02s** +**wall time: 1.23s** ````` (empty) @@ -3405,7 +3390,7 @@ AddressSanitizer:DEADLYSIGNAL Artifact written: ````` - 374998 /aux/ripwire2.cc.json + 409075 /aux/ripwire2.cc.json {"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri ````` @@ -3413,7 +3398,7 @@ Artifact written: *One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* -**wall time: 1.37s** +**wall time: 2.95s** Input file: @@ -3428,34 +3413,40 @@ lego:Vehicle - - - + + + - - + + incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 - + Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) - + Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] - + P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) -inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) -struct FileHealth -inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vec… -inline const McpIndex& getIndex( const std::string& root ) -inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) -struct McpIndex -### See the map — not just the numbers -inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… -inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) -inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& cacheStats … [line truncated: 59 more bytes on this line] -IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool respect … [line truncated: 29 more bytes on this line] -const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) -static bool cachePathIsDirectory( const std::string& cachePath ) -inline constexpr std::array<std::string_view, 8> kCacheRuleNames = -… [57 more display lines; full output is 41093 bytes on 1 raw line(s)] +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +struct FileHealth +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::ve … [line truncated: 8 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +struct McpIndex +inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& ca … [line truncated: 67 more bytes on this line] +IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool … [line truncated: 37 more bytes on this line] +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) +static bool cachePathIsDirectory( const std::string& cachePath ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) +… [57 more display lines; full output is 42099 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 546 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) ````` @@ -3467,18 +3458,18 @@ lego:Vehicle *Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* -**exit code: 1** — **wall time: 2.61s** +**exit code: 1** — **wall time: 3.00s** ````` - - + - - + + - - + @@ -3530,56 +3521,63 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s *Rank by git change-frequency prior instead of PageRank.* -**wall time: 1.07s** +**wall time: 1.32s** ````` - + - - + + - + + - + - + + - - + + ````` -## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~4 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` *Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* ````` - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + - + ````` @@ -3605,12 +3603,12 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf ````` - + 0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp 0,1,1,2,3,3,3 getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap -1165,5606,6252,171,1380,1419,1629 +1165,7965,8611,171,1383,1422,1632 fn,fn,fn,fn,fn,fn,fn 0,0,0,0,0,0,0 @@ -3623,20 +3621,20 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf ````` - - + + inline std::string spanTierMemoPath( const std::string& diskPath ) - + inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) - + ### See the map — not just the numbers - + inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) - + const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) @@ -3663,13 +3661,13 @@ ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidate *Machine-parseable JSON, same content, keys mirror the XML attrs.* ````` -{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, -{"t":"fn","n":"rankGraph","p":"src/graph.h:5606"}, -{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:6252"}, +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:7965"}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:8611"}, {"t":"fn","n":"runEval","p":"src/eval.h:171"}, -{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1380"}, -{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1419"}, -{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1629"}]} +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1383"}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1422"}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1632"}]} ````` ## `./build/ripwire . --hotspots --json` @@ -3692,15 +3690,15 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Pagination: 3 items, skipping the first 3 (deterministic seams).* -**wall time: 1.06s** +**wall time: 1.33s** ````` - - - - + + + + ````` @@ -3709,24 +3707,28 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Drop test paths from the corpus before ranking.* ````` - + - - + + - + - + + - - + + + + + - - + + @@ -3737,24 +3739,28 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Drop matching paths (repeatable) before ranking.* ````` - + - - + + - + - + + - + - - + + + + + @@ -3765,24 +3771,28 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* ````` - + - - + + - + - + + - + - - + + + + + @@ -3792,19 +3802,20 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* -**wall time: 1.72s** +**wall time: 1.73s** ````` - + - - + + - + - + + @@ -3814,19 +3825,20 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* -**wall time: 1.80s** +**wall time: 1.89s** ````` - + - - + + - + - + + @@ -3835,7 +3847,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m Artifact written: ````` - 18649971 /aux/warm2.ripwirecache + 23065349 /aux/warm2.ripwirecache ````` ## `./build/ripwire . --max-file-size=8K --top-k=3` @@ -3843,20 +3855,20 @@ Artifact written: *Skip files above a size bound before parsing (note the corpus shrink in the header).* ````` - + - - + + - + - - + + - - + + @@ -3882,29 +3894,33 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Multi-root workspace: ONE merged graph over two roots, paths labeled /.* -**wall time: 1.59s** +**wall time: 1.62s** ````` - + - - + + - - + + - - + + + - + - - + + + + + @@ -3914,19 +3930,19 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Self-eval: co-change recall vs BM25.* -**wall time: 16.02s** +**wall time: 16.59s** ````` ripwire --eval (co-change recovery, averaged over 80 historical commits) ranker recall@5 recall@10 recall@20 - ripwire 4.1% 8.3% 15.1% - BM25 13.0% 15.1% 20.2% - BM25sub 17.3% 23.3% 31.6% - BM25body 13.7% 25.7% 44.5% - fused 14.2% 18.3% 29.6% - anchored 13.7% 25.7% 44.5% - same-dir 0.0% 8.8% 13.8% - random 0.2% 0.4% 0.8% <- floor (random ranking over F=2484 files) + ripwire 1.2% 8.5% 12.7% + BM25 14.0% 14.9% 20.9% + BM25sub 15.8% 23.8% 31.8% + BM25body 11.3% 24.2% 39.4% + fused 11.3% 23.0% 29.6% + anchored 11.3% 24.2% 39.4% + same-dir 0.2% 1.8% 6.1% + random 0.2% 0.4% 0.7% <- floor (random ranking over F=2688 files) note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 / BM25sub / BM25body are QUERY-TIME lexical rankers (whole-name / subtoken / @@ -3942,23 +3958,23 @@ ripwire --eval (co-change recovery, averaged over 80 historical commits) *Known-item retrieval eval: MRR + recall@k per ranker per query mode.* -**wall time: 134.50s** +**wall time: 126.79s** ````` ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) - sample: population=5833 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population + sample: population=6431 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population (smallest fnv1a64(scope::name) over the population, cut on the key so an identity is never split; path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) ranker query-mode MRR recall@1 recall@5 recall@10 - subtoken name 0.693 56.1% 85.8% 90.8% - subtoken doc-phrase 0.920 90.0% 94.0% 94.9% - name-exact name 0.905 83.5% 96.1% 97.4% + subtoken name 0.680 54.7% 84.4% 90.0% + subtoken doc-phrase 0.913 89.0% 93.5% 94.7% + name-exact name 0.892 81.3% 95.7% 97.0% name-exact doc-phrase 0.013 0.5% 1.8% 2.6% - anchored name 0.698 57.6% 85.2% 89.2% - anchored doc-phrase 0.917 89.5% 94.0% 94.5% - routed name 0.908 83.7% 96.3% 97.7% - routed doc-phrase 0.919 90.0% 93.8% 94.7% + anchored name 0.688 56.6% 83.7% 88.3% + anchored doc-phrase 0.910 88.5% 93.5% 94.3% + routed name 0.895 81.5% 95.9% 97.2% + routed doc-phrase 0.911 89.0% 93.3% 94.4% note: routing chose name-exact on 3986/4000 NAME queries (a NAME query is always identifier-shaped); the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — @@ -3971,7 +3987,7 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co *Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* -**exit code: 3** — **wall time: 7.70s** +**exit code: 3** — **wall time: 4.17s** Input file: @@ -4007,11 +4023,11 @@ what is the weather in Paris none neg ````` ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus '/aux/skills_labels2.tsv'; split test=4 dev=0) arm hit@1 hit@2 mrr sep-auc fire/abstain@ORACLE-th (upper bound) - overlap 66.7% 66.7% 0.690 0.000 50.0% (th=-1.000) + overlap 66.7% 66.7% 0.689 0.000 50.0% (th=-1.000) name 33.3% 66.7% 0.537 0.667 50.0% (th=0.000) - bm25-desc 66.7% 66.7% 0.690 0.667 75.0% (th=2.958) + bm25-desc 66.7% 66.7% 0.689 0.667 75.0% (th=2.865) bm25-full 33.3% 66.7% 0.611 1.000 50.0% (th=0.426) - for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.685) + for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.686) random 6.2% 12.5% 0.211 0.500 <- floor (uniform-random ranking; auc 0.5 by definition) provenance hit@1 (bm25-desc): router 0/0, desc 0/0, judged 2/3 (desc rows quote the descriptions - expect them easiest; judged is the honest number) judged-only hit@1 per arm: overlap 2/3, name 1/3, bm25-desc 2/3, bm25-full 1/3, for-routed 1/3 @@ -4035,7 +4051,7 @@ ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router ripwire-write-tests 0 0 0 0 0 misses (overlap): line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" -… [19 more lines, 3665 bytes total] +… [19 more lines, 3664 bytes total] ````` ## `./build/ripwire wrap claude` @@ -4081,7 +4097,7 @@ Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after th *Version + short build info.* ````` -ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c7920353a) +ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f1e5c2e76) ````` @@ -4089,31 +4105,31 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c792 # navigate — seeds, claims, slices, shapes -## `./build/ripwire . --at=src/graph.h:5567` +## `./build/ripwire . --at=src/graph.h:7926` *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* ````` - - + + ````` -## `./build/ripwire . --callers=@src/graph.h:5567` +## `./build/ripwire . --callers=@src/graph.h:7926` *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* ````` - + - - + + - - - + + + ````` @@ -4130,7 +4146,7 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=c792 stderr: ````` -ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 9418 lines — the seed asked for line 999999) +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 11822 lines — the seed asked for line 999999) ````` ## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` @@ -4139,9 +4155,9 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - - - + + + ````` @@ -4151,15 +4167,15 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. ````` - + - - - - - - - + + + + + + + ````` @@ -4194,25 +4210,23 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* -**wall time: 1.02s** - ````` - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ````` @@ -4222,17 +4236,17 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + @@ -4242,30 +4256,28 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* -**wall time: 1.04s** - ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + - + pw = biasPrior( g, p );]]> - + & p, float alpha = 0.85f )]]> - + & p, float alpha = 0.85f )]]> @@ -4277,38 +4289,38 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + - + - + - + - + - + 0.0 )]]> - + - + @@ -4352,17 +4364,17 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < ````` - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + @@ -4372,12 +4384,12 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* -**wall time: 1.82s** +**wall time: 1.16s** ````` rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) ````` @@ -4385,11 +4397,11 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* -**wall time: 1.67s** +**wall time: 1.23s** ````` -DISCLOSE( result, AbiResult::DisclosureWhy::NoMergeBase, "abi: no merge-base for a ref (unrelated history?) — that ref DISCLOSE( Diagnostics::answerRefused, "--arch exits 1 with path:line and the reason on stderr; no report is printed", DISCLOSE( baseline, ArchBaselineRead::DisclosureWhy::SymlinkRefused, "arch: refusing to read the arch baseline sidecar t @@ -4418,7 +4430,7 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < DISCLOSE( out, CMakeScan::DisclosureWhy::RootWalkFailed, "flags: cannot walk root for CMake files — cmake gates omitte DISCLOSE( r, Result::DisclosureWhy::BaseTreeUnavailable, "dmm: the base commit's tree could not be materialized or parse DISCLOSE( r, Result::DisclosureWhy::TargetTreeUnavailable, "dmm: the target commit's tree could not be materialized or p -… [73 more display lines; full output is 17739 bytes on 1 raw line(s)] +… [73 more display lines; full output is 17740 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --pattern='x'` @@ -4444,29 +4456,29 @@ ripwire: --pattern: this pattern collapses to a single bare token, which is a TE ````` - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -struct SliceScan -inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept -constexpr std::array<LangEntry, 51> kLangTable = -constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u -inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept -std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) -inline ModuleScan scanModule( std::string_view source, std::string_view path ) -std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] -bool jsonNestsTooDeep( std::string_view bytes ) noexcept -constexpr std::uint32_t kMaxYamlNestDepth = 64u -SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline bool hasMainGuard( std::string_view source ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) -struct LintRule -enum class DisclosureWhy : std::uint8_t -inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) -… [55 more display lines; full output is 8839 bytes on 1 raw line(s)] +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +constexpr std::array<LangEntry, 53> kLangTable = +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +struct AstQuerySpec +… [52 more display lines; full output is 9451 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose` @@ -4726,75 +4736,77 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* ````` - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -struct SliceScan -inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept -constexpr std::array<LangEntry, 51> kLangTable = -constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u -inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept -std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) -inline ModuleScan scanModule( std::string_view source, std::string_view path ) -std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] -bool jsonNestsTooDeep( std::string_view bytes ) noexcept -constexpr std::uint32_t kMaxYamlNestDepth = 64u -SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline bool hasMainGuard( std::string_view source ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) -struct LintRule -enum class DisclosureWhy : std::uint8_t -inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) -… [66 more display lines; full output is 9172 bytes on 1 raw line(s)] +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +constexpr std::array<LangEntry, 53> kLangTable = +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +struct AstQuerySpec +… [63 more display lines; full output is 9784 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` *Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* +**wall time: 1.02s** + ````` - - - - -inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) - + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) - +TSTree* parseTree( TSParser* parser, std::string_view src ) + Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() -struct FileHealth -enum class AstWalk : std::uint8_t -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -struct SliceScan -inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept -constexpr std::array<LangEntry, 51> kLangTable = -constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u -inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept -std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) -inline ModuleScan scanModule( std::string_view source, std::string_view path ) -std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] -bool jsonNestsTooDeep( std::string_view bytes ) noexcept -constexpr std::uint32_t kMaxYamlNestDepth = 64u -SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline bool hasMainGuard( std::string_view source ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) -struct LintRule -enum class DisclosureWhy : std::uint8_t -inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) -… [90 more display lines; full output is 12411 bytes on 55 raw line(s)] +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +constexpr std::array<LangEntry, 53> kLangTable = +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +struct AstQuerySpec +… [90 more display lines; full output is 13065 bytes on 55 raw line(s)] ````` ## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` @@ -4802,75 +4814,77 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* ````` - - - - -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] - + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - + H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… -struct DeltaBasis -inline const char* kAcksFile = ".ripwire_quality_acks" -DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) -struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) -inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) -std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) -inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -inline std::string qualityAcksPath( const std::string& root ) -inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at -inline std::string gitHeadSha( const std::string& root ) -inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) -inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) -inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) -int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) -inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) -inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] -inline std::string qualityBaselinePath( const std::string& root ) -… [52 more display lines; full output is 9064 bytes on 1 raw line(s)] +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 6 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [52 more display lines; full output is 9762 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` *The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* +**wall time: 1.01s** + ````` - - - - -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] - + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfSc … [line truncated: 14 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - + H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… -struct DeltaBasis -inline const char* kAcksFile = ".ripwire_quality_acks" -DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) -struct IdentityHealing -inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) -inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) -std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) -inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -struct QualityDeltaOutcome -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -inline std::string qualityAcksPath( const std::string& root ) -inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at -inline std::string gitHeadSha( const std::string& root ) -inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) -inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) -inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) -int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) -inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) -inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BARE NAME … [line truncated: 54 more bytes on this line] -inline std::string qualityBaselinePath( const std::string& root ) -… [53 more display lines; full output is 9230 bytes on 1 raw line(s)] +struct DeltaBasis +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 6 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which degraded to a BA … [line truncated: 62 more bytes on this line] +… [52 more display lines; full output is 9724 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --safe-delete=rankGraphTeleport` @@ -4878,15 +4892,15 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* ````` - - - - - - - - + + + + + + + + ````` @@ -4910,22 +4924,22 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* -**wall time: 1.35s** +**wall time: 1.57s** ````` - - + + - + - - - + + + + ````` @@ -4934,19 +4948,18 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* -**wall time: 1.32s** +**wall time: 1.53s** ````` - - + + - + ````` @@ -4960,7 +4973,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist - @@ -4977,7 +4990,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist - + @@ -4986,7 +4999,7 @@ ripwire: --safe-delete symbol not found: DoesNotExist -… [393 more display lines; full output is 42252 bytes on 1 raw line(s)] +… [415 more display lines; full output is 44441 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --no-ignore --top-k=3` @@ -4994,16 +5007,17 @@ ripwire: --safe-delete symbol not found: DoesNotExist *Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied).* ````` - + - - + + - + - + + @@ -5014,16 +5028,17 @@ ripwire: --safe-delete symbol not found: DoesNotExist *--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of.* ````` - + - - + + - + - + + @@ -5042,18 +5057,18 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o **exit code: 4** ````` - - + + - + const&, float) src/graph.h:5567 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 - #3 0x102e01a44 in main src/main.cpp:3975 - #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> const&, float) src/graph.h:7926 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:7967 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1634 + #3 0x102e01a44 in main src/main.cpp:3999 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> pw = biasPrior( g, p ); @@ -5081,7 +5096,7 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o std::vector r( N, 0.f ); std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); return { std::move( r ), run.iterationCount, run.hasConverged }; -}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 480 more bytes on this line] +}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 524 more bytes on this line] ````` ## `./build/ripwire . --run-trace="true"` @@ -5091,7 +5106,7 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o ````` - + ````` @@ -5099,12 +5114,12 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o *A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* -**exit code: 4** — **wall time: 2.43s** +**exit code: 4** — **wall time: 2.58s** ````` - + ````` @@ -5139,40 +5154,40 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* -**wall time: 3.98s** +**wall time: 4.01s** ````` - - + - - - - - + + + + + - - + + - - + + - - + + - - + + - - + + - -… [186 more display lines; full output is 15787 bytes on 1 raw line(s)] + +… [185 more display lines; full output is 15643 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --biggest-first --limit=8` @@ -5181,13 +5196,13 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - + + - - - - + + + + @@ -5199,11 +5214,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - + - - + + @@ -5217,60 +5232,60 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -… [21 more display lines; full output is 11209 bytes on 1 raw line(s)] + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11200 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --nonlocal-state --limit=8` *Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* -**wall time: 3.31s** +**wall time: 2.36s** ````` - - + - + - + @@ -5279,66 +5294,66 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + - + -… [41 more display lines; full output is 7593 bytes on 1 raw line(s)] +… [41 more display lines; full output is 7594 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --ensemble --limit=8` *The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* -**wall time: 2.62s** +**wall time: 2.10s** ````` - - + - + - + - + - + - + - + - + -… [35 more display lines; full output is 7334 bytes on 1 raw line(s)] +… [35 more display lines; full output is 7386 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --field-affinity` *The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* -**wall time: 1.78s** +**wall time: 1.31s** ````` - + @@ -5349,47 +5364,47 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + - - + - - - -… [509 more display lines; full output is 45771 bytes on 1 raw line(s)] + + + + +… [513 more display lines; full output is 46141 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --field-affinity=Symbol` *The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* -**wall time: 1.41s** +**wall time: 1.23s** ````` - - - - - - - + + + + + + + + - @@ -5411,49 +5426,49 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir ````` - - - - - - + + + + + + - + - + - + - - + + - + - + - + -… [13 more display lines; full output is 4383 bytes on 1 raw line(s)] +… [13 more display lines; full output is 4347 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --naming-calibration` *Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* -**wall time: 12.45s** +**wall time: 9.15s** ````` - - - + + + @@ -5464,60 +5479,60 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir

-

+

-

-

+

+

-

+

+

-

-… [119 more display lines; full output is 12978 bytes on 1 raw line(s)] +… [131 more display lines; full output is 14137 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lint --naming-locals` *The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* -**wall time: 3.89s** +**wall time: 2.18s** ````` - - - + + + - + - - + + - - + + - - + + - + - + - + - -… [640 more display lines; full output is 72432 bytes on 1 raw line(s)] + +… [639 more display lines; full output is 72669 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lint-catalog` @@ -5563,7 +5578,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* ````` -ripwire legend dictionary ripwire.dict/v1 dictv=c888cb4b5732610c entries=786 +ripwire legend dictionary ripwire.dict/v1 dictv=593769ab691623d8 entries=806 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -5593,24 +5608,24 @@ ripwire.impact/v1 : transitive blast radius of of=: reach s ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= outermost-first, spans -… [757 more display lines; full output is 78308 bytes on 787 raw line(s)] +… [777 more display lines; full output is 80949 bytes on 807 raw line(s)] ````` ## `./build/ripwire . --lint --lint-select=cache-` *Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* -**wall time: 3.27s** +**wall time: 1.72s** ````` - + - - + + - + vector<std::vector<double>> @@ -5633,37 +5648,37 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= g_pool[ g_readOf[i] ] g_pool[ g_readOf[i] ] m[ g_pool[i] ] -… [505 more display lines; full output is 61080 bytes on 1 raw line(s)] +… [533 more display lines; full output is 64511 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --lint --lint-ignore=naming-,cache-` *DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* -**wall time: 2.65s** +**wall time: 1.75s** ````` - - - + + - + - - + + - - + + - - - + + + @@ -5673,14 +5688,59 @@ ripwire.at/v1 : enclosing-definition chain at p=:l=: sym= innermost, chain= 20 delta < 0.02 && delta > -0.02 0.02 -… [729 more display lines; full output is 65896 bytes on 1 raw line(s)] +… [727 more display lines; full output is 66106 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=4` + +*A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped="1", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget.* + +**wall time: 2.47s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; + +````` + +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=40` + +*Pasting that findings_next=: the same rule under a 10x budget counts its true total — no count_capped=, no findings_next=.* + +**wall time: 1.93s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto done; +goto done; + ````` ## `./build/ripwire . --lint --lint-select=cach-` *An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* -**exit code: 1** — **wall time: 3.00s** +**exit code: 1** — **wall time: 1.77s** ````` (empty) @@ -5696,7 +5756,7 @@ ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-' *A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* -**exit code: 1** — **wall time: 2.92s** +**exit code: 1** — **wall time: 1.78s** ````` (empty) @@ -5712,7 +5772,7 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- *The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* -**wall time: 2.82s** +**wall time: 1.76s** ````` {"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] @@ -5745,13 +5805,13 @@ ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint- {"id":"atom-nested-ternary","shortDescription":{"text":"atom-nested-ternary"},"properties":{"builtin":true,"capped":false,"applicable":true}}, {"id":"atom-implicit-predicate","shortDescription":{"text":"atom-implicit-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, {"id":"atom-octal-literal","shortDescription":{"text":"atom-octal-literal"},"properties":{"builtin":true,"capped":false,"applicable":true}}, -… [10019 more display lines; full output is 1403667 bytes on 1 raw line(s)] +… [10455 more display lines; full output is 1464271 bytes on 1 raw line(s)] ````` Parsed summary of the same SARIF (past the display cut): ````` -sarif 2.1.0 rules= 39 results= 5005 +sarif 2.1.0 rules= 39 results= 5223 ````` ## `./build/ripwire . --lint --sarif --limit=5` @@ -5774,11 +5834,11 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* -**wall time: 4.45s** +**wall time: 2.84s** ````` -

@@ -5789,14 +5849,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* -**wall time: 10.09s** +**wall time: 6.63s** ````` - -

-

+ +

+

````` @@ -5804,14 +5864,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The range form: tree HEAD vs tree HEAD~3.* -**wall time: 9.73s** +**wall time: 6.50s** ````` - -

-

+ +

+

````` @@ -5819,23 +5879,23 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* -**wall time: 1.79s** +**wall time: 1.36s** ````` - + - + - + - + @@ -5846,54 +5906,54 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M - - + + - -… [726 more display lines; full output is 47798 bytes on 1 raw line(s)] + +… [773 more display lines; full output is 51080 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --cochange --cochange-recur=2` *Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect.* -**wall time: 1.44s** +**wall time: 1.27s** ````` - + - - - - + - - - - - + + + + + + + + + + - - + + + + - - - - - + + + + - - - ````` @@ -5901,7 +5961,7 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M *The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* -**wall time: 1.62s** +**wall time: 1.37s** ````` (empty) @@ -5910,14 +5970,14 @@ ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M Artifact written: ````` - 158888 /aux/map2.html + 159219 /aux/map2.html ````` ## `./build/ripwire . --index-out=/aux/ci_index` *CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* -**wall time: 5.03s** +**wall time: 3.57s** ````` (empty) @@ -5926,16 +5986,16 @@ Artifact written: stderr: ````` -ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (18649971 bytes, lean family) -ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (39975434 bytes, rich family) +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (23065349 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (48813356 bytes, rich family) ````` Artifact written: ````` - 18649971 /aux/ci_index.lean.ripwirecache - 39975434 /aux/ci_index.rich.ripwirecache - 58625405 total + 23065349 /aux/ci_index.lean.ripwirecache + 48813356 /aux/ci_index.rich.ripwirecache + 71878705 total ````` ## `./build/ripwire . --cache=/aux/ci_index.lean.ripwirecache --top-k=3` @@ -5943,16 +6003,17 @@ Artifact written: *Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* ````` - + - - + + - + - + + @@ -5963,16 +6024,17 @@ Artifact written: *Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* ````` - + - - + + - + - + + @@ -5989,7 +6051,7 @@ Artifact written: # whose template-argument list spans lines keeps its line break as \n instead of splitting the row. # line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): # the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. - 114971 /aux/pin_census.tsv + 122387 /aux/pin_census.tsv ````` ## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` @@ -6000,7 +6062,7 @@ Artifact written: ````` - + @@ -6022,7 +6084,7 @@ Artifact written: ````` - + @@ -6034,18 +6096,18 @@ Artifact written: *--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* -**exit code: 1** — **wall time: 2.78s** +**exit code: 1** — **wall time: 2.91s** ````` - - + - - + + - - + @@ -6081,31 +6143,31 @@ Everything below runs with `cwd` = the throwaway clone at `/dirty` (`gi *Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* -**wall time: 5.04s** +**wall time: 4.18s** ````` ripwire situational-awareness — 1 changed file(s), 13 symbols in them root: . -at: c7920353a+dirty - [1] blast radius: 190 symbols across 46 files transitively depend on these changes (showing 8 of 46 files — shown=8 total=46 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=46) - counts_floor=1 graph_ambiguous=10281 graph_unresolved=12878 graph_unindexed=237 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missin … [line truncated: 46 more bytes on this line] - src/mcpverbs.h (32 dependent symbols) +at: f1e5c2e76+dirty + [1] blast radius: 225 symbols across 51 files transitively depend on these changes (showing 8 of 51 files — shown=8 total=51 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=51) + counts_floor=1 graph_ambiguous=12498 graph_unresolved=12552 graph_unindexed=255 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missin … [line truncated: 46 more bytes on this line] + src/mcpverbs.h (33 dependent symbols) src/slice.h (30 dependent symbols) + src/quality.h (10 dependent symbols) + src/verbs_for.h (10 dependent symbols) + src/verbs_navigate.h (10 dependent symbols) + src/resolve.h (9 dependent symbols) src/serialize.h (9 dependent symbols) - src/quality.h (8 dependent symbols) - src/verbs_navigate.h (8 dependent symbols) src/main.cpp (7 dependent symbols) - src/verbs_quality.h (7 dependent symbols) - src/mergescout.h (6 dependent symbols) [2] tests to run (5) order=evidence: [changed] you edited it, [partner] named after a changed file, then hops asc (1 = direct); "(n): a, b" = n runner-less files sharing that evidence; a (run: …) is relative to root: test/verify_radix.cpp [hops=1] (run: bash test/greptiercheck.sh) test/adaptivecutshapefix/adaptive_cut_shape_test.cpp [hops=2] (run: bash test/adaptivecutshapecheck.sh) test/includeprecise_unit.cpp [hops=2] (run: bash test/includeprecisecheck.sh) test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) test/rustimport_unit.cpp [hops=4] (run: bash test/rustimportprecisecheck.sh) - script_gates_unmodelled=729 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) - [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="3939": - (none — 3939 commits were mined and none co-edited a file outside your diff) + script_gates_unmodelled=743 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="4174": + (none — 4174 commits were mined and none co-edited a file outside your diff) next: --test-gate ````` @@ -6113,41 +6175,41 @@ at: c7920353a+dirty *The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* -**exit code: 4** — **wall time: 5.17s** +**exit code: 4** — **wall time: 3.35s** ````` - - - - - - - + + + + + + + + - - - - + + + + - + - + - - ````` @@ -6155,11 +6217,11 @@ at: c7920353a+dirty *Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* -**exit code: 2** — **wall time: 22.67s** +**exit code: 2** — **wall time: 19.98s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. next=: the one pasteable follow-up. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. r surface=: the api-surface tier, new-symbol or contract-change. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -6188,7 +6250,7 @@ at: c7920353a+dirty -… [116 more display lines; full output is 15456 bytes on 1 raw line(s)] +… [127 more display lines; full output is 16474 bytes on 1 raw line(s)] ````` stderr: @@ -6202,11 +6264,11 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend.* -**exit code: 2** — **wall time: 8.55s** +**exit code: 2** — **wall time: 9.87s** ````` - - + @@ -6235,7 +6297,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap -… [116 more display lines; full output is 20270 bytes on 1 raw line(s)] +… [127 more display lines; full output is 21352 bytes on 1 raw line(s)] ````` stderr: @@ -6249,10 +6311,10 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* -**exit code: 2** — **wall time: 8.82s** +**exit code: 2** — **wall time: 9.92s** ````` -{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":134,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"c7920353a+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":145,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"f1e5c2e76+dirty","renames":58,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 211 more bytes on this line] {"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, {"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, @@ -6282,7 +6344,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap {"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, {"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, {"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, -… [113 more display lines; full output is 14067 bytes on 1 raw line(s)] +… [124 more display lines; full output is 15115 bytes on 1 raw line(s)] ````` stderr: @@ -6296,7 +6358,7 @@ ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: ap *NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* -**exit code: 1** — **wall time: 5.63s** +**exit code: 1** — **wall time: 6.41s** ````` (empty) @@ -6306,7 +6368,7 @@ stderr: ````` ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) -ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 224 re-derived, 9 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1328 ineligible (no cur … [line truncated: 198 more bytes on this line] +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 218 re-derived, 15 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1315 ineligible (no cu … [line truncated: 200 more bytes on this line] ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written ````` @@ -6314,7 +6376,7 @@ ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written *NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* -**wall time: 5.59s** +**wall time: 6.42s** ````` (empty) @@ -6324,7 +6386,7 @@ stderr: ````` ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) -ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 224 re-derived, 9 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1328 ineligible (no cur … [line truncated: 198 more bytes on this line] +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 218 re-derived, 15 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 57 left legacy (member set not found cloning here — a floor, not proof it is gone), 1315 ineligible (no cu … [line truncated: 200 more bytes on this line] ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already acked) → ./.ripwire_quality_acks ````` @@ -6332,11 +6394,11 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already *Re-run after the partial ack: acked=3, the rest still gate (exit 2).* -**exit code: 2** — **wall time: 11.33s** +**exit code: 2** — **wall time: 9.67s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. next=: the one pasteable follow-up. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -6365,7 +6427,7 @@ ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already -… [115 more display lines; full output is 15159 bytes on 1 raw line(s)] +… [126 more display lines; full output is 16177 bytes on 1 raw line(s)] ````` stderr: @@ -6395,11 +6457,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - @@ -6411,11 +6473,11 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire row identical.* -**wall time: 1.33s** +**wall time: 1.29s** ````` - @@ -6427,132 +6489,136 @@ ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire - - - + + + + + + - - + + - + - - - - -… [87 more display lines; full output is 8449 bytes on 1 raw line(s)] +… [88 more display lines; full output is 8517 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --map-diff --top-k=5` *The map re-ranked with a teleport toward the changed file (changed=1 here, not 0).* -**wall time: 2.21s** +**wall time: 1.97s** ````` - + - - + + - - - - - - - - - - + + + - - - - - - - - - -… [37 more display lines; full output is 4185 bytes on 1 raw line(s)] + + + + + + + + + + + + + + + + + + + + + ````` ## `./build/ripwire . --clones` *The duplicated helper the sandbox edit introduced shows up as a clone group.* -**wall time: 2.28s** +**wall time: 2.05s** ````` - + - + - + - + - + - + - + - + - + -… [307 more display lines; full output is 16180 bytes on 1 raw line(s)] +… [306 more display lines; full output is 16116 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=zz-orphan` *CHANGED: a ref with NO merge base with HEAD now reports v="unknown" ok="0" in its own bucket — the absence of an answer, never a claim it is merged. (The sandbox carries a deliberately parentless branch built with `git commit-tree`; a shallow CI clone puts every ref here.)* -**wall time: 1.82s** +**wall time: 1.41s** ````` - - + + ````` @@ -6567,11 +6633,11 @@ stderr: *CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* -**wall time: 2.57s** +**wall time: 2.03s** ````` - + ````` @@ -6586,11 +6652,11 @@ stderr: *The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* -**wall time: 3.73s** +**wall time: 3.49s** ````` -

@@ -6601,11 +6667,11 @@ stderr: *OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* -**wall time: 9.69s** +**wall time: 9.91s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. r surface=: the api-surface tier, new-symbol or contract-change. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -6634,7 +6700,7 @@ stderr: -… [118 more display lines; full output is 15426 bytes on 1 raw line(s)] +… [129 more display lines; full output is 16444 bytes on 1 raw line(s)] ````` stderr: @@ -6647,7 +6713,7 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* -**exit code: 1** — **wall time: 5.72s** +**exit code: 1** — **wall time: 7.51s** ````` (empty) @@ -6667,12 +6733,12 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* -**wall time: 2.56s** +**wall time: 3.35s** ````` - - + + @@ -6696,11 +6762,11 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr - - - - - + + + + + ````` @@ -6710,7 +6776,7 @@ ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=sr *Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 keep this branch-free — it sits inside the PageRank sort comparator c7920353a6d41f95f6ad61193358a10a3b255acf lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 keep this branch-free — it sits inside the PageRank sort comparator f1e5c2e76f0a8c29149cc56d865c6627d76b4c24 integration/train-26b ````` stderr: @@ -6735,7 +6801,7 @@ ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sort - + @@ -6787,7 +6853,7 @@ ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByS *The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* ````` -src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort c7920353a6d41f95f6ad61193358a10a3b255acf lane/lean-answers-068 +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort f1e5c2e76f0a8c29149cc56d865c6627d76b4c24 integration/train-26b ````` ## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` @@ -6795,9 +6861,9 @@ src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-08 chose the flat *Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* ````` - + - + - + @@ -6916,16 +6982,16 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* -**wall time: 3.09s** +**wall time: 3.26s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ````` stderr: @@ -6938,7 +7004,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* -**wall time: 1.47s** +**wall time: 1.79s** ````` {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] @@ -6954,19 +7020,19 @@ ripwire edit: applied atomically; receipt carries region, blob_sha (post-check s *Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* -**wall time: 3.37s** +**wall time: 4.29s** ````` {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":743,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ````` stderr: @@ -6979,7 +7045,7 @@ ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, *An unknown TARGET refuses and leaves every file byte-identical.* -**exit code: 1** — **wall time: 1.49s** +**exit code: 1** — **wall time: 1.78s** ````` (empty) @@ -6995,7 +7061,7 @@ ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found *A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* -**wall time: 1.47s** +**wall time: 1.87s** Input file: @@ -7013,21 +7079,21 @@ Input file: ````` ````` -{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 354 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false, … [line truncated: 345 more bytes on this line] ````` ## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` *The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* -**wall time: 3.02s** +**wall time: 3.80s** ````` -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 529 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":10278,"graph_unresolved":12878,"graph_unindexed":237,"counts_floor":true}}]} +"graph_ambiguous":12495,"graph_unresolved":12552,"graph_unindexed":255,"counts_floor":true}}]} ````` ## `./build/ripwire . --edit-plan=/aux/edit_plan.json` @@ -7050,11 +7116,11 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply *After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* -**exit code: 2** — **wall time: 9.05s** +**exit code: 2** — **wall time: 12.77s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. next=: the one pasteable follow-up. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r origin=new-symbol: a finding on NEW code (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -7082,8 +7148,8 @@ ripwire: --edit-plan requires exactly one of --dry-run or --apply - -… [112 more display lines; full output is 14523 bytes on 1 raw line(s)] + +… [123 more display lines; full output is 15541 bytes on 1 raw line(s)] ````` stderr: @@ -7097,7 +7163,7 @@ ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: du *REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* -**exit code: 1** — **wall time: 7.01s** +**exit code: 1** — **wall time: 7.42s** ````` (empty) @@ -7116,7 +7182,7 @@ ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against *The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* -**wall time: 10.01s** +**wall time: 10.88s** ````` (empty) @@ -7126,16 +7192,16 @@ stderr: ````` ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") -ripwire: wrote ./.ripwire_quality_baseline (snapshot of 23138 per-symbol rows, 23853 indexed symbols total) +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 24758 per-symbol rows, 25560 indexed symbols total) ````` Artifact written: ````` - 3876272 .ripwire_quality_baseline + 4195904 .ripwire_quality_baseline # ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit -producer 075ad01d2576bcb1b0bdac7efeb7b3b61d37df7c7fb9adbf3794b90643e96ba0 -head c7920353a6d41f95f6ad61193358a10a3b255acf +producer 8569518cb60b7c1a0c8a57175e169d7bfeab286033b8825969bdb1f593c3096e +head f1e5c2e76f0a8c29149cc56d865c6627d76b4c24 dirty 1 absorbed 2 ccx d19d7c13cbc7 2 @@ -7148,11 +7214,11 @@ ccx 7d8862be4e *Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* -**wall time: 11.55s** +**wall time: 9.26s** ````` - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). --> + @@ -7175,13 +7241,13 @@ ccx 7d8862be4e - - + + + + - - - -… [107 more display lines; full output is 13388 bytes on 1 raw line(s)] + +… [118 more display lines; full output is 14425 bytes on 1 raw line(s)] ````` @@ -7212,7 +7278,7 @@ ccx 7d8862be4e {"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] {"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] {"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] -{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 806 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] {"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] {"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] {"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] @@ -7233,7 +7299,7 @@ ccx 7d8862be4e The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: ````` -tools= 33 manifest_bytes= 46903 (~tokens at 4 bytes/token: 11725 ) +tools= 33 manifest_bytes= 46964 (~tokens at 4 bytes/token: 11741 ) batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] explore desc_bytes= 1322 schema_bytes= 774 required=['task'] edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] @@ -7258,9 +7324,9 @@ find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symb insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] owners desc_bytes= 425 schema_bytes= 625 required=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] -quality_delta desc_bytes= 772 schema_bytes= 193 required=[] path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] cochange desc_bytes= 388 schema_bytes= 476 required=['file'] analyze desc_bytes= 546 schema_bytes= 318 required=[] @@ -7276,7 +7342,7 @@ quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] ````` {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 955 more bytes on this line] {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELAT … [line truncated: 1092 more bytes on this line] -{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 806 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] {"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] {"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] {"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 1933 more bytes on this line] @@ -7288,7 +7354,7 @@ quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] The core manifest, summarised (name / description bytes / required args): ````` -tools= 8 manifest_bytes= 13921 (~tokens at 4 bytes/token: 3480 ) +tools= 8 manifest_bytes= 13982 (~tokens at 4 bytes/token: 3495 ) batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] explore desc_bytes= 1322 schema_bytes= 774 required=['task'] edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] @@ -7296,33 +7362,33 @@ impact desc_bytes= 821 schema_bytes= 666 required=['symb from_trace desc_bytes= 829 schema_bytes= 559 required=['trace'] uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] -quality_delta desc_bytes= 772 schema_bytes= 193 required=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] ````` ## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` *MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* -**wall time: 1.25s** +**wall time: 1.30s** ````` {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= total= capped= has_more= next_offset= offset= limit= (ca … [line truncated: 19100 more bytes on this line] -"_index":"[index: files=2484 symbols=23851 hash=dab62389]","_fresh":"ok"}} -{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"//' | grep -oE '/dev/null )" -printf '%s' "$WDEND" | grep -q ' remainder" \ - || ok "§B8.2 whereis (default listing twin): no past the end (the count is not a page remainder)" +if ! printf '%s' "$WDEND" | grep -q ' root" +elif printf '%s' "$WDEND" | grep -q ' remainder" +else + ok "§B8.2 whereis (default listing twin): no past the end (the count is not a page remainder)" +fi # the stray-content sibling: force the per-ref file listing (capped at 12) past its cap on its own branch. g checkout -q main From 2f19d917e66c242d6a53bf1732e022d32434f2b1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 18:53:13 -0400 Subject: [PATCH 218/229] docs(path): pathgaps.h counts five gap populations, not four, since the name population The contract comment and the PathGapKind table comment still said four after f1e5c2e7 added name. Comment-only; no output changes. Co-Authored-By: Claude Opus 5.5 --- src/pathgaps.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pathgaps.h b/src/pathgaps.h index d60aa1102..b0202474b 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -13,7 +13,7 @@ // A.process, and other/B.process, which calls the target, was invisible behind a plain "no directed call path". // // THE CONTRACT. pathSearchGaps walks the same directed cone the BFS walked (every symbol reachable from the from= -// definitions over resolved call edges) and counts those four populations inside it. When the count is non-zero the +// definitions over resolved call edges) and counts those five populations inside it. When the count is non-zero the // answer never says "no directed call path": it says the search was incomplete, gives the counts (searched=, gaps=), // and names the cone symbols that carry them ( rows, nearest to from= first, capped at kPathGapRows and // disclosed) with a next= that reads their bodies. When the count is zero the old wording stands, byte for byte. @@ -52,7 +52,7 @@ namespace rw inline constexpr std::size_t kPathGapRows = 3; // rows printed; gap_syms= counts them all, gap_syms_capped="1" says so -// The four gap populations, in the order gaps= lists them. A declarative table (CONTRIBUTING §3): the spelling lives once. +// The five gap populations, in the order gaps= lists them. A declarative table (CONTRIBUTING §3): the spelling lives once. enum class PathGapKind : std::uint8_t { Declined, Unresolved, Value, Through, Name }; inline constexpr std::size_t kPathGapKindCount = 5; inline constexpr std::array kPathGapKindNames = { "declined", "unresolved", "value", "through", "name" }; From 18224dcacc35e3002a5c7ff4a44f9f6055563981 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 18:53:43 -0400 Subject: [PATCH 219/229] docs(safe-delete): runSafeDelete's contract comment names the fourth risk= value, unmodelled count-floor added risk="unmodelled" (help, legend table, CHANGELOG and the change-check skill name it); the comment above runSafeDelete still listed three values. Comment-only, same line count; no output changes. Co-Authored-By: Claude Opus 5.5 --- src/verbs_navigate.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 7f6189b52..fc022d1f3 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -1030,8 +1030,8 @@ inline std::vector> safeDeleteCallSites( co // risk= NAMES what was found, never a go/no-go verdict: "none-found" (zero 1-hop callers AND zero use // sites of any role — an ABSENCE of evidence, never evidence of absence: dynamic dispatch, callbacks and // unindexed macros contribute no edge either, same as every call-graph surface here), "untested-radius" -// (callers/uses exist and NONE of the transitive blast radius is test-covered), or "uses-exist" (callers/ -// uses exist and at least part of the radius is tested). +// (callers/uses exist and NONE of the radius is test-covered), "uses-exist" (callers/uses exist and part of the +// radius is tested), or "unmodelled" (nothing found, but uses_floor=: a read/named kind those counts cannot see). std::optional runSafeDelete( const MainDispatch& d ) { using namespace rw; From 5301021ec80eaac6e42e9ddb3ed07ab35b4a9bca Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 19:10:07 -0400 Subject: [PATCH 220/229] test(path): (N7) a via=name namesake the search reached anyway is not a name gap (red arm) helper's B().process() lists only other/B.process; the unlisted namesake pkg/x/A.process is in the searched cone through run, so nothing behind that call went unsearched. The answer must keep the plain no-path reading. RED on 7608d1f6 (gaps="name:1", row helper). Co-Authored-By: Claude Opus 5.5 --- test/pathgapcheck.sh | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index ade44231e..4ef6f6734 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -28,6 +28,8 @@ # (N4) a found path (reachable="1") through a cone with gaps carries no gap clause. # (N5) a function passed as a value that the search ALSO reaches by a call is not a gap. # (N6) a via="name" call whose namesakes are a DIFFERENT language (Ruby) or a different KIND (a Python variable) is not a gap. +# (N7) a via="name" call whose unlisted namesake the search REACHED anyway (another cone symbol calls it) is not a gap: +# helper's B().process() lists only other/B.process, but pkg/x/A.process is in the cone through run, so it was searched. # (N3) above doubles as the name-gap near miss: run's obj.process() is via="name" too, but it has an edge to EVERY # same-language definition of process, so nothing was left unsearched. # (M) MCP path_between carries the same searched/gaps/gap_syms/rows/next as the CLI (P1-P3, P7), and the same no-gap hint shape. @@ -48,7 +50,8 @@ command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } # ── fixtures ──────────────────────────────────────────────────────────────────────────────────────────────────────── mkdir -p "$TMP/decl/core" "$TMP/decl/linux" "$TMP/decl/darwin" "$TMP/val" "$TMP/thr" "$TMP/unr" "$TMP/clean" \ "$TMP/out/core" "$TMP/out/linux" "$TMP/out/darwin" "$TMP/amb" "$TMP/many/core" "$TMP/many/linux" "$TMP/many/darwin" \ - "$TMP/found" "$TMP/valin" "$TMP/lpy/pkg/x" "$TMP/lpy/other" "$TMP/lpyx/pkg/x" "$TMP/lpyx/other" + "$TMP/found" "$TMP/valin" "$TMP/lpy/pkg/x" "$TMP/lpy/other" "$TMP/lpyx/pkg/x" "$TMP/lpyx/other" \ + "$TMP/lpyin/pkg/x" "$TMP/lpyin/other" cat > "$TMP/decl/core/machine.c" <<'EOF' void Machine_scan( void ); void Machine_run( void ) { Machine_scan(); } @@ -205,6 +208,24 @@ EOF cat > "$TMP/lpyx/other/v.py" <<'EOF' process = 3 EOF +# N7: every namesake is in the cone. run reaches A.process (by name) and helper; helper's B().process() lists B.process +# only, and its unlisted namesake A.process is searched through run. Nothing reaches target. +cp "$TMP/lpy/pkg/x/a.py" "$TMP/lpyin/pkg/x/" +cat > "$TMP/lpyin/pkg/x/c.py" <<'EOF' +from other.b import helper +def run(obj): + obj.process() + helper() +EOF +cat > "$TMP/lpyin/other/b.py" <<'EOF' +class B: + def process(self): + return 2 +def helper(): + B().process() +def target(): + return 3 +EOF # One --path answer, run unpiped so rc is ripwire's; the head and the rest are read off the saved file. path(){ # $1 dir, $2 FROM,TO, $3 out file @@ -317,6 +338,15 @@ if grep -q ']*n="process" p="pkg/x/a.py:2" via="name"' "$f"; then else no "(N6) premise: run's obj.process() is not via=name to A.process here: $( grep -o ' "$f" 2>&1; fh_rc=$? +f2="$TMP/lpyinrun.xml"; "$BIN" "$TMP/lpyin" --no-cache --callees=run > "$f2" 2>&1; fr_rc=$? +if [ "$fh_rc" -eq 0 ] && [ "$fr_rc" -eq 0 ] \ + && grep -q ']*n="process" p="other/b.py:2" via="name"' "$f" && ! grep -q 'p="pkg/x/a.py:2"' "$f" \ + && grep -q ']*n="process" p="pkg/x/a.py:2"' "$f2"; then + cleanArm "(N7) via=name, the unlisted namesake is in the searched cone" "$TMP/lpyin" "run,target" +else + no "(N7) premise: helper lists only other/B.process by name and run reaches pkg/x/A.process (rc=$fh_rc/$fr_rc): $( grep -o ' Date: Thu, 8 Oct 2026 19:13:02 -0400 Subject: [PATCH 221/229] fix(path): a via=name namesake the search reached is not a name gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit countUnlistedNamesakes counted a name gap whenever the tree held more same-language, same-kind definitions of a by-name callee than the caller had edges to — also when the unlisted namesake was inside the searched cone (another cone symbol reaches it). That namesake WAS searched, so the answer said "the search is incomplete" and the legend's "that namesake was never searched" was false. A name gap now needs a namesake outside the cone; every edge target of a cone symbol is in the cone, so that also means the call has no edge to it. Legend text unchanged (now true as written); CHANGELOG path-gap entry says "never reached". Arm (N7) GREEN; P7, N3, N6 and both MCP postures unchanged. Answer-changing: --path / path_between reachable=0 answers whose name gaps all had namesakes inside the cone. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- src/pathgaps.h | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aae4dbd93..f4e190411 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -874,8 +874,8 @@ says the search was incomplete: `searched=` (the symbols it reached), `gaps="dec `next=` that expands their bodies. A `` row is where the search could not see, never a hop. Ambiguous calls are not counted (every candidate has an edge the search follows), nor calls to names defined nowhere in the tree. A call bound by name alone (`via="name"`) counts as a `name` gap when the tree defines that name, same language and kind, somewhere the -call has no edge to: the search follows only the candidates such a call lists, so a namesake in another directory is -never searched and may be the missing hop (`run(obj)` calling `obj.process()`, bound to the same-directory +search never reached (a namesake another searched symbol reaches was searched, and is not counted): the search follows +only the candidates such a call lists, so a namesake in another directory is never searched and may be the missing hop (`run(obj)` calling `obj.process()`, bound to the same-directory `A.process`, while `other/B.process` calls the target). When the search met no such call the answer is unchanged, byte for byte. The legend dictionary gains three entries. A floor: the `next=` names a gap row by file and name, so where one file holds two definitions of that name it serves both. Gate: diff --git a/src/pathgaps.h b/src/pathgaps.h index b0202474b..9b66f209c 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -24,7 +24,7 @@ // a FOUND path a guess, which prov="split" / amb= already disclose). Calls to names with no in-repo definition at all // (Undefined) are not counted either: the graph keeps no per-caller tally of them, and nearly all are library calls. // Both are floors the legend sentence states. A via="name" call whose every same-language, same-kind definition of the -// name already has an edge from the caller is not a gap either: nothing it could bind to was left unsearched. +// name is in the searched cone (its own edge or another cone symbol's) is not a gap either: nothing it could bind to was unsearched. // // ONE analysis and ONE emitter for both transports (verbs_navigate.h runPath, mcpverbs.h pathText): the two used to // carry their own copies of the hint and must not drift on this clause. @@ -77,8 +77,10 @@ struct PathSearchGaps }; // NAME gaps: per cone symbol, the callee names it reaches only by name (an edge whose Graph::outNameOnly bit is set) for -// which the tree holds a same-language, same-kind DEFINITION the caller has no edge to — a namesake the search never -// walked. Counted once per such name per caller. Only the names behind the cone's name-only edges are looked up. +// which the tree holds a same-language, same-kind DEFINITION outside the cone — a namesake the search never walked. A +// namesake inside the cone was searched (another cone symbol reaches it), and every edge target of a cone symbol is in +// the cone, so "outside the cone" also means the call has no edge to it. Counted once per such name per caller. Only the +// names behind the cone's name-only edges are looked up. inline void countUnlistedNamesakes( const IngestResult& ing, const Graph& g, std::span cone, std::vector& per ) { EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits one per flattened edge" ); @@ -123,41 +125,47 @@ inline void countUnlistedNamesakes( const IngestResult& ing, const Graph& g, std } std::ranges::sort( names, sortutil::svLess ); names.erase( std::ranges::unique( names ).begin(), names.end() ); - std::vector defs; // the tree's definitions of exactly those names - for( const Symbol& s : ing.symbols ) + std::vector searched( ing.symbols.size(), 0 ); // the cone, by symbol id + for( const NodeId u : cone ) + { + if( u < searched.size() ) + { + searched[u] = 1; + } + } + std::vector unsearched; // the tree's definitions of exactly those names that the search never reached + for( std::size_t i = 0; i < ing.symbols.size(); ++i ) { - if( isDefinitionNotDeclaration( s ) && std::ranges::binary_search( names, std::string_view( s.name ), sortutil::svLess ) ) + const Symbol& s = ing.symbols[i]; + if( searched[i] == 0 && isDefinitionNotDeclaration( s ) && std::ranges::binary_search( names, std::string_view( s.name ), sortutil::svLess ) ) { - defs.push_back( Key{ s.lang, s.kind, s.name } ); + unsearched.push_back( Key{ s.lang, s.kind, s.name } ); } } - std::ranges::sort( defs, keyLess ); + if( unsearched.empty() ) + { + return; + } + std::ranges::sort( unsearched, keyLess ); for( const NodeId u : cone ) { if( !hasHedge( u ) ) { continue; } - std::vector listed, hedged; // every target u has an edge to; the targets it reached by name alone + std::vector hedged; // the targets u reached by name alone for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) { - if( !isSym( k ) ) - { - continue; - } - listed.push_back( keyOf( g.outTargets[k] ) ); - if( g.outNameOnly[k] != 0 ) + if( isSym( k ) && g.outNameOnly[k] != 0 ) { hedged.push_back( keyOf( g.outTargets[k] ) ); } } - std::ranges::sort( listed, keyLess ); std::ranges::sort( hedged, keyLess ); hedged.erase( std::ranges::unique( hedged, keyEq ).begin(), hedged.end() ); for( const Key& key : hedged ) { - const bool unlisted = std::ranges::equal_range( defs, key, keyLess ).size() > std::ranges::equal_range( listed, key, keyLess ).size(); - per[u][ std::size_t( PathGapKind::Name ) ] += unlisted ? 1u : 0u; + per[u][ std::size_t( PathGapKind::Name ) ] += std::ranges::binary_search( unsearched, key, keyLess ) ? 1u : 0u; } } } From 6f28768215d12d81137dc310ba3fc9bcb8a54f58 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 8 Oct 2026 23:17:39 -0400 Subject: [PATCH 222/229] test(loopconservationcheck): inherited merge tombstones; same window under every CI event Two defects in the gate, found when the train-26b pull_request run went red while the workflow_dispatch run at the same head was green. 1. A merge judged a tombstone it INHERITED as one it newly declared. A lane that added a loop entry and retired it inside its own life leaves `# retired: NAME`; every later merge brings that line in from the parent carrying it. Against the other parent (no tombstone, NAME never in its loop) the per-parent rule saw "tombstone matching no vanish". Now a tombstone another parent already carries is not judged stale against a parent whose loop never had NAME, provided NAME is absent from the child's loop too. A tombstone no parent carried (arm G) and a merge keeping NAME in its loop under an inherited tombstone (arm H) stay red; the vanish side is unchanged; ordinary commits are judged exactly as before. 2. The window stopped at the first upstream candidate whose merge-base was REF itself. A CI checkout of a branch tracks its own pushed copy, so the dispatch run fell back to the tip's first parent and judged zero loop-touching commits, while the pull_request run (detached, no upstream) fell through to origin/main and judged the whole branch. A candidate that already contains REF is now skipped instead of final (arm I); on main every candidate contains REF, so the first-parent fallback is unchanged there. Arms F, F2, G, H, I added; the header's arm list now matches the code. Co-Authored-By: Claude Opus 5.5 --- test/loopconservationcheck.sh | 120 +++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/test/loopconservationcheck.sh b/test/loopconservationcheck.sh index 1b78f260f..3b3858edb 100755 --- a/test/loopconservationcheck.sh +++ b/test/loopconservationcheck.sh @@ -23,7 +23,7 @@ # # THE CHECK (arm A). Walk EVERY commit in BASE..REF that touched test/regression.sh, and compare each # one against EACH of its OWN parents — one parent for an ordinary commit, two or more for a merge. -# The merge case is not lost, it is simply the len(parents)>1 case of the same loop; arm (C) proves it +# The merge case is not lost, it is simply the len(parents)>1 case of the same loop; arm (E) proves it # still fires. For a commit C with parent P: # # vanished = loop(P) - loop(C) - tombstones_new(C) @@ -31,11 +31,14 @@ # Any name present in a parent's loop, absent from the child's, and not declared retired IN THAT SAME # COMMIT is named and fails. # -# THE WINDOW. BASE is the merge-base of REF with its upstream tracking ref (falling back to -# origin/main, then origin/HEAD). If no upstream resolves, or the merge-base IS REF — the ordinary -# state on a freshly-pushed main, where the window would otherwise be empty and the gate would be -# inert all over again — BASE falls back to REF's first parent, so the newest commit is always -# examined. A root commit with no parents at all passes trivially and says so. +# THE WINDOW. BASE is the merge-base of REF with the first of: its upstream tracking ref, origin/main, +# origin/HEAD that is NOT already at or ahead of REF. A candidate whose merge-base IS REF is skipped, not +# final: a CI checkout of a branch tracks its own pushed copy, and stopping there judged only the tip +# (train 26b, 2026-10-08: workflow_dispatch green over 0 loop-touching commits while pull_request, detached +# at refs/pull/N/merge with no upstream, fell through to origin/main and judged 25 — arm I). Only when +# every candidate contains REF — the ordinary state on a freshly-pushed main, where the window would +# otherwise be empty and the gate inert all over again — does BASE fall back to REF's first parent, so +# the newest commit is always examined. A root commit with no parents at all passes trivially and says so. # # THE TOMBSTONE CONVENTION: a deliberate removal is not a violation, but it must be DECLARED, not merely # absent. In the SAME commit that drops NAME from the loop, add a standalone comment line anywhere in @@ -47,15 +50,24 @@ # vanish in that commit (either NAME is still in the loop, or NAME was never in the parent's loop) — a # tombstone nobody needed is itself worth noticing. Only NEWLY added tombstones are judged: a tombstone # is permanent once written, so judging the whole accumulated set at every commit would fire forever -# after the first legitimate retirement. +# after the first legitimate retirement. In a merge, a tombstone ANOTHER parent already carries is that +# side's retirement arriving, not a new declaration: against a parent whose loop never had NAME (and with +# NAME absent from the child's loop too) it is not judged stale (arms F/F2 green; the same merge with no +# parent carrying it, arm G, and a merge that keeps NAME in its loop under an inherited tombstone, arm H, +# stay red). The vanish side is unchanged: still per parent, against tombstones that parent lacks. # -# THE MUTATION CONTROLS (arms B, C, D) — this gate spent months green while inert, so it now proves on +# THE MUTATION CONTROLS (arms B..I) — this gate spent months green while inert, so it now proves on # every run that its walker can still go red. Each arm builds a throwaway git repository under a temp # directory (NOT this one: no object, ref, index or working-tree state of the repo under test is -# touched) with a known-bad history and requires arm A's own walker to name the violation: -# (B) single-parent silent drop → must be RED (the failure the old gate missed entirely) -# (C) two-parent merge drop → must be RED (the failure the old gate did catch — preserved) -# (D) the same drop WITH a tombstone → must be GREEN, and a tombstone matching no vanish → RED +# touched) with a known history and requires arm A's own walker to name the violation (or stay green): +# (B) single-parent silent drop → must be RED (the failure the old gate missed entirely) +# (C) the same drop WITH a tombstone → must be GREEN +# (D) a tombstone matching no vanish → must be RED +# (E) two-parent merge drop → must be RED (the failure the old gate did catch — preserved) +# (F, F2) merge inheriting a tombstone from either parent, other parent never had NAME → must be GREEN +# (G) the same merge, tombstone carried by NO parent → must be RED +# (H) merge keeping NAME in its loop under a tombstone one parent carries → must be RED +# (I) window derivation on a branch tracking its own pushed copy reaches origin/main → RED on a drop # # WHY docs/EVALS.md's gate-count pins do NOT get this same treatment (deliberately, not an oversight): # manifestcheck.sh already ties every "N gate scripts" / "loop ... names N" claim in EVALS.md to the @@ -151,20 +163,28 @@ def walk( repo, base, ref, path = "test/regression.sh" ): skipped.append( "%s (child has no `for _g in ...; do` loop)" % child[ :12 ] ) continue childTombs = tombstones( childText ) - for par in parents_of( repo, child ): + parents = parents_of( repo, child ) + tombsOf = { par: tombstones( loopAt( par ) ) for par in parents } + for par in parents: parText = loopAt( par ) parLoop = loop_names( parText ) if parLoop is None: skipped.append( "%s^%s (parent pre-dates the loop file)" % ( child[ :12 ], par[ :8 ] ) ) continue pairs += 1 - newTombs = childTombs - tombstones( parText ) + newTombs = childTombs - tombsOf[ par ] vanished = parLoop - childLoop silent = sorted( vanished - newTombs ) if silent: violations.append( "%s (parent %s): %d entr%s vanished from the loop with no `# retired:` tombstone: %s" % ( child[ :12 ], par[ :12 ], len( silent ), "y" if len( silent ) == 1 else "ies", ", ".join( silent ) ) ) - unneeded = sorted( newTombs - vanished ) + # A merge's tombstone that ANOTHER parent already carries is that side's retirement arriving, not + # a declaration made here — exempt it, but only when NAME is in neither this parent's loop nor + # the child's (it cannot be a vanish against this parent, so there is nothing for it to match). + # A NAME still in the child's loop stays red (arm H). For an ordinary commit `inherited` is + # empty, so the rule is exactly the single-parent one. + inherited = set().union( *( tombsOf[ q ] for q in parents if q != par ) ) + unneeded = sorted( t for t in newTombs - vanished if t in childLoop or t not in inherited ) if unneeded: stale.append( "%s (parent %s): tombstone(s) matching no vanish in that commit: %s" % ( child[ :12 ], par[ :12 ], ", ".join( unneeded ) ) ) @@ -184,8 +204,10 @@ def derive_base( repo, ref ): mb = mb.strip() if mb != refSha: return mb, "merge-base with %s" % upstream - # REF is already contained in its upstream: the window would be empty and this gate inert. - break + # REF is already contained in this candidate (a branch tracking its own pushed copy, as every CI + # checkout of a branch does): the window would be empty. Try the next candidate — origin/main + # gives the same window the pull_request event judges (arm I) — not the first-parent fallback. + continue if firstParent: return firstParent, "REF's first parent (no upstream ahead of REF — window would otherwise be empty)" return None, "REF is a root commit" @@ -290,6 +312,70 @@ def buildMerge( repo ): return a, m arm( "(E) mutation control, MERGE drop —", buildMerge, "rightcheck" ) +# ── (F) a merge that INHERITS a tombstone from one parent, the other parent never having had NAME ────── +# The train-26b shape (red on PR #383, 2026-10-08): a lane added spikecheck and retired it inside its own +# life, leaving `# retired: spikecheck`; every later merge brings that tombstone in from the parent that +# carries it. Judged against the OTHER parent (no tombstone, no spikecheck) it used to look like a +# tombstone newly declared for a vanish that never happened. It is the other side's retirement arriving, +# not a new declaration — GREEN, in both parent orders (the train merges lanes as the second parent; the +# GitHub PR merge commit puts main first and the train second). +def buildInheritedTomb( carrierFirst ): + def build( repo ): + a = commitLoop( repo, BASE_NAMES, [] ) + l1 = commitLoop( repo, BASE_NAMES + [ "spikecheck" ], [ a ] ) + l2 = commitLoop( repo, BASE_NAMES, [ l1 ], tombs = [ "spikecheck" ] ) # retired inside the lane + r = commitLoop( repo, BASE_NAMES + [ "othercheck" ], [ a ] ) # never had spikecheck + m = commitLoop( repo, BASE_NAMES + [ "othercheck" ], [ l2, r ] if carrierFirst else [ r, l2 ], + tombs = [ "spikecheck" ] ) + return a, m + return build +arm( "(F) merge inheriting a tombstone, carrier = 2nd parent —", buildInheritedTomb( False ), None ) +arm( "(F2) merge inheriting a tombstone, carrier = 1st parent —", buildInheritedTomb( True ), None ) + +# ── (G) the same merge where NO parent carried the tombstone — the merge itself declared it: RED ─────── +def buildMergeNewTomb( repo ): + a = commitLoop( repo, BASE_NAMES, [] ) + l = commitLoop( repo, BASE_NAMES + [ "leftcheck" ], [ a ] ) + r = commitLoop( repo, BASE_NAMES + [ "othercheck" ], [ a ] ) + m = commitLoop( repo, BASE_NAMES + [ "leftcheck", "othercheck" ], [ r, l ], tombs = [ "spikecheck" ] ) + return a, m +arm( "(G) mutation control, MERGE declares a tombstone no parent carried —", buildMergeNewTomb, "spikecheck" ) + +# ── (H) a merge that keeps NAME in the loop while one parent carries its tombstone: RED ─────────────── +# One side retired spikecheck, the other side still runs it, and the resolution kept it in the loop under +# the inherited tombstone. Judged against the side without the tombstone that is a tombstone whose NAME +# is still in the loop — the exception (F) makes is only for a NAME absent from both this parent's loop +# and the child's, so this stays red. +def buildMergeKeepsRetired( repo ): + a = commitLoop( repo, BASE_NAMES + [ "spikecheck" ], [] ) + l = commitLoop( repo, BASE_NAMES, [ a ], tombs = [ "spikecheck" ] ) + r = commitLoop( repo, BASE_NAMES + [ "spikecheck", "othercheck" ], [ a ] ) + m = commitLoop( repo, BASE_NAMES + [ "spikecheck", "othercheck" ], [ r, l ], tombs = [ "spikecheck" ] ) + return a, m +arm( "(H) mutation control, MERGE keeps a NAME one parent retired —", buildMergeKeepsRetired, "spikecheck" ) + +# ── (I) the WINDOW: a branch whose upstream is its own pushed copy still reaches back to origin/main ─── +# A CI checkout of a branch (workflow_dispatch, push) tracks origin/ == REF, so the merge-base with +# that upstream IS REF. Stopping there and falling back to REF's first parent judged ONE commit — the +# dispatch run on train 26b was green over a window of zero loop-touching commits while the pull_request +# run (detached at refs/pull/N/merge, no upstream → origin/main) judged all 25. The derived window must +# be the same under both events: an earlier silent drop on the branch is named. +def buildTrackedBranch( repo ): + a = commitLoop( repo, BASE_NAMES + [ "victimcheck" ], [] ) + b = commitLoop( repo, BASE_NAMES, [ a ] ) # silent drop, one commit below the tip + c = commitLoop( repo, BASE_NAMES + [ "newcheck" ], [ b ] ) # harmless tip that touches the loop + for args in ( [ "update-ref", "refs/remotes/origin/main", a ], + [ "update-ref", "refs/remotes/origin/feature", c ], + [ "update-ref", "refs/heads/feature", c ], + [ "config", "remote.origin.url", "file:///nonexistent.invalid" ], + [ "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*" ], + [ "config", "branch.feature.remote", "origin" ], + [ "config", "branch.feature.merge", "refs/heads/feature" ] ): + git( repo, args ) + b_, _why = derive_base( repo, "feature" ) + return ( b_ or c ), "feature" +arm( "(I) mutation control, WINDOW on a branch tracking its own pushed copy —", buildTrackedBranch, "victimcheck" ) + sys.exit( bad ) PY From 8c086c626401f1bc1d4eccc4c29b0e84c25d116f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 04:52:04 -0400 Subject: [PATCH 223/229] coderabbit: exclude gate fixture trees (test/*fix/**) from review Train PR #383 carried 388 files, 211 of them under test/*fix/ (inputs that gates parse), and CodeRabbit skipped the whole review at its 300-file limit. Fixture trees join the existing exclusions of vendored, captured and pinned files; code, gates and docs are still reviewed. Co-Authored-By: Claude Opus 5.5 --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 2cbd9de56..68aa9459f 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -5,6 +5,8 @@ # - No re-review on every push: each fix commit used to start a new pass over a new diff. # - No automatic reply inside review threads: every in-thread answer used to wake another pass. # - Vendored parsers and generated/pinned files are excluded; a review of them cannot change them. +# - Gate fixture trees (test/*fix/) are excluded: they are inputs, and a train carrying many of them +# went over the 300-file review limit (the whole review was skipped). # - No docstring-coverage pre-merge check: the house style documents contracts where they bite, not every symbol. # CodeRabbit reads this file from the PR's head branch, so a fork PR picks it up once it merges main. reviews: @@ -17,6 +19,7 @@ reviews: - "!docs/captures/**" - "!test/*.hash" - "!test/printf_parity.manifest" + - "!test/*fix/**" # gate fixture trees (inputs a gate parses); keeps a train PR under the 300-file review limit path_instructions: - path: "src/**" instructions: >- From 8a0e089658cfdacc3db70d9f79667921b48d6a9c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 05:17:41 -0400 Subject: [PATCH 224/229] graphlegend: no double hyphen in the --impact via="name" clause (CR 5468003465, graphlegend.h:889) CodeRabbit (Major): kViaNameImpactClause said "An --impact row", and viaNameLegend( present, impact = true ) is spliced inside the --impact legend's XML comment on the CLI and on MCP. A --legend=full answer with a shown via="name" row was therefore not well-formed XML ("Comment must not contain '--'"); the compact default never carries the clause. The clause now says "An impact row". test/xmlwellformed.sh gains a premise (the answer really carries a via="name" row and the clause) and three xmllint arms: CLI, CLI --format=columnar, and the MCP impact twin, each under legend=full on test/receiverevidencefix/js lib/reply.js:send. Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 4 +++- test/xmlwellformed.sh | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index d3de35287..ce8475cf6 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -885,8 +885,10 @@ inline constexpr const char* kViaNameLegend = "an import, the class's bases) proves the receiver is that class; every by-name candidate in reach is listed (same file and " "directory, else a lone definition), and any one of them, or none (a builtin or an outside object), may be the real target. " "It does NOT mean the edge is false, and a row without it is evidence-bound, not type-checked. "; +// G4: no double hyphen — viaNameLegend( …, impact = true ) lands inside the --impact legend's XML comment on the CLI and MCP +// (CodeRabbit 5468003465: "An --impact row" made a --legend=full answer with a via="name" row invalid XML; test/xmlwellformed.sh). inline constexpr const char* kViaNameImpactClause = - "An --impact row reached only through such an edge carries it too; one that some all-proven path reaches stays plain. "; + "An impact row reached only through such an edge carries it too; one that some all-proven path reaches stays plain. "; inline constexpr const char* kViaNameColumnarClause = "format=columnar carries it as the column (1 = via=\"name\"). "; inline std::string viaNameLegend( bool present, bool impact = false, bool columnar = false ) { diff --git a/test/xmlwellformed.sh b/test/xmlwellformed.sh index 80549e5a9..ff77af983 100755 --- a/test/xmlwellformed.sh +++ b/test/xmlwellformed.sh @@ -14,6 +14,8 @@ # --arch --baseline arch layering with baseline mode # --around=SYM ego-graph pack, walked over a SYMBOL SAMPLE (see §B4b below) # --edit-check=SYM contract-vs-HEAD bundle (CA4 §B15: this gate did not exercise it at all) +# --impact=SYM --legend=full with a via="name" row: the present-only FE-B clause inside the legend comment +# (CLI, columnar and the MCP twin — CodeRabbit 5468003465) # + the additive-flag shapes (--for --detail/--with-graph, --pack-task, --lego, --exemplar, --query # --adaptive, --expand+--outline+--pack-signatures together, --max-tokens): a verb's WIDEST form is # where a second top-level element appears, and a bare invocation cannot see it (trap #7). @@ -334,6 +336,34 @@ case "$MCPTXT" in esac fi # end of the python3-dependent #60 section +# ── FE-B via="name" under --legend=full: --impact's present-only clause (CodeRabbit 5468003465) ────────── +# viaNameLegend( present, impact = true ) is spliced INSIDE the --impact legend comment, on the CLI and on MCP, and +# rides only when a shown row is via="name" and the posture is full — the compact default never carries it, which +# is why no arm above could see "An --impact row" put a double hyphen inside a comment. Premise first: this root's +# answer really carries a via="name" row AND the clause, so the well-formedness arms below are not vacuous. +RE="$ROOT/test/receiverevidencefix/js" +IMPF="$( "$BIN" "$RE" --no-cache --impact=lib/reply.js:send --legend=full 2>/dev/null )" +if printf '%s' "$IMPF" | grep -q ' via="name"' && printf '%s' "$IMPF" | grep -q 'impact row reached only through such an edge'; then + ok "FE-B premise: --impact=lib/reply.js:send --legend=full carries a via=\"name\" row and the impact clause" +else + no "FE-B premise: no via=\"name\" row or no impact clause in --impact=lib/reply.js:send --legend=full — the arms below prove nothing" +fi +check_xml "FE-B --impact with a via=\"name\" row, --legend=full" "$RE" --impact=lib/reply.js:send --legend=full +check_xml "FE-B --impact with a via=\"name\" row, --legend=full --format=columnar" "$RE" --impact=lib/reply.js:send --legend=full --format=columnar +if ! command -v python3 >/dev/null 2>&1; then + printf ' SKIP FE-B MCP impact twin — python3 is not on PATH (prerequisite, not a failure)\n' +else + MIMP="$( mcp_text impact "{\"path\":\"$RE\",\"symbol\":\"lib/reply.js:send\",\"legend\":\"full\"}" )" + case "$MIMP" in + __ERROR__*|"") no "FE-B MCP impact did not answer: ${MIMP:-empty}" ;; + *'impact row reached only through such an edge'*) + printf '%s' "$MIMP" | xmllint --noout - 2>/dev/null \ + && ok "FE-B MCP impact (legend=full, a via=\"name\" row) is well-formed" \ + || { no "FE-B MCP impact (legend=full, a via=\"name\" row) rejected by xmllint"; printf '%s' "$MIMP" | xmllint --noout - 2>&1 | head -3; } ;; + *) no "FE-B MCP impact premise: legend=full answer carries no impact clause — the arm proves nothing" ;; + esac +fi + if [ "$fail" -eq 0 ]; then echo "ALL PASS" exit 0 From b7aead11b057f471fa53b9f0a2f9d363fe5d6f96 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 05:18:47 -0400 Subject: [PATCH 225/229] mcp: read whereis `listing` before the shared shape gate (CR 5468003465, mcp.h:2319) CodeRabbit (Minor): `listing` was read with strArg inside the whereis arm, after the shared shapeRefusal gate had already run. A non-string value (listing:5, listing:["all"]) decoded to "", its shape refusal was written to an accumulator nothing read any more, and whereisListingFromArg then refused with "got ''" - the caller was told it sent an empty value. `listing` is now read beside `kind` and the other string arguments, so a wrong-shaped value refuses through the shared gate and echoes what was sent; the arm passes the decoded string. A verb that does not declare `listing` still refuses it as an unknown field first (unchanged order). test/crossrefcheck.sh LEAN (L10b): listing 5 / ["all"] / {"v":1} / true refuse -32602 echoing the value; "bogus" still echoes itself. Co-Authored-By: Claude Opus 5.5 --- src/mcp.h | 6 +++++- test/crossrefcheck.sh | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/mcp.h b/src/mcp.h index 2407a0c72..bd1fac0c8 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1494,6 +1494,10 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo const std::string text = strArg( "text" ); // insert_before/after const std::string handle = strArg( "handle" ); // T4 fetch_body const std::string kind = strArg( "kind" ); // exemplar kind token; whereis/stray_content/flags/doc_drift name filter + // whereis: defs|refs|all. Read HERE with the other strings, before the shared shape gate below: read inside the + // whereis arm, a non-string `listing:5` decoded to "" after that gate had run, its shape refusal was dropped, and the + // closed-set refusal then told the caller it sent '' (CodeRabbit 5468003465; test/crossrefcheck.sh LEAN (L10b)). + const std::string listingArg = strArg( "listing" ); const std::string from = strArg( "from" ); // path verb: source symbol const std::string to = strArg( "to" ); // path verb: destination symbol const std::string trace = strArg( "trace" ); // L4 from_trace: the raw trace TEXT @@ -2314,7 +2318,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // the CLI), not a hits="0" answer about the literal string. `seedFault` distinguishes it // from the non-git degrade, which is the other way whereisText returns "". crossref::WhereisListing listing = crossref::WhereisListing::ShorterOfDefsAll; - const std::string listingRefusal = whereisListingFromArg( strArg( "listing" ), + const std::string listingRefusal = whereisListingFromArg( listingArg, mcpdetail::findRawValue( args, "listing" ).isPresent, listing ); resp = !listingRefusal.empty() ? errResultMsg( -32602, listingRefusal ) : pagedResult( [ & ]( McpPageArgs pg ) { diff --git a/test/crossrefcheck.sh b/test/crossrefcheck.sh index ef7c3627b..f210cde53 100755 --- a/test/crossrefcheck.sh +++ b/test/crossrefcheck.sh @@ -593,6 +593,25 @@ if command -v python3 >/dev/null 2>&1; then && [ "$( mcpw ',"listing":"refs"' )" = "$( lw zqLean --whereis-listing=refs )" ] && [ "$( mcpw ',"listing":"bogus"' )" = "ERROR -32602" ] \ && ok "LEAN (L10): MCP whereis default / listing:all / listing:refs are byte-identical to the CLI; listing:bogus refuses -32602" \ || no "LEAN (L10): the MCP twin differs from the CLI" + # (L10b) CodeRabbit 5468003465: a NON-STRING listing refuses through the shared shape gate and echoes the value SENT. + # `listing` used to be read inside the whereis arm, after that gate, so listing:5 decoded to "" and the closed-set + # refusal said "got ''". Near-miss twins: a string outside the set still echoes itself; a string inside it answers (L10). + mcpwmsg(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whereis","arguments":{"path":"%s","symbol":"zqLean"%s}}}\n' "$LR" "$1" \ + | "$BIN" --mcp 2>/dev/null | python3 -c 'import sys,json +for line in sys.stdin: + line=line.strip() + if not line: continue + d=json.loads(line) + if "error" in d: print("ERROR",d["error"].get("code"),d["error"].get("message",""),end="") + elif d.get("result",{}).get("content"): print("ANSWER",end="")'; } + for arm in '5|5' '["all"]|["all"]' '{"v":1}|{"v":1}' 'true|true' '"bogus"|bogus'; do + val="${arm%%|*}"; echo_="${arm#*|}" + m="$( mcpwmsg ",\"listing\":$val" )" + case "$m" in + "ERROR -32602 invalid value for field: listing"*"got '$echo_'"*) ok "LEAN (L10b): MCP whereis listing:$val refuses -32602 and echoes '$echo_'" ;; + *) no "LEAN (L10b): MCP whereis listing:$val — want a -32602 refusal echoing '$echo_', got: ${m:-nothing}" ;; + esac + done else printf ' SKIP LEAN (L10) MCP twin (no python3)\n' fi From 140a6f35b82f67b9ce6bf3ef4bbc7dd4d0bc1ecb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 05:29:43 -0400 Subject: [PATCH 226/229] lint: bound --lint-max-per-rule so the scaled budgets stay representable (CR 5468003465, verbs_lint.h:775) CodeRabbit (Minor): the flag accepted up to kIntFlagMax (1e9), but the budget is multiplied by 20 for the atoms/cache pack engine budget (lintPackQueryBudget) and by 10 more for a floored answer's findings_next=. Where size_t is 32 bits, --lint-max-per-rule=300000000 wrapped. The flag's ceiling is now kLintMaxPerRuleMost = min(kIntFlagMax, SIZE_MAX / 200): refused above it, never saturated. It equals kIntFlagMax on every 64-bit target, so no accepted value moves. Follow-through on the same continuation: lintNextMaxPerRule saturated at INT_MAX, above the flag's own ceiling, so a run floored at a budget over 1e8 named a --lint-max-per-rule= value the parser refuses (e.g. 1000000010). It now saturates at the ceiling. static_asserts in lintPackQueryBudget state both properties; an EXPECTS states the budget is within the ceiling. Co-Authored-By: Claude Opus 5.5 --- src/cli.h | 9 ++++++++- src/lintrules.h | 7 ++++--- src/verbs_lint.h | 10 +++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/cli.h b/src/cli.h index 3705dee7a..93c3bdf92 100644 --- a/src/cli.h +++ b/src/cli.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -3438,6 +3439,12 @@ static_assert( viewFlagEmptyPolicyIsWellFormed(), // table changed which SENTENCE a rejected value gets, never which values are rejected. inline constexpr int kIntFlagMax = 1000000000; // parsePosInt/parseNonNegInt's own overflow ceiling inline constexpr int kConnectRadiusMax = 12; // == connectcfg::kMaxRadius (static_assert at the seam in main.cpp) +// --lint-max-per-rule's domain ceiling. The budget is multiplied twice downstream — 20x for the atoms/cache packs' engine +// budget (verbs_lint.h lintPackQueryBudget) and 10x more for a floored answer's findings_next= (lintrules.h +// lintNextMaxPerRule) — so the ceiling keeps both products representable in std::size_t: == kIntFlagMax wherever size_t is +// 64 bits, lower where it is 32 (CodeRabbit 5468003465). Refused above it, never saturated (§B8.1), and the continuation +// saturates at this same value so the call it names is always one this table accepts. +inline constexpr int kLintMaxPerRuleMost = int( std::min( std::size_t( kIntFlagMax ), std::numeric_limits::max() / 200 ) ); struct IntFlag { @@ -3477,7 +3484,7 @@ inline constexpr IntFlag kIntFlags[] = { "--detail=", &Config::detail, true, kIntFlagMax, "a non-negative integer (0 = off)", "--detail=2" }, // knob-honesty-068: the per-rule lint budget (kLintMaxPerRule is the default); a modifier of --lint/--lint-rules, // refused alone (validateLintSelectionModifierGuards) — the floored answer's findings_next= is the call that spells it. - { "--lint-max-per-rule=", &Config::lintMaxPerRule, false, kIntFlagMax, "a positive integer", "--lint-max-per-rule=50000" }, + { "--lint-max-per-rule=", &Config::lintMaxPerRule, false, kLintMaxPerRuleMost, "a positive integer", "--lint-max-per-rule=50000" }, // --cochange-recur=K: K is a count of SUB-WINDOWS, so it is bounded by kCoRecurSubWindows in practice; // the parser accepts any positive integer and the verb reports zero pairs above the ceiling rather than // refusing — an empty result under a disclosed min_recur= is a truthful answer, not an error. diff --git a/src/lintrules.h b/src/lintrules.h index 097c60623..b3594407d 100644 --- a/src/lintrules.h +++ b/src/lintrules.h @@ -842,11 +842,12 @@ inline std::size_t lintMaxPerRuleOf( int flagValue ) noexcept } // The budget a floored answer's findings_next= asks for: 10x the one that floored it — a re-run still floored names its own, -// so the chain terminates (the raw capture stream is finite). Saturating, so a pathological flag value cannot wrap. -inline std::size_t lintNextMaxPerRule( std::size_t spent ) noexcept +// so the chain terminates (the raw capture stream is finite). Saturating at `most`, the flag's own domain ceiling +// (cli.h kLintMaxPerRuleMost): it used to saturate at INT_MAX, above that ceiling, so a run floored at a budget over +// most/10 named a --lint-max-per-rule= value the parser refuses (CodeRabbit 5468003465 follow-through). +constexpr std::size_t lintNextMaxPerRule( std::size_t spent, std::size_t most ) noexcept { constexpr std::size_t kGrowth = 10; - const std::size_t most = std::size_t( std::numeric_limits::max() ); // the flag parses into an int return spent >= most / kGrowth ? most : spent * kGrowth; } diff --git a/src/verbs_lint.h b/src/verbs_lint.h index 8eba755c5..5e31c6472 100644 --- a/src/verbs_lint.h +++ b/src/verbs_lint.h @@ -544,7 +544,7 @@ std::string lintFindingsNext( const rw::Config& cfg, const std::vector& inv += " --naming-locals"; } inv += " " + rw::nextFlag( "--lint-select=", joined ); - inv += " --lint-max-per-rule=" + std::to_string( rw::lintNextMaxPerRule( spentPerRule ) ); + inv += " --lint-max-per-rule=" + std::to_string( rw::lintNextMaxPerRule( spentPerRule, std::size_t( rw::kLintMaxPerRuleMost ) ) ); if( cfg.sarif ) { inv += " --sarif"; // the SARIF run's findingsNext re-runs in the dialect it was read from (rv-knob-honesty-068 N4) @@ -771,6 +771,14 @@ inline std::size_t lintPackQueryBudget( std::size_t packDefault, std::size_t max constexpr std::size_t kEngineRatio = 20; static_assert( rw::atoms::kAtomsQueryBudget == kEngineRatio * rw::kLintMaxPerRule && rw::cachelint::kCacheQueryBudget == kEngineRatio * rw::kLintMaxPerRule, "lintPackQueryBudget keeps the packs' default engine:per-rule ratio — re-derive kEngineRatio with the constants" ); + // CodeRabbit 5468003465: the flag's ceiling keeps this product, and the 10x continuation's, inside size_t (a 32-bit + // size_t wrapped at --lint-max-per-rule=300000000); the continuation never names a value the flag refuses. + static_assert( std::size_t( rw::kLintMaxPerRuleMost ) <= std::numeric_limits::max() / ( kEngineRatio * 10 ), + "--lint-max-per-rule's ceiling keeps the pack budget and its 10x continuation representable" ); + static_assert( rw::lintNextMaxPerRule( std::size_t( rw::kLintMaxPerRuleMost ) / 10 + 1, std::size_t( rw::kLintMaxPerRuleMost ) ) + == std::size_t( rw::kLintMaxPerRuleMost ), + "findings_next= saturates at the flag's own ceiling, never above it" ); + EXPECTS( maxPerRule <= std::size_t( rw::kLintMaxPerRuleMost ), "the per-rule budget is a parsed --lint-max-per-rule (cli.h refuses above its ceiling) or the default" ); return maxPerRule > packDefault / kEngineRatio ? maxPerRule * kEngineRatio : packDefault; } From 00076e33e6ba696d4bbfb8a51177deae64f6d27d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 05:29:44 -0400 Subject: [PATCH 227/229] lsp: reject an empty hover-word slot at compile time (CR 5468003465 nitpick, lsp.h:370) CodeRabbit (nitpick): the comment said an appended SymKind without a word "does not compile", which is false. std::array value-initializes the elements a short initializer list leaves out, so the missing word would be nullptr and the hover builder would dereference it. A static_assert now rejects any nullptr entry; the table and its enum-order mapping are unchanged, and the comment says what the guard actually is. Co-Authored-By: Claude Opus 5.5 --- src/lsp.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lsp.h b/src/lsp.h index 25f796dd9..f5a2fcdad 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -368,14 +368,17 @@ inline int lspKindOf( rw::SymKind k ) noexcept } // The hover word per SymKind, a declarative table in enum order (CONTRIBUTING §3's table-over-switch rule; it was a -// switch, and the three Go named-type kinds made it a near-clone of model.h's langTag switch). Sized by kSymKindCount, -// so an appended kind without a word does not compile. +// switch, and the three Go named-type kinds made it a near-clone of model.h's langTag switch). Sized by kSymKindCount, and +// the static_assert below rejects an empty slot: std::array value-initializes the elements an initializer list leaves out, +// so the size alone let an appended kind without a word compile to a nullptr hover word (CodeRabbit 5468003465). inline constexpr std::array kLspKindWords = { "function", "method", "class", "struct", "interface", "variable", "section", "macro", "field", "symbol", // Other "module scope", // ModuleScope "type", "type alias", "function type" // NamedType, Alias, FuncType }; +static_assert( std::ranges::none_of( kLspKindWords, []( const char* w ) { return w == nullptr; } ), + "every SymKind needs a hover word in kLspKindWords (an initializer list shorter than kSymKindCount leaves nullptr)" ); inline const char* lspKindWord( rw::SymKind k ) noexcept { EXPECTS( std::size_t( k ) < kLspKindWords.size(), "a symbol's kind is a SymKind enumerator (the cache validates the byte)" ); From 93aeab1d6f03cd6c3e3f6cbde337018988203f65 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 05:30:29 -0400 Subject: [PATCH 228/229] test: lintbudgetcheck 64-bit ceiling twin for --lint-max-per-rule (CR 5468003465, verbs_lint.h:775) The ceiling change is a no-op where size_t is 64 bits; the twin holds the accepted domain there (1e9 answers, 1e9+1 refuses) and skips by name when getconf LONG_BIT is not 64. Co-Authored-By: Claude Opus 5.5 --- test/lintbudgetcheck.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/lintbudgetcheck.sh b/test/lintbudgetcheck.sh index 77d3e0c39..df4a1a4d0 100755 --- a/test/lintbudgetcheck.sh +++ b/test/lintbudgetcheck.sh @@ -165,6 +165,19 @@ esac || no "--lint-max-per-rule without --lint was not refused" "$BIN" "$GF" --lint --lint-max-per-rule=0 --no-cache >/dev/null 2>&1; [ $? = 1 ] \ && ok "refused: --lint-max-per-rule=0 exits 1" || no "--lint-max-per-rule=0 was accepted" +# CodeRabbit 5468003465: the flag's ceiling (cli.h kLintMaxPerRuleMost = min(kIntFlagMax, SIZE_MAX/200)) keeps the 20x pack +# budget and the 10x findings_next= continuation inside size_t, and the continuation saturates AT it (lintNextMaxPerRule; both +# stated by static_asserts in verbs_lint.h lintPackQueryBudget). Where size_t is 64 bits the ceiling IS kIntFlagMax, so the +# accepted domain did not move: the twin below holds that (1e9 answers, 1e9+1 refuses). On a 32-bit size_t the ceiling is +# lower by design, so the twin's premise is the word size, checked by name. +if [ "$( getconf LONG_BIT 2>/dev/null )" = 64 ]; then + "$BIN" "$TMP/smallfix" --lint --lint-max-per-rule=1000000000 --no-cache >/dev/null 2>&1; [ $? = 0 ] \ + && ok "ceiling twin (64-bit): --lint-max-per-rule=1000000000 still answers" || no "--lint-max-per-rule=1000000000 refused on a 64-bit build" + "$BIN" "$TMP/smallfix" --lint --lint-max-per-rule=1000000001 --no-cache >/dev/null 2>&1; [ $? = 1 ] \ + && ok "ceiling twin (64-bit): --lint-max-per-rule=1000000001 refuses (exit 1)" || no "--lint-max-per-rule=1000000001 was accepted" +else + printf ' SKIP ceiling twin — getconf LONG_BIT is not 64 (the ceiling is SIZE_MAX/200 there by design)\n' +fi # SARIF: the CI surface carries the same call (run properties), beside findingsCapped GS="$( "$BIN" "$GF" --lint --sarif --no-cache 2>/dev/null )" printf '%s' "$GS" | python3 -c ' From 4a0e47061bbf22e5937a7bc862de3647c318623d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 9 Oct 2026 10:59:33 -0400 Subject: [PATCH 229/229] fix: base-class receiver is a keyword per language, never a local spelled base/super (CR 5469474915, receiverevidence.h:124) isSuperRoot accepted the root "base" and "super" in every language, so a local named `base` (Python/JS/TS/Go/Java/Kotlin/Swift/C++/Ruby) or `super` (Go, C#) resolved through the caller's bases only and drew a PROVEN edge to a base class's same-named method, ignoring the local's own type. It now takes the Reference and accepts each spelling only where the grammar makes it the keyword: C# `base`; `super` in Java, Kotlin, Swift, JS and TS; Python's super() call (RecvKind::SuperObj). All three call sites pass the reference (ReceiverEvidence::rootClass, narrow's class-side test, and graph.h ladderProves). Arms: receiverevidencecheck (R) over test/receiverevidencefix/superroot/, red on 93aeab1d, with the keyword twins as guards. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 2 +- src/receiverevidence.h | 29 ++++++++--- test/receiverevidencecheck.sh | 52 +++++++++++++++++++ test/receiverevidencefix/README.md | 5 ++ .../superroot/cpp/src/panel.cpp | 16 ++++++ .../superroot/cpp/src/shapes.h | 12 +++++ .../superroot/cs/App/Panel.cs | 24 +++++++++ .../superroot/cs/App/Shapes.cs | 12 +++++ test/receiverevidencefix/superroot/go/go.mod | 3 ++ .../superroot/go/src/panel.go | 24 +++++++++ .../superroot/go/src/shapes.go | 13 +++++ .../superroot/java/src/app/Base.java | 5 ++ .../superroot/java/src/app/Panel.java | 19 +++++++ .../superroot/java/src/app/Widget.java | 5 ++ .../superroot/js/lib/panel.js | 19 +++++++ .../superroot/js/lib/shapes.js | 11 ++++ .../superroot/js/package.json | 1 + .../superroot/kt/src/app/Panel.kt | 19 +++++++ .../superroot/kt/src/app/Shapes.kt | 9 ++++ .../superroot/py/pkg/__init__.py | 0 .../superroot/py/pkg/panel.py | 18 +++++++ .../superroot/py/pkg/shapes.py | 12 +++++ .../superroot/rb/lib/panel.rb | 13 +++++ .../superroot/rb/lib/shapes.rb | 11 ++++ .../superroot/swift/Sources/App/Panel.swift | 17 ++++++ .../superroot/swift/Sources/App/Shapes.swift | 7 +++ .../superroot/ts/package.json | 1 + .../superroot/ts/src/panel.ts | 17 ++++++ .../superroot/ts/src/shapes.ts | 9 ++++ 29 files changed, 378 insertions(+), 7 deletions(-) create mode 100644 test/receiverevidencefix/superroot/cpp/src/panel.cpp create mode 100644 test/receiverevidencefix/superroot/cpp/src/shapes.h create mode 100644 test/receiverevidencefix/superroot/cs/App/Panel.cs create mode 100644 test/receiverevidencefix/superroot/cs/App/Shapes.cs create mode 100644 test/receiverevidencefix/superroot/go/go.mod create mode 100644 test/receiverevidencefix/superroot/go/src/panel.go create mode 100644 test/receiverevidencefix/superroot/go/src/shapes.go create mode 100644 test/receiverevidencefix/superroot/java/src/app/Base.java create mode 100644 test/receiverevidencefix/superroot/java/src/app/Panel.java create mode 100644 test/receiverevidencefix/superroot/java/src/app/Widget.java create mode 100644 test/receiverevidencefix/superroot/js/lib/panel.js create mode 100644 test/receiverevidencefix/superroot/js/lib/shapes.js create mode 100644 test/receiverevidencefix/superroot/js/package.json create mode 100644 test/receiverevidencefix/superroot/kt/src/app/Panel.kt create mode 100644 test/receiverevidencefix/superroot/kt/src/app/Shapes.kt create mode 100644 test/receiverevidencefix/superroot/py/pkg/__init__.py create mode 100644 test/receiverevidencefix/superroot/py/pkg/panel.py create mode 100644 test/receiverevidencefix/superroot/py/pkg/shapes.py create mode 100644 test/receiverevidencefix/superroot/rb/lib/panel.rb create mode 100644 test/receiverevidencefix/superroot/rb/lib/shapes.rb create mode 100644 test/receiverevidencefix/superroot/swift/Sources/App/Panel.swift create mode 100644 test/receiverevidencefix/superroot/swift/Sources/App/Shapes.swift create mode 100644 test/receiverevidencefix/superroot/ts/package.json create mode 100644 test/receiverevidencefix/superroot/ts/src/panel.ts create mode 100644 test/receiverevidencefix/superroot/ts/src/shapes.ts diff --git a/src/graph.h b/src/graph.h index c0540e378..e8eb7b1bc 100644 --- a/src/graph.h +++ b/src/graph.h @@ -5994,7 +5994,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } return owner.empty() && cs.fileId == ref.fileId && cs.kind == SymKind::Method; // an object literal's own member } - if( isSuperRoot( ch.root ) ) + if( isSuperRoot( ref, ch.root ) ) { const std::string_view callerCls = recvEv.callerClass( ref.fromSymbol ); return !callerCls.empty() && !owner.empty() && owner != callerCls && recvEv.inCone( callerCls, owner ) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 2a65dd680..70b2c47cd 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -6,8 +6,8 @@ // with nothing behind it but the name. This header holds the two halves of the fix: // // 1. ReceiverEvidence::narrow — a RESOLVE rule for receivers the source types: `this`/`self`/`cls` inside a class (and -// `super`/`base`, through the bases only), a parameter or local whose class is written (a TS/Python/Go annotation, a -// Go method receiver, a JS `new Foo()` / Python `Foo()` / Go `Foo{}` initializer), a constructed receiver +// the language's base-class receiver, isSuperRoot, through the bases only), a parameter or local whose class is +// written (a TS/Python/Go annotation, a Go method receiver, a JS `new Foo()` / Python `Foo()` / Go `Foo{}` initializer), a constructed receiver // (`new Foo().m()`), a class-name receiver (`Foo.m()`), and a chain of fields whose classes are stated // (`this.bucket = new Schemas()`, Python `self.x = Foo()`, a Go struct field, a Go EMBEDDED field whose methods // are promoted). A hit is the call's whole answer, exactly like Rule 2. Ruby's own method lookup (mixins, typed and @@ -118,9 +118,26 @@ inline bool isThisRoot( std::string_view root ) noexcept return root == "this" || root == "self" || root == "Self"; } -inline bool isSuperRoot( std::string_view root ) noexcept +// Is `root` (the call's receiver root, ReceiverEvidence::chainOf) the language's BASE-CLASS receiver — a lookup that walks +// the caller's bases only? Each spelling counts only where the grammar makes it that keyword: C# `base`; `super` in +// Java, Kotlin, Swift, JS and TS; Python's `super()` / `super(C, self)` call, which ingest records as RecvKind::SuperObj. +// Everywhere else the same text is an ordinary name — a Python/JS/Go/Java/… local `base`, a Go or C# local `super`, a +// Python name `super` that shadows the builtin — and proves nothing about the bases (its own type, if any, decides). +inline bool isSuperRoot( const Reference& r, std::string_view root ) noexcept { - return root == "super" || root == "base"; + if( r.recv == RecvKind::SuperObj ) + { + return true; + } + switch( r.lang ) + { + case Lang::CSharp: + return root == "base"; + case Lang::Java: case Lang::Kotlin: case Lang::Swift: case Lang::JavaScript: case Lang::TypeScript: + return root == "super"; + default: + return false; + } } // The innermost function/method of `id`'s file whose span strictly holds `id`'s span; kNoNode when none. `byFile` is @@ -689,7 +706,7 @@ struct ReceiverEvidence { return callerClass( r.fromSymbol ); } - if( isSuperRoot( root ) ) + if( isSuperRoot( r, root ) ) { superOnly = true; return callerClass( r.fromSymbol ); @@ -762,7 +779,7 @@ struct ReceiverEvidence } // the side the receiver names: the class object (a class-name receiver, or `this`/`super` inside a static member), // else an instance; a stated field below always holds an instance - bool classSide = isStatic || ( ch.ctor.empty() && ( isThisRoot( ch.root ) || isSuperRoot( ch.root ) ) && callerIsStatic( r.fromSymbol ) ); + bool classSide = isStatic || ( ch.ctor.empty() && ( isThisRoot( ch.root ) || isSuperRoot( r, ch.root ) ) && callerIsStatic( r.fromSymbol ) ); std::string_view rest = ch.path; while( !rest.empty() ) // walk the stated field classes { diff --git a/test/receiverevidencecheck.sh b/test/receiverevidencecheck.sh index f38b0b62a..f8afcccae 100755 --- a/test/receiverevidencecheck.sh +++ b/test/receiverevidencecheck.sh @@ -125,6 +125,10 @@ # Output method (fmt.Errorf / strings.Split pinned clean), TS Date#getTime → an in-repo getTime (the # global Response pinned clean), Python list.append on a list literal → Content.append; each on # --callees and its --for hop block, with constructed-local near misses. +# (R) the base-class receiver (CR 5469474915): C# `base` and `super` where it is a keyword (Java/Kotlin/Swift/JS/TS), +# and Python's super() call, walk the bases only; a LOCAL spelled `base` (Python/JS/TS/Go/Java/Kotlin/Swift/C++/ +# Ruby) or `super` (Go, C#, a Python name shadowing the builtin) never proves the base class's method — a typed +# one resolves to its own class, an untyped one is hedged. Twins: the keyword calls still prove the base. # (C) conservation: each root's census dispositions still sum to calls= with unaccounted=0 (no silent drop). # (K) the predicates can fail: a planted unhedged false row fails notproven, a planted hedged row passes it and # fails proven, marked rejects an unhedged row, a rootless or not-found answer is NOROOT, the MCP reader @@ -1012,6 +1016,54 @@ for sel in lib/reply.js:respondWith lib/reply.js:notFound lib/request.js:compile fi done +echo "=== (R) the base-class receiver is a KEYWORD, never a local spelled like one (CR 5469474915, receiverevidence.h:124) ===" +# superroot/: Panel extends Base; Base and Widget both define render. A local `base` (C#/Go: `super`) holding a +# Widget, or an untyped value, never proves Base's render through the bases-only lookup: the typed local resolves to its +# own class, the untyped one is hedged. The keyword twins (`super.render()`, Python `super().render()`, Go's promoted +# p.Render()) still prove Base's render. C# `base.Render()` is pinned as visible only: no C# tree reaches the base-class +# receiver today (the grammar's `base` node is not read as a receiver root — a disclosed floor, never a false row). +SR=superroot +notproven $SR/py callees pkg/panel.py:untyped "render pkg/shapes.py:2" +notproven $SR/py callees pkg/panel.py:shadowed "render pkg/shapes.py:2" +visible $SR/py callees pkg/panel.py:untyped "render pkg/shapes.py:2" "render pkg/shapes.py:7" +exactproven $SR/py callees pkg/panel.py:typed "cls Widget pkg/shapes.py;fn render pkg/shapes.py" # guard: Rule 2 types it first +proven $SR/py callees pkg/panel.py:typed "fn render pkg/shapes.py:7" # guard +proven $SR/py callees pkg/panel.py:real_super "fn render pkg/shapes.py:2" # guard: super() walks the bases +for l in js ts; do + f="$( [ "$l" = js ] && echo lib/panel.js || echo src/panel.ts )"; d="$( [ "$l" = js ] && echo lib/shapes.js || echo src/shapes.ts )" + notproven $SR/$l callees "$f:typed" "render $d:2" + proven $SR/$l callees "$f:typed" "method render $d:6" + notproven $SR/$l callees "$f:untyped" "render $d:2" + visible $SR/$l callees "$f:untyped" "render $d:2" "render $d:6" + proven $SR/$l callees "$f:realSuper" "method render $d:2" # guard: super.render() +done +notproven $SR/go callees src/panel.go:Typed "Render src/shapes.go:5" +proven $SR/go callees src/panel.go:Typed "method Render src/shapes.go:9" +notproven $SR/go callees src/panel.go:Untyped "Render src/shapes.go:5" +notproven $SR/go callees src/panel.go:Super "Render src/shapes.go:5" +visible $SR/go callees src/panel.go:Super "Render src/shapes.go:5" "Render src/shapes.go:9" +proven $SR/go callees src/panel.go:Promoted "method Render src/shapes.go:5" # guard: the embedded Base's promoted method +notproven $SR/java callees src/app/Panel.java:typed "render src/app/Base.java" +proven $SR/java callees src/app/Panel.java:typed "method render src/app/Widget.java" +notproven $SR/java callees src/app/Panel.java:untyped "render src/app/Base.java" +proven $SR/java callees src/app/Panel.java:realSuper "method render src/app/Base.java" # guard +notproven $SR/kt callees src/app/Panel.kt:typed "render src/app/Shapes.kt:4" +proven $SR/kt callees src/app/Panel.kt:typed "fn render src/app/Shapes.kt:8" +notproven $SR/kt callees src/app/Panel.kt:untyped "render src/app/Shapes.kt:4" +proven $SR/kt callees src/app/Panel.kt:realSuper "fn render src/app/Shapes.kt:4" # guard +notproven $SR/swift callees Sources/App/Panel.swift:typed "render Sources/App/Shapes.swift:2" +proven $SR/swift callees Sources/App/Panel.swift:typed "fn render Sources/App/Shapes.swift:6" +notproven $SR/swift callees Sources/App/Panel.swift:untyped "render Sources/App/Shapes.swift:2" +proven $SR/swift callees Sources/App/Panel.swift:realSuper "fn render Sources/App/Shapes.swift:2" # guard +notproven $SR/cs callees App/Panel.cs:Typed "Render App/Shapes.cs:5" +proven $SR/cs callees App/Panel.cs:Typed "method Render App/Shapes.cs:10" +notproven $SR/cs callees App/Panel.cs:Untyped "Render App/Shapes.cs:5" +visible $SR/cs callees App/Panel.cs:RealBase "Render App/Shapes.cs:5" # guard (floor above) +notproven $SR/cpp callees src/panel.cpp:untyped "render src/shapes.h:6" +proven $SR/cpp callees src/panel.cpp:typed "method render src/shapes.h:11" # guard: Rule 2 types it first +notproven $SR/rb callees lib/panel.rb:untyped "render lib/shapes.rb:2" +proven $SR/rb callees lib/panel.rb:typed "method render lib/shapes.rb:8" # guard + echo "=== (C) conservation: no call silently disappears ===" ROOTS="js ts py go java kt cs cpp swift rb" for r in $ROOTS; do diff --git a/test/receiverevidencefix/README.md b/test/receiverevidencefix/README.md index f66f2a7a2..d9a855b89 100644 --- a/test/receiverevidencefix/README.md +++ b/test/receiverevidencefix/README.md @@ -52,3 +52,8 @@ The names are paraphrases of graded false rows; the code is minimal and is never `nestedBlock` / `lambdaOutside` / `blockOutside` call it past the block that hid it, which must never prove Barrel's. A generic whose type parameter is spelled `Tank` (method and class, a field of that type) must never prove class Tank. Kept: the field itself, a typed local, a typed loop / lambda variable's own class, a typed parameter. +- `superroot/{py,js,ts,go,java,kt,swift,cs,cpp,rb}` — CR 5469474915: `Panel` extends `Base`; `Base` and `Widget` both + define `render`. `typed` binds a local spelled `base` (C#: `super`) to a `Widget`, `untyped` to a value nothing types + (Python and Go also a local `super`), then calls `render` on it: never Base's render through the bases-only lookup. + Kept: `super.render()`, Python `super().render()`, Go's promoted `p.Render()`, the typed local's own class. C# + `base.Render()` is visible only (no C# tree reads `base` as a receiver root yet). diff --git a/test/receiverevidencefix/superroot/cpp/src/panel.cpp b/test/receiverevidencefix/superroot/cpp/src/panel.cpp new file mode 100644 index 000000000..3163695a1 --- /dev/null +++ b/test/receiverevidencefix/superroot/cpp/src/panel.cpp @@ -0,0 +1,16 @@ +#include "shapes.h" + +struct Panel : Base +{ + const char* typed() + { + Widget base; + return base.render(); + } + + const char* untyped() + { + auto base = outside::make(); + return base.render(); + } +}; diff --git a/test/receiverevidencefix/superroot/cpp/src/shapes.h b/test/receiverevidencefix/superroot/cpp/src/shapes.h new file mode 100644 index 000000000..c7df0aa86 --- /dev/null +++ b/test/receiverevidencefix/superroot/cpp/src/shapes.h @@ -0,0 +1,12 @@ +#pragma once +#include + +struct Base +{ + virtual const char* render() { return "base"; } +}; + +struct Widget +{ + const char* render() { return "widget"; } +}; diff --git a/test/receiverevidencefix/superroot/cs/App/Panel.cs b/test/receiverevidencefix/superroot/cs/App/Panel.cs new file mode 100644 index 000000000..fec3817ec --- /dev/null +++ b/test/receiverevidencefix/superroot/cs/App/Panel.cs @@ -0,0 +1,24 @@ +using Outside; + +namespace App +{ + public class Panel : Base + { + public string Typed() + { + var super = new Widget(); + return super.Render(); + } + + public string Untyped() + { + var super = Factory.Make(); + return super.Render(); + } + + public string RealBase() + { + return base.Render(); + } + } +} diff --git a/test/receiverevidencefix/superroot/cs/App/Shapes.cs b/test/receiverevidencefix/superroot/cs/App/Shapes.cs new file mode 100644 index 000000000..8090d1679 --- /dev/null +++ b/test/receiverevidencefix/superroot/cs/App/Shapes.cs @@ -0,0 +1,12 @@ +namespace App +{ + public class Base + { + public virtual string Render() { return "base"; } + } + + public class Widget + { + public string Render() { return "widget"; } + } +} diff --git a/test/receiverevidencefix/superroot/go/go.mod b/test/receiverevidencefix/superroot/go/go.mod new file mode 100644 index 000000000..8ade2c593 --- /dev/null +++ b/test/receiverevidencefix/superroot/go/go.mod @@ -0,0 +1,3 @@ +module example.com/superroot + +go 1.21 diff --git a/test/receiverevidencefix/superroot/go/src/panel.go b/test/receiverevidencefix/superroot/go/src/panel.go new file mode 100644 index 000000000..c1868d90d --- /dev/null +++ b/test/receiverevidencefix/superroot/go/src/panel.go @@ -0,0 +1,24 @@ +package src + +type Panel struct { + Base +} + +func (p *Panel) Typed() string { + base := &Widget{} + return base.Render() +} + +func (p *Panel) Untyped() string { + base := makeAny() + return base.Render() +} + +func (p *Panel) Super() string { + super := makeAny() + return super.Render() +} + +func (p *Panel) Promoted() string { + return p.Render() +} diff --git a/test/receiverevidencefix/superroot/go/src/shapes.go b/test/receiverevidencefix/superroot/go/src/shapes.go new file mode 100644 index 000000000..22cd90855 --- /dev/null +++ b/test/receiverevidencefix/superroot/go/src/shapes.go @@ -0,0 +1,13 @@ +package src + +type Base struct{} + +func (b *Base) Render() string { return "base" } + +type Widget struct{} + +func (w *Widget) Render() string { return "widget" } + +func NewWidget() *Widget { return &Widget{} } + +func makeAny() interface{ Render() string } { return nil } diff --git a/test/receiverevidencefix/superroot/java/src/app/Base.java b/test/receiverevidencefix/superroot/java/src/app/Base.java new file mode 100644 index 000000000..c89e3fe1d --- /dev/null +++ b/test/receiverevidencefix/superroot/java/src/app/Base.java @@ -0,0 +1,5 @@ +package app; + +public class Base { + public String render() { return "base"; } +} diff --git a/test/receiverevidencefix/superroot/java/src/app/Panel.java b/test/receiverevidencefix/superroot/java/src/app/Panel.java new file mode 100644 index 000000000..5032d3b45 --- /dev/null +++ b/test/receiverevidencefix/superroot/java/src/app/Panel.java @@ -0,0 +1,19 @@ +package app; + +import outside.Factory; + +public class Panel extends Base { + public String typed() { + Widget base = new Widget(); + return base.render(); + } + + public String untyped() { + var base = Factory.make(); + return base.render(); + } + + public String realSuper() { + return super.render(); + } +} diff --git a/test/receiverevidencefix/superroot/java/src/app/Widget.java b/test/receiverevidencefix/superroot/java/src/app/Widget.java new file mode 100644 index 000000000..b8480fa12 --- /dev/null +++ b/test/receiverevidencefix/superroot/java/src/app/Widget.java @@ -0,0 +1,5 @@ +package app; + +public class Widget { + public String render() { return "widget"; } +} diff --git a/test/receiverevidencefix/superroot/js/lib/panel.js b/test/receiverevidencefix/superroot/js/lib/panel.js new file mode 100644 index 000000000..4ecf586c5 --- /dev/null +++ b/test/receiverevidencefix/superroot/js/lib/panel.js @@ -0,0 +1,19 @@ +const { Base, Widget, makeBase } = require('./shapes'); + +class Panel extends Base { + typed() { + const base = new Widget(); + return base.render(); + } + + untyped() { + const base = makeBase(); + return base.render(); + } + + realSuper() { + return super.render(); + } +} + +module.exports = { Panel }; diff --git a/test/receiverevidencefix/superroot/js/lib/shapes.js b/test/receiverevidencefix/superroot/js/lib/shapes.js new file mode 100644 index 000000000..e5f60a717 --- /dev/null +++ b/test/receiverevidencefix/superroot/js/lib/shapes.js @@ -0,0 +1,11 @@ +class Base { + render() { return 'base'; } +} + +class Widget { + render() { return 'widget'; } +} + +function makeBase() { return {}; } + +module.exports = { Base, Widget, makeBase }; diff --git a/test/receiverevidencefix/superroot/js/package.json b/test/receiverevidencefix/superroot/js/package.json new file mode 100644 index 000000000..728d11064 --- /dev/null +++ b/test/receiverevidencefix/superroot/js/package.json @@ -0,0 +1 @@ +{ "name": "superroot-js", "version": "0.0.0" } diff --git a/test/receiverevidencefix/superroot/kt/src/app/Panel.kt b/test/receiverevidencefix/superroot/kt/src/app/Panel.kt new file mode 100644 index 000000000..69ff5c01f --- /dev/null +++ b/test/receiverevidencefix/superroot/kt/src/app/Panel.kt @@ -0,0 +1,19 @@ +package app + +import outside.makeAny + +class Panel : Base() { + fun typed(): String { + val base = Widget() + return base.render() + } + + fun untyped(): String { + val base = makeAny() + return base.render() + } + + fun realSuper(): String { + return super.render() + } +} diff --git a/test/receiverevidencefix/superroot/kt/src/app/Shapes.kt b/test/receiverevidencefix/superroot/kt/src/app/Shapes.kt new file mode 100644 index 000000000..07d04d615 --- /dev/null +++ b/test/receiverevidencefix/superroot/kt/src/app/Shapes.kt @@ -0,0 +1,9 @@ +package app + +open class Base { + open fun render(): String = "base" +} + +class Widget { + fun render(): String = "widget" +} diff --git a/test/receiverevidencefix/superroot/py/pkg/__init__.py b/test/receiverevidencefix/superroot/py/pkg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/receiverevidencefix/superroot/py/pkg/panel.py b/test/receiverevidencefix/superroot/py/pkg/panel.py new file mode 100644 index 000000000..59e41ae54 --- /dev/null +++ b/test/receiverevidencefix/superroot/py/pkg/panel.py @@ -0,0 +1,18 @@ +from pkg.shapes import Base, Widget, make_base + + +class Panel(Base): + def typed(self): + base = Widget() + return base.render() + + def untyped(self): + base = make_base() + return base.render() + + def shadowed(self): + super = make_base() + return super.render() + + def real_super(self): + return super().render() diff --git a/test/receiverevidencefix/superroot/py/pkg/shapes.py b/test/receiverevidencefix/superroot/py/pkg/shapes.py new file mode 100644 index 000000000..1cc3e732f --- /dev/null +++ b/test/receiverevidencefix/superroot/py/pkg/shapes.py @@ -0,0 +1,12 @@ +class Base: + def render(self): + return "base" + + +class Widget: + def render(self): + return "widget" + + +def make_base(): + return object() diff --git a/test/receiverevidencefix/superroot/rb/lib/panel.rb b/test/receiverevidencefix/superroot/rb/lib/panel.rb new file mode 100644 index 000000000..6d7fbde6b --- /dev/null +++ b/test/receiverevidencefix/superroot/rb/lib/panel.rb @@ -0,0 +1,13 @@ +require_relative "shapes" + +class Panel < Base + def typed + base = Widget.new + base.render + end + + def untyped + base = Outside.make + base.render + end +end diff --git a/test/receiverevidencefix/superroot/rb/lib/shapes.rb b/test/receiverevidencefix/superroot/rb/lib/shapes.rb new file mode 100644 index 000000000..38111d963 --- /dev/null +++ b/test/receiverevidencefix/superroot/rb/lib/shapes.rb @@ -0,0 +1,11 @@ +class Base + def render + "base" + end +end + +class Widget + def render + "widget" + end +end diff --git a/test/receiverevidencefix/superroot/swift/Sources/App/Panel.swift b/test/receiverevidencefix/superroot/swift/Sources/App/Panel.swift new file mode 100644 index 000000000..4b4e71c5f --- /dev/null +++ b/test/receiverevidencefix/superroot/swift/Sources/App/Panel.swift @@ -0,0 +1,17 @@ +import Outside + +class Panel: Base { + func typed() -> String { + let base = Widget() + return base.render() + } + + func untyped() -> String { + let base = makeAny() + return base.render() + } + + func realSuper() -> String { + return super.render() + } +} diff --git a/test/receiverevidencefix/superroot/swift/Sources/App/Shapes.swift b/test/receiverevidencefix/superroot/swift/Sources/App/Shapes.swift new file mode 100644 index 000000000..42fe7167b --- /dev/null +++ b/test/receiverevidencefix/superroot/swift/Sources/App/Shapes.swift @@ -0,0 +1,7 @@ +class Base { + func render() -> String { return "base" } +} + +class Widget { + func render() -> String { return "widget" } +} diff --git a/test/receiverevidencefix/superroot/ts/package.json b/test/receiverevidencefix/superroot/ts/package.json new file mode 100644 index 000000000..2aa201d1e --- /dev/null +++ b/test/receiverevidencefix/superroot/ts/package.json @@ -0,0 +1 @@ +{ "name": "superroot-ts", "version": "0.0.0" } diff --git a/test/receiverevidencefix/superroot/ts/src/panel.ts b/test/receiverevidencefix/superroot/ts/src/panel.ts new file mode 100644 index 000000000..ebf1a2f30 --- /dev/null +++ b/test/receiverevidencefix/superroot/ts/src/panel.ts @@ -0,0 +1,17 @@ +import { Base, Widget, makeBase } from './shapes'; + +export class Panel extends Base { + typed(): string { + const base: Widget = new Widget(); + return base.render(); + } + + untyped(): string { + const base = makeBase(); + return base.render(); + } + + realSuper(): string { + return super.render(); + } +} diff --git a/test/receiverevidencefix/superroot/ts/src/shapes.ts b/test/receiverevidencefix/superroot/ts/src/shapes.ts new file mode 100644 index 000000000..ba98f5ed6 --- /dev/null +++ b/test/receiverevidencefix/superroot/ts/src/shapes.ts @@ -0,0 +1,9 @@ +export class Base { + render(): string { return 'base'; } +} + +export class Widget { + render(): string { return 'widget'; } +} + +export function makeBase(): any { return {}; }

@@ -2972,7 +2990,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3002,7 +3020,7 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - @@ -3241,7 +3259,7 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - + @@ -3278,7 +3296,7 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - + @@ -3359,7 +3377,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3385,11 +3403,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3464,7 +3482,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3498,11 +3516,11 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - - + + @@ -3534,7 +3552,7 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - @@ -3569,7 +3587,7 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - + @@ -3594,17 +3612,17 @@ The default lists defs when that page is strictly shorter in bytes than all unde **Try it** -_The whole hit list, references included: the page the default is measured against. The default lists only the definitions (listing="defs", the references counted in ) when that page is strictly shorter in bytes, and this page otherwise._ +_The definitions-only page asked for by name: every kind="def" row (here the first 60, most on branches, each carrying tip= and date=) and the references counted in . The default lists this page only when it is strictly shorter in bytes than the whole list (--whereis-listing=all) under the same row cap; on this symbol it is not, so the default above listed every hit._ ``` -$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - +$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=defs + + - - - + + ... [24 more line(s); run it to see the whole thing] ``` @@ -3745,17 +3763,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3780,17 +3798,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3814,18 +3832,18 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - - - - - - - - + + + + + + + + + + + + ... [18 more line(s); run it to see the whole thing] ``` @@ -3850,7 +3868,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3915,7 +3933,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4075,7 +4093,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + @@ -4150,13 +4168,13 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - + @@ -4184,13 +4202,13 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - + @@ -4308,7 +4326,7 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 - + @@ -4340,19 +4358,19 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - + + + + + - - - - - - + + + ... [8 more line(s); run it to see the whole thing] @@ -4412,25 +4430,19 @@ full restores the prose output legend posture for EVERY XML verb. The DEFAULT is **Try it** -_The grep compact legend (ripwire.grep/v1)._ +_The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend._ ``` -$ ./build/ripwire . --grep=DISCLOSE --legend=compact - + + + + + + +... [23 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4571,7 +4583,7 @@ $ ./build/ripwire . --map-diff --top-k=5 - + @@ -4846,9 +4858,9 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" @@ -63,6 +61,8 @@ *Same map, capped to the 5 highest-ranked symbols.* +**wall time: 1.84s** + ````` @@ -91,7 +91,7 @@ *NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* -**wall time: 1.97s** +**wall time: 1.59s** ````` @@ -150,7 +150,7 @@ ripwire: --top-k=0 means "no ranked map, payload only" — pass a payload verb ( *SHAPE the map to fit ~1500 tokens (binary-search top-K).* -**wall time: 1.67s** +**wall time: 1.70s** ````` @@ -168,7 +168,7 @@ ripwire: --top-k=0 means "no ranked map, payload only" — pass a payload verb ( *GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* -**exit code: 3** — **wall time: 1.66s** +**exit code: 3** ````` @@ -185,10 +185,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *The task lens: ranked signatures + quality metrics framed for the task.* -**wall time: 10.39s** +**wall time: 7.39s** ````` - @@ -218,17 +218,17 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline std::string spanTierMemoPath( const std::string& diskPath ) inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) inline NodeId resolveHandleAll( const McpIndex& ix, std::uint64_t idHash, std::vector< NodeId >& matches ) -… [46 more display lines; full output is 9110 bytes on 1 raw line(s)] +… [46 more display lines; full output is 8961 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="rankGraphTeleport"` *Name-shaped query: the router picks name-exact BM25 (header says which/why).* -**wall time: 3.88s** +**wall time: 2.71s** ````` - + @@ -265,10 +265,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* -**wall time: 1.51s** +**wall time: 3.53s** ````` - + @@ -295,7 +295,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 -### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] ### 2. Gates for A #### The convergence disclosure contract … [102 more display lines; full output is 14999 bytes on 72 raw line(s)] @@ -305,10 +305,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* -**wall time: 1.88s** +**wall time: 2.02s** ````` - + @@ -326,10 +326,10 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* -**wall time: 1.61s** +**wall time: 2.02s** ````` - + @@ -340,8 +340,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() struct FileHealth -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -enum class AstWalk : std::uint8_t +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… struct SliceScan inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept constexpr std::array<LangEntry, 51> kLangTable = @@ -353,8 +353,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 bool jsonNestsTooDeep( std::string_view bytes ) noexcept constexpr std::uint32_t kMaxYamlNestDepth = 64u SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline bool hasMainGuard( std::string_view source ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) struct LintRule enum class DisclosureWhy : std::uint8_t @@ -366,13 +366,13 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* -**wall time: 1.89s** +**wall time: 3.42s** ````` - - - + + The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) @@ -385,8 +385,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) -struct LensRanking -struct NameCorpusStats +struct NameCorpusStats +struct LensRanking inline bool hasIdentifierShape( std::string_view w ) noexcept inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) struct Bm25Params @@ -399,17 +399,17 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) struct RecallSectionPick inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) -… [56 more display lines; full output is 9311 bytes on 1 raw line(s)] +… [55 more display lines; full output is 9161 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` *Same task with the anchor disabled — the contrast the flag exists for.* -**wall time: 1.45s** +**wall time: 6.01s** ````` - @@ -424,8 +424,8 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) -struct LensRanking -struct NameCorpusStats +struct NameCorpusStats +struct LensRanking inline bool hasIdentifierShape( std::string_view w ) noexcept inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) @@ -446,8 +446,6 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Interface -> implementors view: every existing impl of the named interface; the method contract is extracted for the C-family/Java/TS/Python tiers — for a Rust trait (this fixture) it discloses caveat="not-extracted-for-lang" rather than an empty list.* -**wall time: 1.57s** - ````` @@ -464,7 +462,7 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* -**wall time: 1.58s** +**wall time: 1.42s** ````` @@ -507,8 +505,6 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* -**wall time: 1.09s** - ````` @@ -520,12 +516,12 @@ ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 *Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* -**wall time: 2.14s** +**wall time: 1.92s** ````` ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 -━━ README.md (relevance 7.995) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +━━ README.md (relevance 7.969) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -549,7 +545,7 @@ refuses by default. -━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.345) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] +━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.324) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] ### Root spelling (maintainer-owned; reproduce it once so you recognise it) ```bash @@ -558,47 +554,32 @@ refuses by default. ## `./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` -*The directory-as-knowledge-base pattern: --recall pointed at a 1588991-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* +*The directory-as-knowledge-base pattern: --recall pointed at a 1399178-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* ````` -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -━━ commands.md (relevance 10.658) ━━ [sections: 1 of 158 selected (187 in doc), section-granular; whole doc 721575 B; lines="515-540"; dropped_by_budget=157] [truncated: 1854 of 4242 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 10.211) ━━ [sections: 1 of 157 selected (187 in doc), section-granular; whole doc 533991 B; lines="3700-3704"; dropped_by_budget=156] [truncated: 1865 of 7739 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] -1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd - `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, - `.log`, `.json` and extensionless files are **not** documents to it. A directory of those - answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched - your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, - never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir - holds five dumps, and the header says `2 relevant of 2 document files` because only the two - `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not - in the population at all. +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` -2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so - an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document - SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R - selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got - and what the ceiling cost. … +stderr: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` +ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pass --no-redact to disable ````` ## `./build/ripwire . --tree` *File-by-file orientation map (top symbols per file).* -**wall time: 2.58s** +**wall time: 1.99s** ````` @@ -638,7 +619,7 @@ and both are yours, because the file you write is the only thing that sets them: *Self-contained HTML force-directed call graph.* -**wall time: 5.03s** +**wall time: 4.39s** ````` (empty) @@ -654,7 +635,7 @@ Artifact written: *Stable (path/id) emit order — provider KV-cache hits across re-runs.* -**wall time: 1.51s** +**wall time: 1.15s** ````` @@ -689,7 +670,7 @@ Artifact written: *Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* -**wall time: 1.63s** +**wall time: 1.03s** ````` @@ -729,8 +710,6 @@ Artifact written: *The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* -**wall time: 1.58s** - ````` @@ -769,7 +748,7 @@ Artifact written: *The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* -**wall time: 1.75s** +**wall time: 1.74s** ````` @@ -809,7 +788,7 @@ Artifact written: *Who calls SYM (1-hop in-edges).* -**wall time: 1.20s** +**wall time: 2.15s** ````` @@ -824,13 +803,16 @@ Artifact written: ````` -## `./build/ripwire . --callers=rankGraphTeleport --legend=compact` +## `./build/ripwire . --callers=rankGraphTeleport --legend=full` -*The same rows under --legend=compact: the prose legend becomes one compact comment plus schema="ripwire.callers/v1" on the root — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB of legend gone. Works on EVERY XML verb now, not four.* +*The same rows under --legend=full: the default is the compact dialect (one compact comment plus schema="ripwire.callers/v1" on the root), and --legend=full restores the prose legend — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB more legend. Works on EVERY XML verb.* + +**wall time: 2.14s** ````` - - + + + @@ -845,7 +827,7 @@ Artifact written: *Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* -**exit code: 1** — **wall time: 2.42s** +**exit code: 1** — **wall time: 1.08s** ````` @@ -862,7 +844,7 @@ ripwire: --callers symbol not found: DoesNotExist *What SYM calls (1-hop out-edges).* -**wall time: 1.98s** +**wall time: 1.93s** ````` @@ -882,7 +864,7 @@ ripwire: --callers symbol not found: DoesNotExist *The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* -**wall time: 1.23s** +**wall time: 2.72s** ````` @@ -903,7 +885,7 @@ ripwire: --callers symbol not found: DoesNotExist *Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* -**wall time: 1.91s** +**wall time: 1.85s** ````` @@ -943,7 +925,7 @@ ripwire: --callers symbol not found: DoesNotExist *Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* -**wall time: 1.55s** +**wall time: 2.08s** ````` @@ -983,7 +965,7 @@ ripwire: --callers symbol not found: DoesNotExist *The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* -**wall time: 2.02s** +**wall time: 1.89s** ````` @@ -1023,7 +1005,7 @@ ripwire: --callers symbol not found: DoesNotExist *Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* -**wall time: 1.45s** +**wall time: 2.01s** ````` @@ -1040,7 +1022,7 @@ ripwire: --callers symbol not found: DoesNotExist *Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* -**wall time: 2.67s** +**wall time: 1.90s** ````` @@ -1059,7 +1041,7 @@ ripwire: --callers symbol not found: DoesNotExist *Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* -**wall time: 2.21s** +**wall time: 1.90s** ````` @@ -1099,7 +1081,7 @@ ripwire: --callers symbol not found: DoesNotExist *Markdown docs that name SYM in a backtick (doc<->code edges).* -**wall time: 3.00s** +**wall time: 1.86s** ````` @@ -1118,7 +1100,7 @@ ripwire: --callers symbol not found: DoesNotExist *Test files that transitively reach the changed file.* -**wall time: 3.23s** +**wall time: 2.23s** ````` @@ -1146,12 +1128,12 @@ ripwire: --callers symbol not found: DoesNotExist *Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* -**wall time: 2.41s** +**wall time: 2.25s** ````` ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 1c08714cf +at: 75cc95803 (0 changed files — working tree is clean, nothing to analyze) ````` @@ -1159,7 +1141,7 @@ at: 1c08714cf *Pre-PR gate on a CLEAN tree: no obligations, exit 0.* -**wall time: 14.17s** +**wall time: 8.55s** ````` @@ -1171,12 +1153,12 @@ at: 1c08714cf *Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* -**wall time: 2.37s** +**wall time: 2.43s** ````` @@ -1354,7 +1336,7 @@ at: 1c08714cf *The same shape query WITH an explicit capture — identical hits, no auto_captured= attribute.* -**wall time: 5.18s** +**wall time: 5.04s** ````` @@ -1394,7 +1376,7 @@ at: 1c08714cf *Raw BM25 ranking (debug lens; --for is the real verb).* -**wall time: 1.95s** +**wall time: 2.11s** ````` @@ -1403,11 +1385,11 @@ at: 1c08714cf - + - + @@ -1421,12 +1403,12 @@ at: 1c08714cf - + - + … [28 more display lines; full output is 3359 bytes on 1 raw line(s)] ````` @@ -1439,10 +1421,10 @@ at: 1c08714cf *Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* -**wall time: 4.41s** +**wall time: 2.59s** ````` - + @@ -1456,8 +1438,8 @@ at: 1c08714cf What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) struct RankedGraph -inline std::uint32_t sliceRdIterationCeiling() noexcept -inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept struct PageRankRun inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) @@ -1472,14 +1454,14 @@ at: 1c08714cf ## Background — read these, in this order ## Design space and constraints COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] -… [106 more display lines; full output is 13658 bytes on 72 raw line(s)] +… [106 more display lines; full output is 13660 bytes on 72 raw line(s)] ````` ## `./build/ripwire . --pack-signatures --top-k=10` *Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* -**wall time: 2.22s** +**wall time: 1.26s** ````` @@ -1519,7 +1501,7 @@ at: 1c08714cf *Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* -**wall time: 2.77s** +**wall time: 1.47s** ````` @@ -1827,7 +1809,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *File->file dependency graph (god-files, cycles).* -**wall time: 2.40s** +**wall time: 2.63s** ````` @@ -1867,18 +1849,18 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Complexity x recent git churn (maintenance pain).* -**wall time: 4.49s** +**wall time: 4.55s** ````` - + - + @@ -1890,7 +1872,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + @@ -1907,11 +1889,11 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Token-normalized duplicate bodies.* -**wall time: 12.54s** +**wall time: 9.83s** ````` - + @@ -1947,41 +1929,41 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Files that change together in git (hidden coupling).* -**wall time: 4.64s** +**wall time: 5.03s** ````` - + - - - - - - + + + - + - - - + + + + + + - - + + - - - + + + ````` @@ -1989,26 +1971,26 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Hotspots scoped to RECENT churn (the regression lens).* -**wall time: 4.10s** +**wall time: 3.09s** ````` - + - - + + - + - + @@ -2016,7 +1998,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan - + @@ -2029,7 +2011,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* -**wall time: 2.66s** +**wall time: 1.94s** ````` @@ -2069,7 +2051,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* -**wall time: 9.45s** +**wall time: 7.75s** ````` @@ -2109,7 +2091,7 @@ ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchan *User lint rules (YAML, ast-grep style) from a directory.* -**wall time: 3.09s** +**wall time: 2.69s** ````` @@ -2136,7 +2118,7 @@ ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x *Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* -**wall time: 2.00s** +**wall time: 1.53s** Input file: @@ -2191,7 +2173,7 @@ The joined finding — past the display cut above, extracted so the join is visi *Cluster the call graph into cohesive modules.* -**wall time: 3.10s** +**wall time: 2.57s** ````` @@ -2231,6 +2213,8 @@ The joined finding — past the display cut above, extracted so the join is visi *Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* +**wall time: 2.88s** + ````` of ; levels_shown= of levels= printed; symbols= = isolated= + size= of all top_modules=. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). counts_floor=1: every count is a FLOOR, never a total. graph_ambiguous=/graph_unresolved=: resolver gauge. graph_unindexed=N: N files no grammar could read (the map header's unindexed=); their calls raise neither gauge. pr_iters=N: PageRank iterations. children=K: K child modules below the levels_shown= cut, unprinted. next=: the one pasteable follow-up. --> @@ -2309,7 +2293,7 @@ The joined finding — past the display cut above, extracted so the join is visi *Architecture summary (modules, god-files, cycles) as markdown.* -**wall time: 1.63s** +**wall time: 1.41s** ````` @@ -2349,7 +2333,7 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 *Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* -**wall time: 2.36s** +**wall time: 2.60s** ````` @@ -2389,7 +2373,7 @@ Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 *Module (directory) dependency graph as a Mermaid diagram.* -**wall time: 2.41s** +**wall time: 1.41s** ````` %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. @@ -2429,11 +2413,11 @@ flowchart LR *Bus-factor: recency-weighted author ownership per file.* -**wall time: 7.08s** +**wall time: 3.54s** ````` - + @@ -2469,7 +2453,7 @@ flowchart LR *Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* -**wall time: 2.88s** +**wall time: 2.49s** ````` @@ -2482,7 +2466,7 @@ flowchart LR *Which symbols a TEST FILE exercises — the reverse direction of --affected.* -**wall time: 2.20s** +**wall time: 1.93s** ````` @@ -2495,7 +2479,7 @@ flowchart LR *Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* -**wall time: 2.44s** +**wall time: 2.83s** ````` @@ -2508,11 +2492,11 @@ flowchart LR *On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* -**wall time: 71.17s** +**wall time: 55.48s** ````` - @@ -2536,7 +2520,7 @@ flowchart LR - + @@ -2548,17 +2532,18 @@ stderr: ````` ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: cache : over its 2048 MiB budget — evicted 373 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) ````` ## `./build/ripwire . --edit-check=rankGraphTeleport` *Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* -**wall time: 20.64s** +**wall time: 34.55s** ````` - @@ -2573,11 +2558,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* -**wall time: 2.31s** +**wall time: 4.03s** ````` - ````` @@ -2586,11 +2571,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* -**wall time: 5.67s** +**wall time: 8.23s** ````` - - (t=modscope): a file's MODULE SCOPE — where a top-level call and an anonymous callback body's calls live; a CALLER, never a callee, with no body to expand. --> + @@ -2619,22 +2604,22 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g -… [256 more display lines; full output is 17060 bytes on 1 raw line(s)] +… [283 more display lines; full output is 18391 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` *Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* -**wall time: 28.10s** +**wall time: 33.23s** ````` - - + + - + @@ -2646,11 +2631,11 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* -**wall time: 8.41s** +**wall time: 9.18s** ````` - + @@ -2679,18 +2664,18 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g -… [304 more display lines; full output is 37801 bytes on 1 raw line(s)] +… [304 more display lines; full output is 37803 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` *A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* -**wall time: 3.56s** +**wall time: 3.75s** ````` - + @@ -2713,15 +2698,15 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g *Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* -**wall time: 183.54s** +**wall time: 111.93s** ````` - + - - + + @@ -2746,14 +2731,14 @@ ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs g -… [60154 more display lines; full output is 7430341 bytes on 1 raw line(s)] +… [60909 more display lines; full output is 7506091 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` *A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* -**exit code: 1** — **wall time: 2.28s** +**exit code: 1** — **wall time: 4.56s** ````` (empty) @@ -2770,11 +2755,11 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* -**exit code: 2** — **wall time: 27.04s** +**exit code: 2** — **wall time: 66.00s** ````` - @@ -2810,11 +2795,11 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Which ref's tree defines or mentions SYM — HEAD first, then every local branch.* -**wall time: 33.01s** +**wall time: 44.36s** ````` - + @@ -2846,56 +2831,56 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer … [34 more display lines; full output is 13695 bytes on 1 raw line(s)] ````` -## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` +## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=defs` -*The whole hit list, references included: the page the default is measured against. The default lists only the definitions (listing="defs", the references counted in ) when that page is strictly shorter in bytes, and this page otherwise.* +*The definitions-only page asked for by name: every kind="def" row (here the first 60, most on branches, each carrying tip= and date=) and the references counted in . The default lists this page only when it is strictly shorter in bytes than the whole list (--whereis-listing=all) under the same row cap; on this symbol it is not, so the default above listed every hit.* -**wall time: 22.64s** +**wall time: 39.02s** ````` - - + + - - - - - - - - - - - - - - - - - - - - - - - - - -… [34 more display lines; full output is 13695 bytes on 1 raw line(s)] + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 18430 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` *Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* -**wall time: 29.68s** +**wall time: 27.84s** ````` - - + + @@ -2930,7 +2915,7 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* -**wall time: 2.47s** +**wall time: 1.21s** ````` @@ -2970,8 +2955,6 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* -**wall time: 1.32s** - ````` @@ -3005,7 +2988,7 @@ ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zer *Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* -**exit code: 1** — **wall time: 1.35s** +**exit code: 1** ````` (empty) @@ -3022,10 +3005,10 @@ ripwire: run `ripwire . --flags` for the gate table *NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* -**wall time: 2.92s** +**wall time: 2.40s** ````` -{"v":1,"verb":"plan-lanes","at":"1c08714cf","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":342 … [line truncated: 339 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"75cc95803","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":342 … [line truncated: 339 more bytes on this line] "symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, @@ -3055,14 +3038,14 @@ ripwire: run `ripwire . --flags` for the gate table {"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":8,"hotspot_rank":23}]},"blast_radius":{"reaches":51,"files_total":17,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editplan.h","./src/flipimpact.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./s … [line truncated: 203 more bytes on this line] "tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":51,"module_span":1,"notes":[], "execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"high","rule":"complex-or-wide","basis":"structural-only","signals":{"claims":13,"files":9,"module_span":1,"max_ccx":11,"sum_ccx":59,"ambiguous_calls":49,"blast_reaches":51,"contract_touches":0,"conflicts":0,"untested":51,"tests … [line truncated: 135 more bytes on this line] -… [77 more display lines; full output is 21301 bytes on 1 raw line(s)] +… [72 more display lines; full output is 20570 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --plan-lanes --brief=./../../tmp/lean-answers-068/showcase_scratch/aux/lanes_brief.txt` *NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* -**wall time: 2.23s** +**wall time: 1.66s** Input file: @@ -3073,11 +3056,11 @@ write regression tests for the new filter ````` ````` -{"v":1,"verb":"plan-lanes","at":"1c08714cf","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":34232,"ambiguous":10278,"unresolved":12860},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] +{"v":1,"verb":"plan-lanes","at":"75cc95803","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":34232,"ambiguous":10278,"unresolved":12860},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] "symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] {"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, {"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":640,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":308,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, {"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, {"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":12,"cx":14,"ccx":15,"churn":36,"tested":0}, {"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":12,"cx":26,"ccx":38,"churn":36,"tested":0}, @@ -3086,20 +3069,20 @@ write regression tests for the new filter {"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, {"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":67,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, {"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], -"files":[{"p":"./src/cli.h","symbols":1,"churn":434,"ccx":0,"hotspot_rank":6}, +"files":[{"p":"./src/cli.h","symbols":1,"churn":435,"ccx":0,"hotspot_rank":6}, {"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":31}, {"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":87}, {"p":"./src/mcprefusal.h","symbols":1,"churn":40,"ccx":0,"hotspot_rank":55}, {"p":"./src/mcpverbs.h","symbols":1,"churn":308,"ccx":0,"hotspot_rank":5}, {"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":52}, {"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":4,"hotspot_rank":23}], -"symbols_total":3137,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"symbols_total":3146,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], "tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":12,"module_span":6,"notes":[], "execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":7,"module_span":6,"max_ccx":38,"sum_ccx":62,"ambiguous_calls":26,"blast_reaches":12,"contract_touches":1,"conflicts":3,"untested":12,"t … [line truncated: 139 more bytes on this line] {"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":107,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] -{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2825,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, -{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, -{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2904,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2825,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2904,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":435,"tested":0}, {"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":107,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, {"p":"./src/slice.h","n":"sliceRdIterationCeiling","scope":"slicev","key":"449132b302bb4324","id":"./src/slice.h::slicev::sliceRdIterationCeiling","id_addressable":true,"id_collides_with":0,"l":1372,"ord":0,"overloads":1,"amb":0,"cx":9,"ccx":12,"churn":51,"tested":0}, {"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, @@ -3126,8 +3109,6 @@ ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out *CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* -**wall time: 1.44s** - ````` @@ -3184,24 +3165,22 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped *Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* -**wall time: 1.15s** - ````` - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -3217,31 +3196,31 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped -… [243 more display lines; full output is 25193 bytes on 1 raw line(s)] +… [243 more display lines; full output is 25197 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --doc-drift --gateability` *The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* -**wall time: 1.19s** +**wall time: 1.02s** ````` - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -3257,7 +3236,7 @@ ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped -… [269 more display lines; full output is 26392 bytes on 1 raw line(s)] +… [269 more display lines; full output is 26396 bytes on 1 raw line(s)] ````` Tail of the same output — the `` section: @@ -3294,25 +3273,25 @@ Tail of the same output — the `` section: *Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* -**wall time: 2.06s** +**wall time: 1.20s** ````` - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -3327,7 +3306,7 @@ Tail of the same output — the `` section: -… [221 more display lines; full output is 25107 bytes on 1 raw line(s)] +… [221 more display lines; full output is 25111 bytes on 1 raw line(s)] ````` ## `./build/ripwire . --from-trace=-` @@ -3409,8 +3388,6 @@ AddressSanitizer:DEADLYSIGNAL *ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* -**wall time: 1.12s** - ````` @@ -3506,8 +3483,8 @@ AddressSanitizer:DEADLYSIGNAL What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) struct RankedGraph -inline std::uint32_t sliceRdIterationCeiling() noexcept -inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept struct PageRankRun inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) @@ -3522,14 +3499,14 @@ AddressSanitizer:DEADLYSIGNAL ## Background — read these, in this order ## Design space and constraints COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] -… [60 more display lines; full output is 9335 bytes on 10 raw line(s)] +… [60 more display lines; full output is 9337 bytes on 10 raw line(s)] ````` ## `./build/ripwire . --export=cc.json:./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json` *Per-file metrics as CodeCharta cc.json.* -**wall time: 1.91s** +**wall time: 1.05s** ````` (empty) @@ -3546,7 +3523,7 @@ Artifact written: *One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* -**wall time: 5.91s** +**wall time: 2.11s** Input file: @@ -3561,7 +3538,7 @@ lego:Vehicle - + @@ -3574,8 +3551,8 @@ lego:Vehicle Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) -struct FileHealth -inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +struct FileHealth inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vec… inline const McpIndex& getIndex( const std::string& root ) inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) @@ -3600,17 +3577,17 @@ lego:Vehicle *Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* -**exit code: 1** — **wall time: 6.80s** +**exit code: 1** — **wall time: 4.30s** ````` - - + - - + + - + @@ -3663,13 +3640,13 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s *Rank by git change-frequency prior instead of PageRank.* -**wall time: 2.63s** +**wall time: 2.44s** ````` - + @@ -3689,34 +3666,34 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s ````` -## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` *Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* -**wall time: 1.12s** +**wall time: 1.22s** ````` - - + + + + + - - - - - - + + + - + + + - - - + ````` @@ -3740,7 +3717,7 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf *Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* -**wall time: 1.09s** +**wall time: 1.29s** ````` @@ -3760,24 +3737,24 @@ ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf *Flat top-K export for an external reranker.* -**wall time: 1.20s** +**wall time: 1.55s** ````` - + inline std::string spanTierMemoPath( const std::string& diskPath ) - + inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) - + ### See the map — not just the numbers - + inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) - + const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) @@ -3833,15 +3810,15 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Pagination: 3 items, skipping the first 3 (deterministic seams).* -**wall time: 2.79s** +**wall time: 2.47s** ````` - + - + ````` @@ -3877,7 +3854,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Drop matching paths (repeatable) before ranking.* -**wall time: 1.14s** +**wall time: 1.13s** ````` @@ -3907,13 +3884,13 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* -**wall time: 1.06s** +**wall time: 1.59s** ````` - + @@ -3937,7 +3914,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* -**wall time: 5.12s** +**wall time: 4.20s** ````` @@ -3959,7 +3936,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m *Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* -**wall time: 5.97s** +**wall time: 4.93s** ````` @@ -3980,7 +3957,7 @@ ripwire: --json is not yet supported for --hotspots — supported: the default m Artifact written: ````` - 18642990 ./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache + 18643301 ./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache ````` ## `./build/ripwire . --max-file-size=8K --top-k=3` @@ -4027,7 +4004,7 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Multi-root workspace: ONE merged graph over two roots, paths labeled /.* -**wall time: 7.01s** +**wall time: 4.10s** ````` @@ -4059,17 +4036,17 @@ ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather t *Self-eval: co-change recall vs BM25.* -**wall time: 54.45s** +**wall time: 30.20s** ````` ripwire --eval (co-change recovery, averaged over 80 historical commits) ranker recall@5 recall@10 recall@20 - ripwire 4.1% 8.5% 11.8% + ripwire 4.1% 8.5% 12.3% BM25 12.8% 15.0% 19.5% - BM25sub 17.5% 23.3% 30.1% - BM25body 13.8% 25.9% 44.0% - fused 14.3% 17.1% 29.1% - anchored 13.8% 25.9% 44.0% + BM25sub 17.3% 23.1% 30.6% + BM25body 13.7% 26.2% 44.2% + fused 14.2% 17.6% 29.4% + anchored 13.7% 26.2% 44.2% same-dir 0.0% 8.8% 13.1% random 0.2% 0.4% 0.8% <- floor (random ranking over F=2484 files) note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not @@ -4087,7 +4064,7 @@ ripwire --eval (co-change recovery, averaged over 80 historical commits) *Known-item retrieval eval: MRR + recall@k per ranker per query mode.* -**wall time: 182.66s** +**wall time: 151.77s** ````` ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) @@ -4096,11 +4073,11 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) ranker query-mode MRR recall@1 recall@5 recall@10 - subtoken name 0.694 56.2% 85.8% 90.8% + subtoken name 0.693 56.1% 85.7% 90.8% subtoken doc-phrase 0.920 90.0% 94.0% 94.9% name-exact name 0.905 83.5% 96.1% 97.4% name-exact doc-phrase 0.013 0.5% 1.8% 2.6% - anchored name 0.699 57.8% 85.2% 89.2% + anchored name 0.699 57.6% 85.2% 89.2% anchored doc-phrase 0.917 89.5% 94.0% 94.5% routed name 0.908 83.7% 96.3% 97.7% routed doc-phrase 0.919 90.0% 93.8% 94.7% @@ -4116,7 +4093,7 @@ ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-co *Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* -**exit code: 3** — **wall time: 14.81s** +**exit code: 3** — **wall time: 9.64s** Input file: @@ -4226,7 +4203,7 @@ Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after th *Version + short build info.* ````` -ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=1c08714cf) +ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=75cc95803) ````` @@ -4238,8 +4215,6 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=1c08 *Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* -**wall time: 1.96s** - ````` @@ -4251,8 +4226,6 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=1c08 *The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* -**wall time: 1.75s** - ````` @@ -4270,7 +4243,7 @@ ripwire 0.6.5 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=1c08 *A seed past the end of the file — the refusal shape for a faulted location.* -**exit code: 1** — **wall time: 1.89s** +**exit code: 1** ````` (empty) @@ -4286,8 +4259,6 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. *VERIFY a closed claim in one call: three-valued verdict (confirmed / refuted / not-established) with the evidence rows inline.* -**wall time: 1.23s** - ````` @@ -4300,8 +4271,6 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. *A claim that is FALSE — the refuted shape, with the references that refute it.* -**wall time: 4.23s** - ````` @@ -4321,8 +4290,6 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. *A literal-scan absence: refuted only with complete= evidence, never on a partial scan.* -**wall time: 2.50s** - ````` @@ -4333,7 +4300,7 @@ ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph. *An unparseable claim — the refusal names the accepted shapes.* -**exit code: 1** — **wall time: 2.81s** +**exit code: 1** ````` (empty) @@ -4349,11 +4316,9 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* -**wall time: 2.53s** - ````` - + @@ -4375,11 +4340,9 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* -**wall time: 2.11s** - ````` - + @@ -4399,11 +4362,9 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* -**wall time: 2.51s** - ````` - + @@ -4432,11 +4393,9 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *Forward flow: which statements the seed's value reaches, at the default depth bound.* -**wall time: 2.87s** - ````` - + @@ -4477,7 +4436,7 @@ ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. *A variable the definition does not bind — the refusal shape, naming the inventory.* -**exit code: 1** — **wall time: 2.82s** +**exit code: 1** ````` (empty) @@ -4509,11 +4468,11 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* -**wall time: 1.97s** +**wall time: 1.03s** ````` - + @@ -4533,7 +4492,7 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* -**wall time: 5.03s** +**wall time: 1.81s** ````` @@ -4546,7 +4505,7 @@ ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire < *The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* -**wall time: 3.91s** +**wall time: 2.01s** ````` @@ -4602,12 +4561,10 @@ ripwire: --pattern: this pattern collapses to a single bare token, which is a TE *Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default).* -**wall time: 1.90s** - ````` @@ -4870,8 +4819,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() struct FileHealth -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -enum class AstWalk : std::uint8_t +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… struct SliceScan inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept constexpr std::array<LangEntry, 51> kLangTable = @@ -4883,8 +4832,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? bool jsonNestsTooDeep( std::string_view bytes ) noexcept constexpr std::uint32_t kMaxYamlNestDepth = 64u SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline bool hasMainGuard( std::string_view source ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) struct LintRule enum class DisclosureWhy : std::uint8_t @@ -4892,14 +4841,54 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? … [55 more display lines; full output is 8839 bytes on 1 raw line(s)] ````` +## `./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose` + +*Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* + +**wall time: 1.04s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… +struct SliceScan +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::array<LangEntry, 51> kLangTable = +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [66 more display lines; full output is 9172 bytes on 1 raw line(s)] +````` + ## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` *Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* -**wall time: 3.28s** +**wall time: 1.07s** ````` - + @@ -4910,8 +4899,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() struct FileHealth -inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… -enum class AstWalk : std::uint8_t +enum class AstWalk : std::uint8_t +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… struct SliceScan inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept constexpr std::array<LangEntry, 51> kLangTable = @@ -4923,30 +4912,30 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? bool jsonNestsTooDeep( std::string_view bytes ) noexcept constexpr std::uint32_t kMaxYamlNestDepth = 64u SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) -inline void runParseWorker( ParsePoolShared& sh, unsigned t ) -inline bool hasMainGuard( std::string_view source ) +inline bool hasMainGuard( std::string_view source ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) struct LintRule enum class DisclosureWhy : std::uint8_t inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) -… [90 more display lines; full output is 12414 bytes on 55 raw line(s)] +… [90 more display lines; full output is 12411 bytes on 55 raw line(s)] ````` ## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` *Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* -**wall time: 4.46s** +**wall time: 1.39s** ````` - + inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… struct DeltaBasis @@ -4957,8 +4946,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -struct QualityDeltaOutcome +struct QualityDeltaOutcome +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) inline std::string qualityAcksPath( const std::string& root ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at inline std::string gitHeadSha( const std::string& root ) @@ -4976,17 +4965,17 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* -**wall time: 3.86s** +**wall time: 1.24s** ````` - + inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) - +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) -std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] H10 (capture-audit 2026-09-04): the --quality-ack run that has NOTHING to accept. It used to re-…int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… struct DeltaBasis @@ -4997,8 +4986,8 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) -inline bool atomicWriteFile( const std::string& path, const std::string& blob ) -struct QualityDeltaOutcome +struct QualityDeltaOutcome +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) inline std::string qualityAcksPath( const std::string& root ) inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at inline std::string gitHeadSha( const std::string& root ) @@ -5016,8 +5005,6 @@ ripwire: --grep given twice; did you mean --grep='A' --and='B'? *"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* -**wall time: 1.32s** - ````` disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* -**wall time: 5.21s** +**wall time: 1.64s** ````` - + - + - - - + + + + ````` @@ -5075,19 +5062,19 @@ ripwire: --safe-delete symbol not found: DoesNotExist *The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* -**wall time: 4.62s** +**wall time: 2.11s** ````` - + - + + ````` @@ -5096,8 +5083,6 @@ ripwire: --safe-delete symbol not found: DoesNotExist *WHY a file is not in the index (oversize / excluded / unsupported-ext / gitignored) and which indexed files it cannot vouch for (degraded-parse, minified-suspect), plus the per-language census.* -**wall time: 1.13s** - ````` @@ -5136,8 +5121,6 @@ ripwire: --safe-delete symbol not found: DoesNotExist *Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied).* -**wall time: 1.95s** - ````` @@ -5158,8 +5141,6 @@ ripwire: --safe-delete symbol not found: DoesNotExist *--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of.* -**wall time: 2.02s** - ````` @@ -5186,13 +5167,13 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o *EXEC-MODE --from-trace: run a command, and on a non-zero exit map its captured output onto indexed symbols in the same call — the whole fix-loop entry. ripwire exits 4 here because the wrapped command failed, which is the signal, not an incident.* -**exit code: 4** — **wall time: 1.77s** +**exit code: 4** ````` - + - + ````` @@ -5248,12 +5227,12 @@ ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable o *A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* -**exit code: 4** — **wall time: 3.11s** +**exit code: 4** — **wall time: 2.55s** ````` - + ````` @@ -5288,7 +5267,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* -**wall time: 14.75s** +**wall time: 5.06s** ````` @@ -5328,8 +5307,6 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Per-function size ranking, largest Halstead volume/token entropy/lines first — a size proxy, not a readability order (withdrawn, docs/EVALS.md §8) — a RANKING lens, not a grade.* -**wall time: 2.73s** - ````` @@ -5348,8 +5325,6 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Functions WITH a doc comment, most name-restating first: c_coeff (high = the comment repeats the name) and cic (Jaccard of comment vs identifier vocabulary), both reported, never collapsed.* -**wall time: 1.77s** - ````` @@ -5368,8 +5343,6 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The local-reasoning lens: to understand this symbol, how much must you know that is NOT in front of you (ent_ratio= edge share, read_ratio= token-weighted).* -**wall time: 2.66s** - ````` over rows. window: shown= total= capped= has_more= next_offset= offset= limit= (capped=1 cut; next_offset= pastes as offset=). cells_capped=: 1 = cut. counts_floor=1: every count is a FLOOR, never a total. graph_ambiguous=/graph_unresolved=: resolver gauge. graph_unindexed=N: N files no grammar could read (the map header's unindexed=); their calls raise neither gauge. root=: p= relative to it. cells=N: mutable non-local cells found in the corpus (globals, statics, Python module globals; FLOOR). functions=N: functions reaching at least one cell, directly or via callees (all rows, before paging). direct_writes=/direct_reads=: the writes=/reads= cells this function's OWN body writes/reads. cells_total=N: distinct cells reached (read and written counts once); at most 12 cell rows print. at=/at_dir=: one own-body use site (may be more) and what own-body sites do (can be narrower than dir=). cells_shown=/cells_capped=1: only this many of cells_total= cell rows print (cap 12). unanalyzed_langs=/unanalyzed_files=: indexed languages (and their files) this lens skips; NOT zero cells. --> @@ -5448,7 +5421,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* -**wall time: 6.39s** +**wall time: 2.64s** ````` @@ -5478,7 +5451,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir - + … [35 more display lines; full output is 7334 bytes on 1 raw line(s)] @@ -5488,7 +5461,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* -**wall time: 4.21s** +**wall time: 1.63s** ````` @@ -5528,7 +5501,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* -**wall time: 3.53s** +**wall time: 1.58s** ````` @@ -5564,8 +5537,6 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename).* -**wall time: 1.52s** - ````` @@ -5604,11 +5575,11 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir *Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* -**wall time: 30.62s** +**wall time: 9.75s** ````` - + @@ -5619,7 +5590,7 @@ ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwir

@@ -2992,8 +2971,8 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; only these gate (when major). new-symbol=N: regressions on NEW code; never gate, but the debt is yours: read them. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. r surface=: the api-surface tier, new-symbol or contract-change. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. r sev=minor: a small numeric delta, counted in minor=, never gating (absent: major). r origin=new-symbol: the finding is on NEW code, never gating (absent: preexisting-worse). sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). r members=/tokens=: a duplication row's clone group (member ids) / their shared normalized-token count. --> + @@ -3022,15 +3001,15 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - + + - - - - - - + + + + + + ``` @@ -3053,7 +3032,7 @@ disclosed as trailing_newline_folded=true; a second one, a deliberate blank line _An unknown TARGET refuses and leaves every file byte-identical._ ``` -$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h +$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h (empty) ``` @@ -3072,16 +3051,17 @@ $ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/ _Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._ ``` -$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h +$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, -{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3095,13 +3075,14 @@ Both inserts PAD the block (newlines only, never removed) so it is separated fro _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call._ ``` -$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h +$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, -{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3121,7 +3102,7 @@ empty payloads refuse, never imply deletion _An unknown TARGET refuses and leaves every file byte-identical._ ``` -$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h +$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h (empty) ``` @@ -3142,16 +3123,17 @@ RELATIVE (matched against the indexed spelling) or ABSOLUTE (matched against the _Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._ ``` -$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h +$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --edit-target-file=src/infra/sortutil.h {"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, -{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":729,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3173,7 +3155,7 @@ pass this when you are about to edit again immediately). The MCP spelling is pos _The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr._ ``` -$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check +$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=./../../tmp/lean-answers-068/showcase_scratch/aux/payload_note.h --no-post-check {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] ``` @@ -3194,7 +3176,7 @@ op is one of replace_symbol_body, insert_before_symbol, insert_after_symbol each _Neither --dry-run nor --apply: the mode is explicit, so this refuses._ ``` -$ ./build/ripwire . --edit-plan=/aux/edit_plan.json +$ ./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json (empty) ``` @@ -3209,12 +3191,12 @@ exactly one of the two is required the plan's explicit mode: --dry-run preflight _The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure._ ``` -$ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multi … [line truncated: 536 more bytes on this line] +$ ./build/ripwire . --edit-plan=./../../tmp/lean-answers-068/showcase_scratch/aux/edit_plan.json --apply +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mul … [line truncated: 538 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true}}]} +"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3258,20 +3240,20 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - - - - - - - - - - - - - - + + + + + + + + + + + + + + ... [3 more line(s); run it to see the whole thing] ``` @@ -3295,21 +3277,21 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - - - - + + + + + + teleport( pw.begin(), pw.end() );]]> - + - + - - -... [23 more line(s); run it to see the whole thing] +... [22 more line(s); run it to see the whole thing] ``` **Shaped by:** `--slice-depth` @@ -3350,10 +3332,9 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:5565 - - - +$ ./build/ripwire . --at=src/graph.h:5567 + + ``` @@ -3377,8 +3358,8 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - - + + ``` @@ -3403,12 +3384,12 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - - + + + - + @@ -3458,7 +3439,7 @@ Each line is ranked on its own — no community carve, no bin packing — so the _NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode)._ ``` -$ ./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt +$ ./build/ripwire . --plan-lanes --brief=./../../tmp/lean-answers-068/showcase_scratch/aux/lanes_brief.txt add a --since filter to the doc-drift verb add the CLI parse arm and help text for the new filter write regression tests for the new filter @@ -3478,13 +3459,22 @@ Per local ref (SUBSTR filters ref names): the lines its own divergent work AUTHO **Try it** -_Which refs of the one branch `main` (no ref family left to select on this checkout) still hold divergent authored work vs HEAD, with verdicts._ +_Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts._ ``` -$ ./build/ripwire . --stray-content=main - - - +$ ./build/ripwire . --stray-content=lane/ + + + + + + + + + + + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--plan`, `--abi`, `--whereis`, `--limit`, `--eval-stray` @@ -3503,13 +3493,25 @@ $ ./build/ripwire . --stray-content=main **Try it** -_Select the genuinely-unmerged refs of the one branch `main` (no ref family left to select on this checkout) and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root)._ +_Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root)._ ``` -$ ./build/ripwire . --stray-content=main --plan - - - +$ ./build/ripwire . --stray-content=lane/ --plan + + + + + + + + + + + + + + +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--stray-content`, `--legend`, `--json` @@ -3527,13 +3529,25 @@ SCOPE is what each ref AUTHORED — the paths `git diff base..tip` reports again **Try it** -_Cross-branch ABI-break gate over the one branch `main` (no ref family left to select on this checkout): struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all._ +_Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all._ ``` -$ ./build/ripwire . --stray-content=main --abi - - - +$ ./build/ripwire . --stray-content=lane/ --abi + + + + + + + + + + + + + +... [17 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -3546,7 +3560,7 @@ $ ./build/ripwire . --stray-content=main --abi **Answers:** find which branch's tree defines or mentions SYM, HEAD first which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch. -Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page is strictly shorter than listing every hit; otherwise every hit is listed (see --whereis-listing). +Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. A checkout that differs from HEAD is read from disk: each changed path's rows say ref="worktree" and replace HEAD's, at= gains +dirty, and worktree= says whether every one was read. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR. LISTING: by default only the kind="def" rows are listed and the kind="ref" rows counted in one element, when that page is strictly shorter than the --whereis-listing=all page; otherwise every hit is listed. **Try it** @@ -3554,13 +3568,13 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - - + + + + + + element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. +The default lists defs when that page is strictly shorter in bytes than all under the same row cap, else all (a tie lists all). defs lists every kind="def" row and COUNTS the kind="ref" rows in one element whose next= lists exactly them (refs). kind="def" is the parser's label: a definition it does not model (define_method, setattr, a name bound by assignment) is a counted ref. With no ref row, or no def row (the mentions are then the answer), every hit is listed and the root carries no listing=. all lists every row, the whole hit list. shown=/capped= and --limit/--offset window the LISTED rows; hits= counts every row. Refused without --whereis, and on an unknown value. + +**Try it** + +_The whole hit list, references included: the page the default is measured against. The default lists only the definitions (listing="defs", the references counted in ) when that page is strictly shorter in bytes, and this page otherwise._ + +``` +$ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all + + + + + + + - + @@ -3679,9 +3709,9 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - - + + @@ -3714,18 +3744,18 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - + + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3749,18 +3779,18 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - - - - - - - - - + + + + + + + + + + + + ... [19 more line(s); run it to see the whole thing] ``` @@ -3783,19 +3813,19 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - - - - - - - - - + + + + + + + + + + + + + ... [18 more line(s); run it to see the whole thing] ``` @@ -3819,8 +3849,8 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - - + + @@ -3855,10 +3885,10 @@ $ ./build/ripwire . --from-trace=- AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:3949 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:3447 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1441 - #3 0x102e01a44 in main src/main.cpp:3475 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:5567 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 + #3 0x102e01a44 in main src/main.cpp:3975 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -3883,9 +3913,9 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc ``` $ ./build/ripwire . --run-trace="true" - - - + + + ``` @@ -3944,12 +3974,13 @@ _List all field notes (write-side memory) — the committed .ripwire_notes at th ``` $ ./build/ripwire . --notes - - + + + - @@ -3957,8 +3988,7 @@ $ ./build/ripwire . --notes gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> - -... [1 more line(s); run it to see the whole thing] +... [2 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-redact` @@ -3975,19 +4005,21 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - - - + + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept - -ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 104 more bytes on this line] - -knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedBy makes in full above, which applies here verbatim: firstFlagOutside() walks the rows parseArgs itself minline constexpr std::string_view kInRideAlong[] = - -F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexcept -... [19 more line(s); run it to see the whole thing] + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--top-k`, `--token-budget`, `--for`, `--test-gate`, `--expand`, `--compress`, `--no-redact`, `--partition` @@ -4010,21 +4042,19 @@ _Fan-out form: one shared core + 3 per-agent slices carved along call-graph comm ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3 - - - + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> + + + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept - -ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const std::vector<std::string>& verbLines… - -knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedB…inline constexpr std::string_view kInRideAlong[] = - -... [17 more line(s); run it to see the whole thing] + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const WrapListing& listing ) noexcept +... [19 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4045,19 +4075,18 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - - - - - + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) - -What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure - -What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph - -L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… -... [19 more line(s); run it to see the whole thing] + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + +... [20 more line(s); run it to see the whole thing] ``` **Shaped by:** `--partition` @@ -4073,7 +4102,7 @@ feeds a CodeCharta 3D city _Per-file metrics as CodeCharta cc.json._ ``` -$ ./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json +$ ./build/ripwire . --export=cc.json:./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json (empty) ``` @@ -4090,10 +4119,10 @@ caps at 16 (over-cap = capped=1). THE SAME `verb:arg` line is accepted by the MC _One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped ._ ``` -$ ./build/ripwire . --batch=/aux/batch2.txt +$ ./build/ripwire . --batch=./../../tmp/lean-answers-068/showcase_scratch/aux/batch2.txt for:incremental cache invalidation callers:rankGraphTeleport -grep:DEGRADED_PATH_ALERT +grep:DISCLOSE lego:Vehicle ``` @@ -4120,16 +4149,17 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - - - - - - - + + + + + + + ``` @@ -4153,20 +4183,20 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - - - - - - - + + + + + + + - - +... [2 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4185,11 +4215,11 @@ _WHY a file is not in the index (oversize / excluded / unsupported-ext / gitigno ``` $ ./build/ripwire . --skipped - - + + - @@ -4226,7 +4256,9 @@ _Scan a single skill file for injection/exfiltration patterns before installing. ``` $ ./build/ripwire --scan-skill=skills/ripwire-orient/SKILL.md - + + + ``` **Shaped by:** `--scan-skills` @@ -4243,7 +4275,9 @@ _Scan a whole skills directory (exit 2 = CRITICAL, 1 = WARN). Explicit-DIR form ``` $ ./build/ripwire --scan-skills=skills - + + + ``` **Caveats (stated by the binary):** @@ -4271,21 +4305,21 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - - - - - - - - - - - + + + + + + + + - + -... [16 more line(s); run it to see the whole thing] + + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--since`, `--in` @@ -4306,22 +4340,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~7 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - - - - - - - -... [16 more line(s); run it to see the whole thing] +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 + + + + + + + + + + + + + + +... [8 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4378,19 +4412,25 @@ full restores the prose output legend posture for EVERY XML verb. The DEFAULT is **Try it** -_The same gating report under --legend=compact — same rows, same exit 2, schema="ripwire.quality-delta/v1", ~4 KB of legend down to one comment: the shape an agent's edit loop should run._ +_The grep compact legend (ripwire.grep/v1)._ ``` -$ ./build/ripwire . --quality-delta --legend=compact - - - - - - - -... [23 more line(s); run it to see the whole thing] +$ ./build/ripwire . --grep=DISCLOSE --legend=compact + - - - + + - + - - + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4497,21 +4537,21 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - - - - - - - - - + + + + - + - + + + -... [10 more line(s); run it to see the whole thing] + + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--skipped`, `--in`, `--index-out` @@ -4528,21 +4568,21 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - - - - - - - - - - + + + + - + - -... [11 more line(s); run it to see the whole thing] + + + + + + + +... [7 more line(s); run it to see the whole thing] ``` **Shaped by:** `--json`, `--limit` @@ -4561,22 +4601,20 @@ $ ./build/ripwire . --map-diff --top-k=5 _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` -$ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - - - - - - - - - +$ ./build/ripwire . --cache=./../../tmp/lean-answers-068/showcase_scratch/aux/warm2.ripwirecache --top-k=3 + + + + - + - + + + -... [2 more line(s); run it to see the whole thing] + + ``` **Shaped by:** `--doctor`, `--index-out` @@ -4592,7 +4630,7 @@ Writes BOTH families — BASE.lean.ripwirecache (map/ nav/--pr-context) and BASE _CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout._ ``` -$ ./build/ripwire . --index-out=/aux/ci_index +$ ./build/ripwire . --index-out=./../../tmp/lean-answers-068/showcase_scratch/aux/ci_index (empty) ``` @@ -4612,21 +4650,19 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co ``` $ ./build/ripwire . --no-cache --top-k=3 - - - - - - - - - + + + + - + - + + + -... [2 more line(s); run it to see the whole thing] + + ``` **Shaped by:** `--doctor` @@ -4643,21 +4679,19 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses ``` $ ./build/ripwire . --no-ignore --top-k=3 - - - - - - - - - + + + + - + - + + + -... [2 more line(s); run it to see the whole thing] + + ``` **Caveats (stated by the binary):** @@ -4677,21 +4711,21 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea ``` $ ./build/ripwire . --max-file-size=8K --top-k=3 - - - - - - - - - - - + + + + + + -... [8 more line(s); run it to see the whole thing] + + + + + +... [3 more line(s); run it to see the whole thing] ``` **Shaped by:** `--skipped` @@ -4756,22 +4790,20 @@ Under --scip it also writes the index's covered sites, so a precision join needs _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target._ ``` -$ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - - - - - - - - - +$ ./build/ripwire . --pin-census=./../../tmp/lean-answers-068/showcase_scratch/aux/pin_census.tsv --top-k=3 + + + + - + - + + + -... [2 more line(s); run it to see the whole thing] + + ``` ### `--mcp` @@ -4786,11 +4818,11 @@ _initialize + tools/list: the manifest an agent host loads at session start — ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp -{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a symbol; run edit_c … [line truncated: 368 more bytes on this line] +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] -{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1111 more bytes on this line] -{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 874 more bytes on this line] -... [28 more line(s); run it to see the whole thing] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +... [30 more line(s); run it to see the whole thing] ``` **Shaped by:** `--no-stable`, `--no-redact`, `--agent`, `--mcp-tools`, `--lsp`, `--listen` @@ -4806,6 +4838,19 @@ $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jso later answers list rows first, carry each definition only the first time the session meets it, and end with (the first of them also carries the core). inline: every answer keeps its legend until the client reads ripwire://legend-dict. legend:"compact"/"full" on a call keeps that answer inline either way. Stdio only. +**Try it** + +_--mcp-legend=inline: every answer of the stdio session carries its own legend (the default, session, sends each definition once per session after the first answer)._ + +``` +$ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [800 more display lines; full output is 22162 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --top-k=0 --expand=rankGraphTeleport` + +*NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* + +**wall time: 1.97s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --token-budget=100` + +*GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* + +**exit code: 3** — **wall time: 1.66s** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=8928 > budget=100 +````` + +## `./build/ripwire . --for="incremental cache invalidation when a file content hash changes"` + +*The task lens: ranked signatures + quality metrics framed for the task.* + +**wall time: 10.39s** + +````` + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex + +the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no gra…inline const McpIndex& getIndex( const std::string& root ) +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFileScan& … [line truncated: 12 more bytes on this line] +inline std::uint64_t blobChecksum( std::string_view s ) noexcept +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… +inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes, const s … [line truncated: 16 more bytes on this line] +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, … [line truncated: 54 more bytes on this line] +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +struct IngestFileScan +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t>& f … [line truncated: 17 more bytes on this line] +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, cons … [line truncated: 91 more bytes on this line] +inline std::string spanTierMemoPath( const std::string& diskPath ) +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +inline NodeId resolveHandleAll( const McpIndex& ix, std::uint64_t idHash, std::vector< NodeId >& matches ) +… [46 more display lines; full output is 9110 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +**wall time: 3.88s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { +… [12 more display lines; full output is 4288 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --no-route` + +*Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* + +**wall time: 1.51s** + +````` + + + + + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + +L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +struct RankedGraph +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago", bo … [line truncated: 11 more bytes on this line] +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… +inline std::string didYouMean( const IngestResult& ing, std::string_view name ) +inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = nullptr ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double& … [line truncated: 37 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = nullptr )< … [line truncated: 3 more bytes on this line] +struct DataSectionsCut +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### 2. Gates for A +#### The convergence disclosure contract +… [102 more display lines; full output is 14999 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` + +*T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* + +**wall time: 1.88s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --adaptive` + +*Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* + +**wall time: 1.61s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) + +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar… +enum class AstWalk : std::uint8_t +struct SliceScan +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::array<LangEntry, 51> kLangTable = +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& keptBy … [line truncated: 9 more bytes on this line] +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +inline bool hasMainGuard( std::string_view source ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +struct LintRule +enum class DisclosureWhy : std::uint8_t +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [55 more display lines; full output is 8943 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` + +*Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* + +**wall time: 1.89s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct LensRanking +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] +struct CandidateProvenance +std::optional<int> runEvalViews( const MainDispatch& d ) +inline constexpr int kLexWeightDoc = 2 +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +… [56 more display lines; full output is 9311 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` + +*Same task with the anchor disabled — the contrast the flag exists for.* + +**wall time: 1.45s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct LensRanking +struct NameCorpusStats +inline bool hasIdentifierShape( std::string_view w ) noexcept +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the W … [line truncated: 57 more bytes on this line] +struct CandidateProvenance +std::optional<int> runEvalViews( const MainDispatch& d ) +inline constexpr int kLexWeightDoc = 2 +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +… [58 more display lines; full output is 9391 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lego=Vehicle` + +*Interface -> implementors view: every existing impl of the named interface; the method contract is extracted for the C-family/Java/TS/Python tiers — for a Rust trait (this fixture) it discloses caveat="not-extracted-for-lang" rather than an empty list.* + +**wall time: 1.57s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --exemplar="format byte sizes for humans"` + +*The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* + +**wall time: 1.58s** + +````` + + + + + f, A&&... a ) +{ + // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; + // `{}` on a char[N] is a different question that library versions answer differently, and an + // implementation that formats the ARRAY emits the trailing NUL and the uninitialised bytes after it. + // A regex sweep missed sites twice, so the compiler enforces it instead of a reviewer. + // Only a MUTABLE char[N] is rejected. A string literal is const char[N]; every implementation formats + // that as a string, and the hazard here is the reusable buffer that was written short. + static_assert( ( ... && !( std::is_array_v> + && !std::is_const_v>> ) ), + "pass rw::cstr( buf ) for a fixed char buffer: {} on a char[N] is not printf's %s" ); + std::fputs( std::format( f, std::forward( a )... ).c_str(), stream ); +}]]>[[nodiscard]] const char16_t* c_str() const noexcept +````` + +## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` + +*Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* + +**wall time: 1.12s** + +````` + + + + +ripwire '.' --verify='calls(runDefaultMap, rankGraphTeleport)' + + +````` + +## `./build/ripwire . --help-task="write a cheerful release announcement"` + +*The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* + +**wall time: 1.09s** + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +**wall time: 2.14s** + +````` +ripwire recall — "quality delta gating exit codes" — 113 relevant of 210 document files, best-first — total=113 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2161 est_tokens=5489 + +━━ README.md (relevance 7.995) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 213934 B; lines="2707-2719,2720-2727"; dropped_by_budget=23] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | +| 5 | The memory guard stopped the run: over `--max-memory` (default 65% of the machine's memory) with no partial answer possible, or a verb other than the default map facing a partial index. One stderr line names the limit and the override. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ prompts/help-wanted/quality-delta-unchanged-tree-zero.md (relevance 7.345) ━━ [sections: 1 of 18 selected (21 in doc), section-granular; whole doc 25463 B; lines="133-158"; dropped_by_budget=17] +### Root spelling (maintainer-owned; reproduce it once so you recognise it) + +```bash +… [159 more lines, 14007 bytes total] +````` + +## `./build/ripwire ./../../tmp/lean-answers-068/showcase_scratch/aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` + +*The directory-as-knowledge-base pattern: --recall pointed at a 1588991-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* + +````` +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 + +━━ commands.md (relevance 10.658) ━━ [sections: 1 of 158 selected (187 in doc), section-granular; whole doc 721575 B; lines="515-540"; dropped_by_budget=157] [truncated: 1854 of 4242 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base + +**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a +`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch +directory, instead of a source repo? + +Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can +walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store +between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, +and both are yours, because the file you write is the only thing that sets them: + +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. + +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … + +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` + +## `./build/ripwire . --tree` + +*File-by-file orientation map (top symbols per file).* + +**wall time: 2.58s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [366 more display lines; full output is 11120 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 5.03s** + +````` +(empty) +````` + +Artifact written: + +````` + 158883 ./../../tmp/lean-answers-068/showcase_scratch/aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +**wall time: 1.51s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + + +--- + +# navigate / answer a question + +## `./build/ripwire . --around=rankGraphTeleport` + +*Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* + +**wall time: 1.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [225 more display lines; full output is 9149 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` + +*The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* + +**wall time: 1.58s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [2996 more display lines; full output is 79305 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` + +*The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* + +**wall time: 1.75s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [31 more display lines; full output is 4174 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +**wall time: 1.20s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --legend=compact` + +*The same rows under --legend=compact: the prose legend becomes one compact comment plus schema="ripwire.callers/v1" on the root — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB of legend gone. Works on EVERY XML verb now, not four.* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** — **wall time: 2.42s** + +````` + + +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +**wall time: 1.98s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +**wall time: 1.23s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +**wall time: 1.91s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3649 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface` + +*Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* + +**wall time: 1.55s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5563 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface --include-builtins` + +*The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* + +**wall time: 2.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5441 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --path=main,rankGraphTeleport` + +*Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* + +**wall time: 1.45s** + +````` + + + + + + + + +````` + +## `./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +**wall time: 2.67s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +**wall time: 2.21s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [48 more display lines; full output is 5280 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +**wall time: 3.00s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +**wall time: 3.23s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --situ` + +*Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* + +**wall time: 2.41s** + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: 1c08714cf + (0 changed files — working tree is clean, nothing to analyze) +````` + +## `./build/ripwire . --test-gate` + +*Pre-PR gate on a CLEAN tree: no obligations, exit 0.* + +**wall time: 14.17s** + +````` + + +````` + +## `./build/ripwire . --grep=DISCLOSE` + +*Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* + +**wall time: 2.37s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16956 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --match='(if_statement) @i'` + +*The same shape query WITH an explicit capture — identical hits, no auto_captured= attribute.* + +**wall time: 5.18s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16840 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --query="teleport pagerank" --top-k=5` + +*Raw BM25 ranking (debug lens; --for is the real verb).* + +**wall time: 1.95s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3359 bytes on 1 raw line(s)] +````` + + +--- + +# zoom the detail ladder + +## `./build/ripwire . --for="pagerank power iteration" --detail=2` + +*Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* + +**wall time: 4.41s** + +````` + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline std::uint32_t sliceRdIterationCeiling() noexcept +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] +## Background — read these, in this order +## Design space and constraints +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] +… [106 more display lines; full output is 13658 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --pack-signatures --top-k=10` + +*Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 92.7% fewer bytes at top-10, 88.7% at top-50, 87.6% at top-100 (re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* + +**wall time: 2.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [135 more display lines; full output is 11815 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` + +*Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* + +**wall time: 2.77s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +## `./build/ripwire . --outline=rankGraphTeleport:1-10 --top-k=0` + +*CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* + +**wall time: 2.08s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +stderr: + +````` +ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — outlining the whole symbol (use --expand=rankGraphTeleport:1-10 for a body slice) +````` + +## `./build/ripwire . --expand=rankGraphTeleport --top-k=0` + +*Full body + inline callee signatures.* + +**wall time: 3.20s** + +````` + + + + +, with a BRANCH-FREE size(). Both, not one or the other. +// +// ── THE DESIGN, AND WHY IT IS THIS ONE ─────────────────────────────────────────────────────────────── +// The shape the host tree leans on hardest is `Map>` — many tiny id-lists (byName / +// canonByName / shard maps, and ~100 more structures after the conversion wave): WRITE-ONCE during the +// parse/merge, then READ-HOT during resolve. +// +// Three things matter for that shape, and the layout below gets all three: +// • no per-list malloc — the N small lists that would each allocate are inline; +// • a BRANCH-FREE size() — `return sz_`, because the size lives in its own field; +// • 16 BYTES per instance — because `inl_` and `heap_` are never both live, so they share storage. +// +// The union is the whole trick. The previous revision of this file paid 8 extra bytes (24 B) for the +// explicit size field, on the reasoning that a branch-free size() was worth it. That was a false +// choice: union the inline array with the heap pointer and the struct reaches 16 B — ankerl's size — +// while the size stays in its own field and size() stays branch-free. +// +// THE TRADE THAT REMAINS, stated honestly. At 16 bytes you can have: +// • ankerl::svector's 3 inline slots, with a size() that branches on is_direct() (and, once spilled, +// dereferences into the heap block to read the size — a dependent load, not just a branch); or +// • this type's 2 inline slots, with size() branch-free. +// The measurement chose the second. See bench/SVECTORAB.md; the short version is below. +// +… [1088 more display lines; full output is 66245 bytes on 1114 raw line(s)] +````` + +stderr: + +````` +ripwire: --pack-top-n is deprecated — use --pack-task/--detail instead (unchanged behavior for now) +````` + + +--- + +# assess quality / structure + +## `./build/ripwire . --metrics --top-k=10` + +*Fan-in/out + complexity annotations on the map.* + +**wall time: 3.46s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [18 more display lines; full output is 5295 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +**wall time: 2.40s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [888 more display lines; full output is 23141 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 4.49s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5747 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 12.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [307 more display lines; full output is 16180 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 4.64s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 4.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5103 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --arch=test/archfix/rules.txt` + +*Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* + +**wall time: 2.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [563 more display lines; full output is 82067 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 9.45s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [696 more display lines; full output is 69779 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +**wall time: 3.09s** + +````` + + + + +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf + +````` + +stderr: + +````` +ripwire: lint-rules: test/lintrulesfix/rules/malformed.yaml:5: expected 'key: value' — file skipped +[math degraded] lint-rules: malformed rule file skipped (lintrules.h:395, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +**wall time: 2.00s** + +Input file: + +````` +#PROF_TSV_BEGIN one row per scope, aggregated across threads; counters are RAW integers +scope file line calls total_ms l1d_mpki +walk: chase pass x.cpp 9 12 48.500 7.250 +#PROF_TSV_END +````` + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 3428 bytes on 1 raw line(s)] +````` + +The joined finding — past the display cut above, extracted so the join is visible: + +````` +p = p->next +````` + +## `./build/ripwire . --communities` + +*Cluster the call graph into cohesive modules.* + +**wall time: 3.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [187 more display lines; full output is 16530 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom` + +*Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [171 more display lines; full output is 8315 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom --zoom-levels=3` + +*The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* + +**wall time: 3.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [701 more display lines; full output is 27553 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +**wall time: 1.63s** + +````` + + +# ripwire architecture report + +2484 files · 23849 symbols · 34232 edges · 2322 modules (10910 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 2889 declaration, 1577 header, 1922 source, 4522 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 2322) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1378 symbols +- **src::append@elixir_resolve.h:111:4875 [resolve,collect,add]** — 1136 symbols +- **src::compare@structlayout.h:166:4708 [read,close,parse]** — 538 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [emit,collect,push]** — 256 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:8861 [pack,allow,insert]** — 177 symbols +- **src::realpath@infra/os.h:199:13067 [parse,split,read]** — 63 symbols +- **src::relForHash@model.h:1438:116507 [compute,clone,read]** — 60 symbols +- **src::assign@infra/svector.h:342:19905 [scan,run,resolve]** — 54 symbols +- **src/infra::DYNMAP_ASSUME@dynamic_map.hpp:90:4411 [erase,find,count]** — 51 symbols +- **src/infra::active@profilePmc.h:845:34910 [print,ensure,report]** — 49 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [compute,connect,verify]** — 38 symbols +- **test::no@decltodefcheck.sh:127:11708 [run]** — 30 symbols + +## God files (most depended-on; showing 10 of 445) +- `test/lib/clean-env.sh` — 180 dependents +- `src/infra/emit.h` — 86 dependents +- `src/model.h` — 85 dependents +- `src/infra/Diagnostics.h` — 58 dependents +- `src/serialize.h` — 39 dependents +… [32 more lines, 3523 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +**wall time: 2.36s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [102 more display lines; full output is 13421 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +**wall time: 2.41s** + +````` +%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. +flowchart LR + subgraph sg0 ["src"] + n101["src
7066"] + n102["src/infra
1276"] + end + subgraph sg1 ["test"] + n103["test
4851"] + n328["test/fixtures/recallpassage
314"] + n280["test/expandmodefix
151"] + n282["test/expandsibsfix
149"] + n458["test/rubyattrsfix
85"] + n395["test/massfix
77"] + n360["test/javamethodreffix
62"] + end + n73["docs
831"] + n2[".github
598"] + subgraph sg4 ["bench"] + n4["bench
531"] + n5["bench/agentloop
331"] + n49["bench/locbench/results/r5_pooling
237"] + n50["bench/locbench/results/r6_expansion
187"] + n59["bench/recalleval
113"] + n43["bench/locbench
107"] + n38["bench/headtohead/r3-headroom-2026-08-03
102"] + n65["bench/skillrater/results/2026-09-07/arms
100"] + n62["bench/shotgun
96"] + n45["bench/locbench/results/r1cpp_anchorhop
94"] + n11["bench/arb
85"] + n56["bench/nestcal/r1-2026-08-07
84"] +… [21 more lines, 1606 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 7.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [884 more display lines; full output is 97595 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --dead-code=src` + +*Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* + +**wall time: 2.88s** + +````` + + + + +````` + +## `./build/ripwire . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +**wall time: 2.20s** + +````` + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +**wall time: 2.44s** + +````` + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 71.17s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [106 more display lines; full output is 13491 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --edit-check=rankGraphTeleport` + +*Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* + +**wall time: 20.64s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +**wall time: 2.31s** + +````` + + + +````` + +## `./build/ripwire . --pr-context=HEAD~1` + +*The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* + +**wall time: 5.67s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [256 more display lines; full output is 17060 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 28.10s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 8.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [304 more display lines; full output is 37801 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` + +*A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* + +**wall time: 3.56s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/ --plan` + +*Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* + +**wall time: 183.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60154 more display lines; full output is 7430341 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` + +*A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* + +**exit code: 1** — **wall time: 2.28s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zero here would be a failure, not a measurement + (the filter is a substring match against refs/heads names; run bare --stray-content to list them, e.g. --stray-content=feat/) +````` + +## `./build/ripwire . --stray-content=lane/ --abi` + +*Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* + +**exit code: 2** — **wall time: 27.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [37 more display lines; full output is 4510 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=rankGraphTeleport` + +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch.* + +**wall time: 33.01s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [34 more display lines; full output is 13695 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` + +*The whole hit list, references included: the page the default is measured against. The default lists only the definitions (listing="defs", the references counted in ) when that page is strictly shorter in bytes, and this page otherwise.* + +**wall time: 22.64s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [34 more display lines; full output is 13695 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` + +*Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* + +**wall time: 29.68s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 15174 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags` + +*The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* + +**wall time: 2.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [366 more display lines; full output is 22644 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` + +*Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* + +**wall time: 1.32s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASA` + +*Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* + +**exit code: 1** — **wall time: 1.35s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --flip: no gate named 'RIPWIRE_ASA' in . (did you mean 'RIPWIRE_ASAN'?) +ripwire: run `ripwire . --flags` for the gate table +````` + +## `./build/ripwire . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it with tests"` + +*NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* + +**wall time: 2.92s** + +````` +{"v":1,"verb":"plan-lanes","at":"1c08714cf","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":342 … [line truncated: 339 more bytes on this line] +"symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2909,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":640,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1642,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2685,"ord":0,"overloads":1,"amb":3,"cx":6,"ccx":6,"churn":36,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":2,"cx":8,"ccx":8,"churn":31,"tested":0}, +{"p":"./src/docdrift.h","n":"docDriftNextAttr","scope":"docdrift","key":"7723ed789a1c9c09","id":"./src/docdrift.h::docdrift::docDriftNextAttr","id_addressable":true,"id_collides_with":0,"l":2864,"ord":0,"overloads":1,"amb":0,"cx":6,"ccx":5,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"recordWeakDisclosures","scope":"docdrift","key":"972779210e49c0cf","id":"./src/docdrift.h::docdrift::recordWeakDisclosures","id_addressable":true,"id_collides_with":0,"l":2430,"ord":0,"overloads":1,"amb":2,"cx":2,"ccx":1,"churn":36,"tested":0}, +{"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1344,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":48,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":2863,"ord":0,"overloads":1,"amb":33,"cx":8,"ccx":11,"churn":414,"tested":0}, +{"p":"./src/gitmine.h","n":"resolveSinceScope","scope":"rw","key":"9caaa3dbeaa688d0","id":"./src/gitmine.h::rw::resolveSinceScope","id_addressable":true,"id_collides_with":0,"l":326,"ord":0,"overloads":1,"amb":3,"cx":7,"ccx":7,"churn":59,"tested":0}, +{"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":350,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":18,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":685,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":414,"tested":0}, +{"p":"./src/taskroute.h","n":"testCoverageTaskChoice","scope":"rw::taskroute","key":"bf72991aa28355c7","id":"./src/taskroute.h::rw::taskroute::testCoverageTaskChoice","id_addressable":true,"id_collides_with":0,"l":1365,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":34,"tested":0}, +{"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5191,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":308,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":31,"ccx":8,"hotspot_rank":45}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":31}, +{"p":"./src/gitmine.h","symbols":1,"churn":59,"ccx":7,"hotspot_rank":19}, +{"p":"./src/main.cpp","symbols":2,"churn":414,"ccx":12,"hotspot_rank":4}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":308,"ccx":2,"hotspot_rank":5}, +{"p":"./src/pageview.h","symbols":1,"churn":18,"ccx":2,"hotspot_rank":134}, +{"p":"./src/taskroute.h","symbols":1,"churn":34,"ccx":2,"hotspot_rank":40}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":34}, +{"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":8,"hotspot_rank":23}]},"blast_radius":{"reaches":51,"files_total":17,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editplan.h","./src/flipimpact.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h","./src/mcpedit.h","./s … [line truncated: 203 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":51,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"high","rule":"complex-or-wide","basis":"structural-only","signals":{"claims":13,"files":9,"module_span":1,"max_ccx":11,"sum_ccx":59,"ambiguous_calls":49,"blast_reaches":51,"contract_touches":0,"conflicts":0,"untested":51,"tests … [line truncated: 135 more bytes on this line] +… [77 more display lines; full output is 21301 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=./../../tmp/lean-answers-068/showcase_scratch/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +**wall time: 2.23s** + +Input file: + +````` +add a --since filter to the doc-drift verb +add the CLI parse arm and help text for the new filter +write regression tests for the new filter +````` + +````` +{"v":1,"verb":"plan-lanes","at":"1c08714cf","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2484,"symbols":23849,"edges":34232,"ambiguous":10278,"unresolved":12860},"carve":null,"core":{"files … [line truncated: 5 more bytes on this line] +"symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] +{"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":640,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":308,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":12,"cx":14,"ccx":15,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":12,"cx":26,"ccx":38,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1642,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":70,"tested":0}, +{"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":67,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":1,"churn":434,"ccx":0,"hotspot_rank":6}, +{"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":31}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":87}, +{"p":"./src/mcprefusal.h","symbols":1,"churn":40,"ccx":0,"hotspot_rank":55}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":308,"ccx":0,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":52}, +{"p":"./src/verbs_change.h","symbols":1,"churn":70,"ccx":4,"hotspot_rank":23}], +"symbols_total":3137,"symbols_capped":true},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":12,"module_span":6,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":7,"module_span":6,"max_ccx":38,"sum_ccx":62,"ambiguous_calls":26,"blast_reaches":12,"contract_touches":1,"conflicts":3,"untested":12,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":107,"l":271,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":10, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2825,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3201,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2904,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":434,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":107,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, +{"p":"./src/slice.h","n":"sliceRdIterationCeiling","scope":"slicev","key":"449132b302bb4324","id":"./src/slice.h::slicev::sliceRdIterationCeiling","id_addressable":true,"id_collides_with":0,"l":1372,"ord":0,"overloads":1,"amb":0,"cx":9,"ccx":12,"churn":51,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":40,"tested":0}, +… [63 more display lines; full output is 19235 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes=99 --task=x` + +*Out-of-range refusal shape for the lane count.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out) +````` + +## `./build/ripwire . --layout=Symbol` + +*CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* + +**wall time: 1.44s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped enum's underlying type is not a byte layout) +````` + +## `./build/ripwire . --doc-drift` + +*Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* + +**wall time: 1.15s** + +````` + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +… [243 more display lines; full output is 25193 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --doc-drift --gateability` + +*The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* + +**wall time: 1.19s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [269 more display lines; full output is 26392 bytes on 1 raw line(s)] +````` + +Tail of the same output — the `` section: + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doc-drift --with-history` + +*Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* + +**wall time: 2.06s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [221 more display lines; full output is 25107 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --from-trace=-` + +*Map a pasted stack trace onto indexed symbols. CHANGED: in_corpus= now reports the real count (was 0).* + +Input file: + +````` +AddressSanitizer:DEADLYSIGNAL +================================================================= +==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:5567 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:5608 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1631 + #3 0x102e01a44 in main src/main.cpp:3975 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) +==41337==ABORTING +````` + +````` + + + + + + + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) +int runDefaultMap( const MainDispatch& d ) +int main( int argc, char** argv ) + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; +… [18 more display lines; full output is 5100 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --notes` + +*List all field notes (write-side memory) — the committed .ripwire_notes at the repo root, each with the sha/branch it was recorded at.* + +````` + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI"` + +*ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* + +**wall time: 1.12s** + +````` + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 83 more bytes on this line] + +mcp-tools pass-through. Written into the server command only where its argument shape is known to be one more plain argument: the JSON args arrays (cursor/windsurf/gemini), opencode's command arrayinline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + +knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedBy makes in full above, which applies here verbatim: firstFlagOutside() walks the rows parseArgs itself minline constexpr std::string_view kInRideAlong[] = + +F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexcept + + + + + + + + +… [47 more display lines; full output is 9435 bytes on 38 raw line(s)] +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` + +*Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* + +**wall time: 1.14s** + +````` + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. --> + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const WrapListing& listing ) noexcept + +mcp-tools pass-through. Written into the server command only where its argument shape is known t…inline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + + + + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline std::uint32_t sliceRdIterationCeiling() noexcept +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +struct PageRankRun +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Same answer, a fraction of the tokens — read this table first if your agent is on a budget +### Anchor-only auto bodies — T3 substitution round, PRE-REGISTERED 2026-08-22 (before any fix code)< … [line truncated: 3 more bytes on this line] +## Background — read these, in this order +## Design space and constraints +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "src/ingest_astquery.h", "the --match / --lint AS … [line truncated: 40 more bytes on this line] +… [60 more display lines; full output is 9335 bytes on 10 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +**wall time: 1.91s** + +````` +(empty) +````` + +Artifact written: + +````` + 374998 ./../../tmp/lean-answers-068/showcase_scratch/aux/ripwire2.cc.json +{"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri +````` + +## `./build/ripwire . --batch=./../../tmp/lean-answers-068/showcase_scratch/aux/batch2.txt` + +*One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* + +**wall time: 5.91s** + +Input file: + +````` +for:incremental cache invalidation +callers:rankGraphTeleport +grep:DISCLOSE +lego:Vehicle +````` + +````` + + + + + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) + +Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] + +P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) +struct FileHealth +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vec… +inline const McpIndex& getIndex( const std::string& root ) +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +struct McpIndex +### See the map — not just the numbers +inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& cacheStats … [line truncated: 59 more bytes on this line] +IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool respect … [line truncated: 29 more bytes on this line] +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) +static bool cachePathIsDirectory( const std::string& cachePath ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +… [57 more display lines; full output is 41093 bytes on 1 raw line(s)] +````` + + +--- + +# self-diagnosis + +## `./build/ripwire . --doctor` + +*Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* + +**exit code: 1** — **wall time: 6.80s** + +````` + + + + + + + + + + +````` + + +--- + +# security + +## `./build/ripwire --scan-skill=skills/ripwire-orient/SKILL.md` + +*Scan a single skill file for injection/exfiltration patterns before installing.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) in skills/ripwire-orient/SKILL.md +````` + +## `./build/ripwire --scan-skills=skills` + +*Scan a whole skills directory (exit 2 = CRITICAL, 1 = WARN). Explicit-DIR form only.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s) skipped, 0 denylisted subtree(s) not descended) +````` + + +--- + +# knobs / modes + +## `./build/ripwire . --rank-by=churn --top-k=5` + +*Rank by git change-frequency prior instead of PageRank.* + +**wall time: 2.63s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=bogus --top-k=5` + +*An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf|churn|churn-decay) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=columnar` + +*Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* + +**wall time: 1.09s** + +````` + + +0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp + +0,1,1,2,3,3,3 +getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap +1165,5606,6252,171,1380,1419,1629 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +**wall time: 1.20s** + +````` + + + +inline std::string spanTierMemoPath( const std::string& diskPath ) + + +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) + + +### See the map — not just the numbers + + +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) + + +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=bogus` + +*An unknown --format value REFUSES (exit 1), named, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidates) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --json` + +*Machine-parseable JSON, same content, keys mirror the XML attrs.* + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":10278,"graph_unresolved":12860,"graph_unindexed":237,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1165"}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:5606"}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:6252"}, +{"t":"fn","n":"runEval","p":"src/eval.h:171"}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1380"}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1419"}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1629"}]} +````` + +## `./build/ripwire . --hotspots --json` + +*JSON refusal shape: an unsupported verb refuses loudly instead of silently falling back to XML.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --json is not yet supported for --hotspots — supported: the default map, --for, --pack-task, --callers/--callees, --impact, --quality-delta, --test-gate, --metrics, and --plan-lanes which is JSON-native (e.g. ripwire